Compare commits

..
261 Commits
Author SHA1 Message Date
Kim Gardner 5168f563ef Merge pull request #557 from coralproject/admin-queue-polish
load more comments to moderate if available
2017-05-03 18:02:35 -04:00
Kim Gardner 7d0d4aa397 Merge branch 'master' into admin-queue-polish 2017-05-03 17:57:34 -04:00
Kim Gardner 4f2126ef74 Merge pull request #562 from coralproject/flag-queue-loadmore
Flag queue Load More fix
2017-05-03 17:35:53 -04:00
riley d02c82d0cd Merge branch 'master' into admin-queue-polish 2017-05-03 14:25:46 -06:00
Riley Davis 1cecad4d32 action_type must be passed to the load more query 2017-05-03 14:13:58 -06:00
Riley Davis 6b4505fa3d Merge branch 'master' into flag-queue-loadmore 2017-05-03 13:42:25 -06:00
Riley Davis f03c87c26d Merge pull request #561 from coralproject/like-fix
Removed LikeAction from core typeName interface resolver
2017-05-03 13:42:01 -06:00
riley c15c312c2c typo 2017-05-03 13:35:33 -06:00
Wyatt Johnson d9a4653484 Removed LikeAction from core typeName interface resolver 2017-05-03 13:31:57 -06:00
Riley Davis e993328dc4 load only flagged comments in mod queue load more 2017-05-03 13:15:29 -06:00
Belén Curcio 15afce516c Merge pull request #556 from coralproject/coral-plugin-like
Coral Plugin Like
2017-05-03 15:50:09 -03:00
riley 3c6391e741 count should never be less than 0 2017-05-03 12:49:22 -06:00
riley 39bd0eca6f remove pointless change 2017-05-03 12:45:49 -06:00
Belen Curcio 99b3d0021a Linting 2017-05-03 15:45:31 -03:00
Belen Curcio 13a6ee67e9 Merge branch 'coral-plugin-like' of github.com:coralproject/talk into coral-plugin-like 2017-05-03 15:39:41 -03:00
Belen Curcio 32fb0714ce updated tests 2017-05-03 15:37:56 -03:00
Belén Curcio 7cc8176cc4 Merge branch 'master' into coral-plugin-like 2017-05-03 15:31:04 -03:00
riley 354b51b4fe use a reduce instead 2017-05-03 11:14:21 -06:00
Riley Davis ae52ec983e Merge remote-tracking branch 'origin/admin-queue-polish' into admin-queue-polish 2017-05-03 10:55:43 -06:00
Riley Davis c78e00711a remove if statements 2017-05-03 10:54:52 -06:00
Riley Davis b8ddade518 Merge branch 'master' into admin-queue-polish 2017-05-03 10:28:52 -06:00
Riley Davis 324e82c070 not pretty, but it works 2017-05-03 10:20:03 -06:00
Kim Gardner e6651086d5 Merge pull request #560 from coralproject/bump-version-1-6-0
Bump version to 1.6.0
2017-05-03 12:17:51 -04:00
Kim Gardner 28ea498df2 Bump version to 1.6.0 2017-05-03 12:12:25 -04:00
riley e60bcb9caf make sure load more on approve/reject queues doesn't cause a bug 2017-05-02 15:58:57 -06:00
riley b7d3113677 apparently you can't make a fragment on RootQuery 2017-05-02 15:18:48 -06:00
Kim Gardner f6e2caf65f Merge branch 'master' into admin-queue-polish 2017-05-02 12:38:48 -04:00
Kim Gardner cdc1835f72 Merge branch 'master' into coral-plugin-like 2017-05-02 12:37:55 -04:00
Kim Gardner 9d2b47f02b Merge pull request #549 from coralproject/count-abbr
format mod queue counts
2017-05-02 12:37:34 -04:00
Riley Davis da5e8cda99 Merge branch 'master' into admin-queue-polish 2017-05-01 11:13:13 -06:00
Riley Davis 32dd554331 load more comments if the mod has moderated them all and there are more availabile 2017-05-01 10:30:18 -06:00
Riley Davis 8293408c52 pull some custom code out of ActionButton 2017-05-01 10:22:11 -06:00
Belen Curcio 23a5b236a2 Linting 2017-05-01 12:05:45 -03:00
Belen Curcio b02088e329 Like color 2017-05-01 12:04:14 -03:00
Belen Curcio 1a50b6cc85 Ádding like as a reaction 2017-05-01 12:00:44 -03:00
Belen Curcio f1bebaa627 Merge 2017-05-01 11:54:00 -03:00
Belen Curcio 1250054fb7 Prettier 2017-05-01 11:45:23 -03:00
Belen Curcio f3980a03b2 Removing like from core action summary 2017-05-01 11:44:24 -03:00
Belen Curcio a90802a046 Coral-Plugin-Like 2017-05-01 11:43:59 -03:00
Belen Curcio 400615cc58 Adding translations 2017-05-01 11:43:40 -03:00
David Erwin a94f6220f9 Merge branch 'master' into count-abbr 2017-05-01 10:40:20 -04:00
Belén Curcio f509d29a94 Merge pull request #540 from coralproject/slots-b
Slots
2017-05-01 11:28:02 -03:00
Belen Curcio 8bed64373e Removing LIKE from talk 2017-05-01 11:15:53 -03:00
Belen Curcio 322434f751 Removing LIKE from talk 2017-05-01 11:13:35 -03:00
Belen Curcio 9ee749edb2 Removing LIKE from the core graph 2017-05-01 11:00:37 -03:00
Belen Curcio 5c14ac3f67 Ádding LIKE to the plugin graph 2017-05-01 10:58:29 -03:00
Belen Curcio e1929f148a Removing likes from the core graph 2017-05-01 10:58:14 -03:00
Belen Curcio 717ebf40b7 Adding like plugin to the ignore 2017-05-01 10:56:10 -03:00
Belen Curcio ef40174be9 Adding Coral-Plugin-Like 2017-05-01 10:55:50 -03:00
Belen Curcio 3b819144e0 Merge branch 'slots-b' of github.com:coralproject/talk into slots-b 2017-05-01 10:48:04 -03:00
Belen Curcio c58813cf48 merge 2017-05-01 10:47:56 -03:00
Belen Curcio d14b965f54 Adding fragment slots 2017-05-01 10:46:37 -03:00
Belén Curcio ed88a59ef1 Merge branch 'master' into slots-b 2017-05-01 10:41:21 -03:00
Kim Gardner ffcb1b464f Merge pull request #548 from coralproject/action-count-fix
don't double count no-ops
2017-04-28 15:13:37 -04:00
Kim Gardner fd11cc5249 Merge branch 'master' into action-count-fix 2017-04-28 15:09:44 -04:00
Kim Gardner 9b155833a1 Merge branch 'master' into count-abbr 2017-04-28 15:07:18 -04:00
Kim Gardner 2484141ecf Merge pull request #553 from coralproject/load-more-accepted
enable load more on accepted queue
2017-04-28 15:07:06 -04:00
Kim Gardner 78ea42f34f Merge branch 'master' into load-more-accepted 2017-04-28 15:03:02 -04:00
Kim Gardner 3a1e78579a Merge pull request #554 from coralproject/yarn-fix
Updated yarn.lock
2017-04-28 15:02:48 -04:00
Riley Davis c85596a9bf Merge branch 'master' into load-more-accepted 2017-04-28 12:59:01 -06:00
David Erwin 211a6fd868 Merge branch 'master' into yarn-fix 2017-04-28 14:58:02 -04:00
David Erwin 59b048002b Merge pull request #555 from coralproject/fix-more-reply-count
Fix reply count polling
2017-04-28 14:57:50 -04:00
Kim Gardner 5506481182 Merge branch 'master' into load-more-accepted 2017-04-28 14:57:07 -04:00
Kim Gardner 12e6501d4d Merge branch 'master' into fix-more-reply-count 2017-04-28 14:51:01 -04:00
Kim Gardner 7a53cab2f5 Merge branch 'master' into action-count-fix 2017-04-28 14:44:28 -04:00
Kim Gardner cfbff62d1c Merge pull request #550 from coralproject/losowsky-patch-1
tiny typo that was bugging me
2017-04-28 14:43:41 -04:00
Wyatt Johnson 5d4c813800 removed empty gif 2017-04-28 12:03:29 -06:00
Chi Vinh Le e486d48556 postComment should increase replyCount 2017-04-29 01:02:59 +07:00
Chi Vinh Le 8663aa0cb5 Fix polling reply count 2017-04-29 01:02:11 +07:00
Wyatt Johnson f1e70102d0 Updated yarn.lock 2017-04-28 11:55:28 -06:00
Riley Davis 41d4bd1012 counts update 2017-04-28 11:52:07 -06:00
riley ace3e2398b enable load more on accepted queue 2017-04-28 11:41:03 -06:00
riley 521082eb3e add new query for counts 2017-04-28 11:39:37 -06:00
Riley Davis a6dd03beef Merge branch 'master' into action-count-fix 2017-04-28 10:45:34 -06:00
Riley Davis 7246adc467 Merge branch 'master' into count-abbr 2017-04-28 10:45:27 -06:00
Kim Gardner 4d0b5b7149 Merge branch 'master' into losowsky-patch-1 2017-04-28 11:24:27 -04:00
Kiwi bfb2eb30e2 Merge pull request #552 from coralproject/fix-empty-slot-fragments
Ret empty string when encountering undefined fragment
2017-04-28 22:04:44 +07:00
Chi Vinh Le 5d26f8e3de Ret empty string when encountering undefined fragment 2017-04-28 21:59:02 +07:00
Andrew Losowsky a46c8528e1 tiny typo that was bugging me 2017-04-28 07:19:44 -04:00
riley cd9648b251 format mod queue counts 2017-04-27 15:01:59 -06:00
riley 59ddde1856 semi 2017-04-27 14:07:01 -06:00
riley 862355b0cd don't double count no-ops 2017-04-27 14:02:29 -06:00
Kim Gardner 78878b4295 Merge pull request #539 from coralproject/approved-comments-queue
Approved comments queue
2017-04-27 15:28:00 -04:00
Riley Davis b1d4fff929 Merge branch 'master' into approved-comments-queue 2017-04-27 13:12:40 -06:00
riley 8cf11d83c8 Merge branch 'approved-comments-queue' of github.com:coralproject/talk into approved-comments-queue 2017-04-27 13:10:50 -06:00
riley a3ec23c273 mange counts and such for all queue 2017-04-27 13:09:00 -06:00
Kim Gardner af9f143b31 Merge pull request #547 from coralproject/lock-fix
Updated yarn.lock
2017-04-27 15:05:52 -04:00
Wyatt Johnson 48bffdbb3b Updated yarn.lock 2017-04-27 13:00:30 -06:00
Kiwi 8f1448823c Merge pull request #543 from coralproject/no-logic-user-box
Remove embed stream logic from UserBox
2017-04-28 01:07:56 +07:00
Kim Gardner 6a46fb822d Merge branch 'master' into approved-comments-queue 2017-04-27 13:56:15 -04:00
Kiwi b186a74199 Merge branch 'master' into no-logic-user-box 2017-04-28 00:55:50 +07:00
David Erwin e96c5bbc8f Merge pull request #542 from coralproject/optimise-initial-query
Only query once during initial render
2017-04-27 13:53:56 -04:00
David Erwin 06378209a2 Merge branch 'master' into optimise-initial-query 2017-04-27 13:49:12 -04:00
riley f6cede3fda add accepted to propTypes 2017-04-27 11:23:27 -06:00
riley d5b800f590 add accepted rejected aciton buttons 2017-04-27 11:17:53 -06:00
Riley Davis 8cade04431 Merge branch 'master' into approved-comments-queue 2017-04-27 11:07:43 -06:00
David Erwin efda925efc Merge pull request #546 from coralproject/sub-fix
Moved subscription dep to devDeps
2017-04-27 12:53:04 -04:00
Wyatt Johnson ec915208e5 Moved dep to devDeps 2017-04-27 10:48:03 -06:00
Kim Gardner af80c4d8ad Merge branch 'master' into approved-comments-queue 2017-04-27 11:59:30 -04:00
Kim Gardner 1de3614fe8 Merge branch 'master' into slots-b 2017-04-27 11:59:23 -04:00
Kim Gardner 663bce2875 Merge branch 'master' into no-logic-user-box 2017-04-27 11:59:08 -04:00
Kim Gardner d0090283c0 Merge pull request #535 from coralproject/report-popup-fix
position the reporting popup in a more reliable way
2017-04-27 11:58:43 -04:00
Chi Vinh Le 8c53bc2d22 Postpone first query until checked login 2017-04-27 22:55:36 +07:00
Kim Gardner f47bd269e5 Merge branch 'master' into report-popup-fix 2017-04-27 11:48:17 -04:00
Kim Gardner 7a99c9358a Merge pull request #533 from coralproject/143530761-remove-loading-when-searching
Only shows the Loading component when there is no search value
2017-04-27 11:47:50 -04:00
Riley Davis 6330d69649 Merge branch 'master' into approved-comments-queue 2017-04-27 09:42:28 -06:00
Kim Gardner 6e98d176e7 Merge branch 'master' into 143530761-remove-loading-when-searching 2017-04-27 11:41:51 -04:00
Kim Gardner 7b84b3daf1 Merge pull request #537 from coralproject/profanity-verbiage
update bad username copy
2017-04-27 11:41:24 -04:00
Riley Davis 40b7669ed8 Merge branch 'master' into approved-comments-queue 2017-04-27 09:40:30 -06:00
Kim Gardner 3bb994c562 Merge branch 'master' into profanity-verbiage 2017-04-27 11:35:47 -04:00
Belén Curcio 9c97d85859 Merge branch 'master' into slots-b 2017-04-27 12:31:07 -03:00
Riley Davis ea8e469c1c Merge branch 'master' into report-popup-fix 2017-04-27 09:28:00 -06:00
David Erwin 6e3159c7e1 Merge pull request #545 from coralproject/docker-fix
Fixed Dockerfile for regression with yarn
2017-04-27 10:34:26 -04:00
Wyatt Johnson 5559940967 Fixed dockerfile temp, updated yarn.lock 2017-04-27 08:29:16 -06:00
David Erwin 64eb16c3d6 Merge pull request #519 from coralproject/more-fragment-matcher
more matching on the fragment
2017-04-27 10:17:58 -04:00
Belén Curcio f779c5fe01 Merge branch 'master' into no-logic-user-box 2017-04-27 11:12:55 -03:00
David Erwin 6972b87b0b Merge branch 'master' into more-fragment-matcher 2017-04-27 10:10:19 -04:00
Belén Curcio 8c320d09da Merge branch 'master' into optimise-initial-query 2017-04-27 11:04:11 -03:00
David Erwin 279ff0655f Merge pull request #544 from coralproject/login-ios-safari
More reliable login support including IOS browsers
2017-04-27 10:03:29 -04:00
Chi Vinh Le 794cc0fc6c Support login on all IOS browsers 2017-04-27 19:28:54 +07:00
Chi Vinh Le 7b16a71e9b Remove embed stream logic from UserBox 2017-04-27 18:20:55 +07:00
Chi Vinh Le 20b4c4e2d0 Only query once during initial render 2017-04-27 17:30:43 +07:00
Belen Curcio 37e093f60a Linting 2017-04-26 23:15:40 -03:00
Belen Curcio de6492a26d Ádding reactions, commentContent and updating current Slot 2017-04-26 23:06:09 -03:00
Belen Curcio c60db41047 Updating slot name 2017-04-26 23:04:22 -03:00
Belen Curcio 00fe4b7c83 Úpdating slot names 2017-04-26 23:03:51 -03:00
Belen Curcio e0b177d955 Updated slot name 2017-04-26 23:02:09 -03:00
Belen Curcio 29e398c51d Rename Slot to commentInputDetailArea in Coral Plugin Offtopic 2017-04-26 22:37:37 -03:00
Belen Curcio 3dd2da96e8 Adding Slots to QuestionBox, ref 2017-04-26 22:35:30 -03:00
Belen Curcio 1fea1405ef Adding Slots, CommentBox ref 2017-04-26 22:34:38 -03:00
Belen Curcio 9fa3e361b5 Ref Stream 2017-04-26 22:34:17 -03:00
Belen Curcio 06ba5a53c1 Stateless Slot 2017-04-26 22:33:49 -03:00
David Erwin bb532b5eb2 Merge pull request #531 from coralproject/shortcuts-in-cog
shortcuts list should be accessible from the cog
2017-04-26 17:41:04 -04:00
David Erwin 5eb26946cb Merge branch 'master' into profanity-verbiage 2017-04-26 17:21:34 -04:00
David Erwin 81680442f0 Merge branch 'master' into shortcuts-in-cog 2017-04-26 17:20:37 -04:00
David Erwin c86f9877f3 Merge pull request #534 from coralproject/143202319-mod-queue-shortcuts
Recover the scroll.
2017-04-26 17:20:17 -04:00
David Erwin 1b3868edec Merge branch 'master' into 143202319-mod-queue-shortcuts 2017-04-26 17:13:46 -04:00
David Erwin 4936003f5f Merge pull request #536 from coralproject/fix-case
make Comment comment
2017-04-26 17:13:28 -04:00
David Erwin 5c4696ed61 Merge branch 'master' into fix-case 2017-04-26 17:08:56 -04:00
David Erwin bf898fbaec Merge pull request #460 from coralproject/subscriptions
Subscriptions
2017-04-26 17:05:58 -04:00
David Erwin 41065ba24a Merge branch 'master' into subscriptions 2017-04-26 17:01:27 -04:00
David Erwin 0e77c93ae5 Merge pull request #521 from coralproject/user-metadata
Added user metadata
2017-04-26 17:01:15 -04:00
David Erwin 2756b794ee Merge branch 'master' into more-fragment-matcher 2017-04-26 16:57:37 -04:00
David Erwin 626f5cd1e1 Merge branch 'master' into user-metadata 2017-04-26 16:56:50 -04:00
David Erwin 077daeb793 Merge branch 'master' into subscriptions 2017-04-26 16:55:32 -04:00
Riley Davis fc567e8a92 Merge branch 'master' into shortcuts-in-cog 2017-04-26 13:34:13 -06:00
gaba 60d2735fa5 Remove isfetching all together. 2017-04-26 14:26:22 -05:00
gaba 6f58754617 Removing the Loading all together. 2017-04-26 14:26:21 -05:00
Gabriela Rodríguez Berón e935fff8ac Merge branch 'master' into 143530761-remove-loading-when-searching 2017-04-26 12:52:30 -05:00
Gabriela Rodríguez Berón 3ad8d9ffec Merge branch 'master' into 143202319-mod-queue-shortcuts 2017-04-26 12:51:30 -05:00
Riley Davis 0df40c7e35 make the popup not full width like before 2017-04-26 11:50:17 -06:00
Riley Davis a793d63f65 Merge branch 'master' into profanity-verbiage 2017-04-26 11:45:20 -06:00
Riley Davis ccc512cddd if there is no available translation, do not show error.whatever in the UI 2017-04-26 11:43:34 -06:00
David Erwin e20b6e1b25 Merge branch 'master' into fix-case 2017-04-26 13:19:50 -04:00
Riley Davis 4ff37d9063 Merge branch 'master' into approved-comments-queue 2017-04-26 11:01:41 -06:00
David Erwin c02890af00 Merge pull request #526 from coralproject/query-composition
Query composition
2017-04-26 12:58:51 -04:00
Riley Davis cea0b4942a prevent text overflow 2017-04-26 09:32:00 -06:00
Riley Davis 4ddaca7655 español 2017-04-26 09:20:23 -06:00
Riley Davis 03e0c62b72 accepted => approved 2017-04-26 08:45:37 -06:00
Riley Davis a21d73dcad accepted queue 2017-04-26 08:26:29 -06:00
Kiwi b20d8c18a3 Merge branch 'master' into report-popup-fix 2017-04-26 21:22:23 +07:00
David Erwin 85674b3ef5 Merge branch 'master' into query-composition 2017-04-26 10:09:14 -04:00
David Erwin 80f695e23d Merge branch 'master' into profanity-verbiage 2017-04-26 09:53:19 -04:00
David Erwin 8b4c2876c7 Merge pull request #538 from coralproject/ios-safari-full-width
Safari IOS ignores width=100% in iframes
2017-04-26 09:44:01 -04:00
Chi Vinh Le d56c35ea64 Merge branch 'master' into query-composition 2017-04-26 19:28:46 +07:00
Chi Vinh Le 9ff2ccf1ed Safari IOS ignores width=100% in iframes 2017-04-26 18:52:30 +07:00
riley 0807663965 add z-index to popup 2017-04-25 16:31:33 -06:00
riley f296fe2abd update bad username copy 2017-04-25 16:22:31 -06:00
Kim Gardner 2654e3d452 Merge branch 'master' into user-metadata 2017-04-25 18:21:45 -04:00
Kim Gardner 2c2c2dc604 Merge pull request #502 from coralproject/all-comments-queue
All comments queue
2017-04-25 18:20:36 -04:00
riley aad66ec7e0 fix casing 2017-04-25 16:18:21 -06:00
Kim Gardner 94e429e431 Merge branch 'master' into all-comments-queue 2017-04-25 18:16:01 -04:00
Riley Davis 7db5e4d68b Merge branch 'master' into report-popup-fix 2017-04-25 16:14:56 -06:00
riley f3822d09b2 position the reporting popup in a more reliable way 2017-04-25 15:54:18 -06:00
Gabriela Rodríguez Berón 9e35b7b39f Merge branch 'master' into 143202319-mod-queue-shortcuts 2017-04-25 16:10:23 -05:00
gaba 33d71df049 Recover the scroll. 2017-04-25 15:52:52 -05:00
David Erwin 7d0aac5d4a Merge pull request #532 from coralproject/fix-view-more
Fix: View More working with asset_id
2017-04-25 16:01:38 -04:00
gaba 10cd53b40b Only shows the Loading component when there is no search value 2017-04-25 14:28:37 -05:00
David Erwin d4c4b5a3e9 Merge branch 'master' into fix-view-more 2017-04-25 15:26:20 -04:00
Riley Davis b88cbe30de have all the comments show up in All Queue with proper statuses 2017-04-25 13:23:39 -06:00
Belen Curcio 9c448d5143 View More working with asset_id 2017-04-25 16:17:10 -03:00
Chi Vinh Le cb5228a423 Document getSlotFragments 2017-04-26 01:58:07 +07:00
Kim Gardner 4f70a24a32 Merge branch 'master' into subscriptions 2017-04-25 14:54:07 -04:00
Kim Gardner 62835acc76 Merge branch 'master' into user-metadata 2017-04-25 14:53:30 -04:00
riley 67301e6e85 Merge branch 'comment-status-display' into all-comments-queue 2017-04-25 12:52:32 -06:00
riley ad4386b235 Merge branch 'all-comments-queue' into comment-status-display 2017-04-25 12:51:20 -06:00
Riley Davis a94e6a0ada have default...just in case 2017-04-25 12:40:28 -06:00
Riley Davis 89f0663723 shortcuts list should be accessible from the cog 2017-04-25 12:38:00 -06:00
Chi Vinh Le eb9846ba40 Merge branch 'master' into query-composition
Conflicts:
	client/coral-embed-stream/src/Embed.js
	client/coral-embed-stream/src/Stream.js
2017-04-26 01:22:14 +07:00
Chi Vinh Le 9d7524644c Merge branch 'master' into query-composition 2017-04-26 01:11:07 +07:00
Chi Vinh Le f142f3a27f Introduce separateDataAndRoot 2017-04-26 01:10:16 +07:00
Chi Vinh Le df2c33ac35 Introduce getDefinitionName 2017-04-26 01:07:07 +07:00
Riley Davis 5666c4ac90 condense some var definitions 2017-04-25 11:57:36 -06:00
Riley Davis 37ac5157dd Merge branch 'master' into comment-status-display 2017-04-25 11:45:27 -06:00
Riley Davis 5db9ea6520 Merge pull request #520 from coralproject/shortcut-hint
add a little keyboard shortcut hint widget
2017-04-25 11:44:18 -06:00
Chi Vinh Le f79af56468 Better API for slot fragments 2017-04-26 00:44:02 +07:00
Belén Curcio a4b9f86e29 Merge branch 'master' into shortcut-hint 2017-04-25 14:38:47 -03:00
Riley Davis cee1c5bc1c have active state on action buttons depending on comment status 2017-04-25 11:21:28 -06:00
Chi Vinh Le 15d410ba12 Workaround weird Apollo bug 2017-04-25 23:20:23 +07:00
Chi Vinh Le 3a7b17750e Use unique name for fragment 2017-04-25 23:00:11 +07:00
Chi Vinh Le 4bacc2b0c2 Update Apollo 2017-04-25 22:31:59 +07:00
Chi Vinh Le 3d10534da1 RespectPlugin use fat query 2017-04-25 19:54:54 +07:00
Chi Vinh Le 54ea71bedf Support fragments in plugins 2017-04-25 18:24:25 +07:00
riley 19a83a902a Merge branch 'master' into all-comments-queue 2017-04-24 15:15:10 -06:00
Belén Curcio 7d56404e84 Merge branch 'master' into subscriptions 2017-04-24 18:09:34 -03:00
Riley Davis b71b7cc883 move the shortcuts note logic into redux state 2017-04-24 14:04:57 -06:00
Riley Davis aaa5d17ca3 Merge branch 'master' into shortcut-hint 2017-04-24 12:34:44 -06:00
Kim Gardner 860f434335 Merge branch 'master' into all-comments-queue 2017-04-24 13:45:02 -04:00
Chi Vinh Le 027358913d Use container 2017-04-24 23:35:49 +07:00
Chi Vinh Le a91db35f91 Add comments for filter 2017-04-24 23:34:53 +07:00
Chi Vinh Le c261cdc0c6 Compose comment fragments 2017-04-24 22:49:52 +07:00
Chi Vinh Le 11fb5421b5 Implement Stream Container 2017-04-24 19:27:54 +07:00
Chi Vinh Le 4dc3b0c2f4 Merge branch 'master' into query-composition-2
Conflicts:
	client/coral-embed-stream/src/Embed.js
	client/coral-embed-stream/src/components/Comment.js
	client/coral-embed-stream/src/index.js
	client/coral-framework/services/store.js
	client/coral-settings/containers/ProfileContainer.js
	graph/resolvers/root_query.js
2017-04-24 15:24:34 +07:00
Riley Davis 2743f6faf6 return all the known types 2017-04-21 16:44:42 -06:00
Gabriela Rodríguez Berón a41108a53c Merge branch 'master' into shortcut-hint 2017-04-21 15:10:42 -07:00
Gabriela Rodríguez Berón e337a0d18b Merge branch 'master' into subscriptions 2017-04-21 14:35:21 -07:00
Gabriela Rodríguez Berón e9c81f0639 Merge branch 'master' into all-comments-queue 2017-04-21 14:31:16 -07:00
Wyatt Johnson 441ea12a65 Added user metadata 2017-04-21 15:22:52 -06:00
Riley Davis d1ab279f74 add a little keyboard shortcut widget 2017-04-21 13:26:32 -06:00
Riley Davis 387e52b510 Merge branch 'master' into more-fragment-matcher 2017-04-21 12:56:47 -06:00
Riley Davis 2a2a069cea more matching on the fragment 2017-04-21 12:50:29 -06:00
Riley Davis f20f0c5d10 Merge branch 'master' into all-comments-queue 2017-04-21 09:05:38 -06:00
Wyatt Johnson b559b81198 Removed some demo code 2017-04-20 15:20:07 -06:00
Wyatt Johnson 1f562c8fad Merge branch 'master' into subscriptions 2017-04-20 15:17:44 -06:00
Chi Vinh Le 3cd9e802dd WIP 2017-04-20 00:37:45 +07:00
Chi Vinh Le 34fee9439f Move active tab to redux 2017-04-19 23:26:26 +07:00
Chi Vinh Le 0e6b0c01fd Fix issue with ignoredUsers 2017-04-19 21:06:57 +07:00
Chi Vinh Le 6fb2a95bf2 Move query logic to coral-embed-stream 2017-04-19 20:28:14 +07:00
Chi Vinh Le d5d424bfc5 Merge branch 'master' into embed-refactor 2017-04-19 16:16:14 +07:00
Wyatt Johnson ecbb66e270 Fix linting 2017-04-18 15:53:15 -06:00
Wyatt Johnson 45094fd176 Optimized sub model 2017-04-18 15:48:33 -06:00
Wyatt Johnson e1c8e75405 Merge branch 'master' into subscriptions 2017-04-18 15:43:25 -06:00
Chi Vinh Le e9331e90d3 Add missing check 2017-04-19 01:07:49 +07:00
Chi Vinh Le d3b81dff35 Merge branch 'master' into embed-refactor
Conflicts:
	client/coral-plugin-commentbox/CommentBox.js
2017-04-19 00:18:50 +07:00
Chi Vinh Le af5be0469f Refactor embed 2017-04-18 23:20:39 +07:00
Chi Vinh Le b40eed4751 Move to components folder 2017-04-18 23:14:28 +07:00
Riley Davis 673bbb61b1 Merge branch 'master' into all-comments-queue 2017-04-17 15:41:30 -06:00
riley 449a6f67f1 add some tab icons 2017-04-17 15:37:00 -06:00
Riley Davis df859dc782 add all tab 2017-04-17 14:12:45 -06:00
Wyatt Johnson 7fac8ef073 Merge branch 'master' into subscriptions 2017-04-17 08:03:14 -06:00
Wyatt Johnson 9f8c9c7eb1 Merge branch 'master' into subscriptions 2017-04-12 10:37:15 -06:00
David Erwin ab74ebdc52 Merge branch 'master' into subscriptions 2017-04-11 11:58:55 -04:00
Wyatt Johnson 97e3d7220e Removed test code 2017-04-10 14:39:50 -06:00
Wyatt Johnson 7acaaadcd6 Merge branch 'master' into subscriptions 2017-04-10 14:33:08 -06:00
Wyatt Johnson c12660be3b Merge branch 'master' into subscriptions 2017-04-06 15:39:24 -06:00
Wyatt Johnson ffb0ee20e8 Cleanup of subscriptions 2017-04-05 16:15:22 -06:00
Wyatt Johnson e6dba11984 Merge branch 'master' into subscriptions 2017-04-05 15:31:08 -06:00
David Erwin b335cb3493 Document subscription plugins 2017-04-05 17:26:12 -04:00
David Erwin 6735296c3c Abstract setupFunctions 2017-04-05 17:07:14 -04:00
David Erwin 350d9e74f2 Get plugin setupFuncitons for subscriptions 2017-04-05 16:56:32 -04:00
Wyatt Johnson b3582025b7 Fixed import 2017-04-05 13:37:32 -06:00
Wyatt Johnson 3c91b9cda1 Merge branch 'master' into subscriptions 2017-04-05 13:36:57 -06:00
Wyatt Johnson 1fecd4589b Removed unused file 2017-04-05 09:55:15 -06:00
David Erwin 48b7a67665 Add comments 2017-04-05 11:24:52 -04:00
Wyatt Johnson 45c7cbad9e Merge branch 'master' into subscriptions 2017-04-05 08:08:56 -06:00
Wyatt Johnson 75c88df8f1 Fix for build 2017-04-04 10:12:25 -06:00
Wyatt Johnson 07d1f9d3cf Merge branch 'master' into subscriptions 2017-04-04 09:58:50 -06:00
Wyatt Johnson dd3d1e9fe7 fix for session service 2017-03-29 12:45:57 -06:00
Wyatt Johnson ffbe976760 working websockets 2017-03-29 09:35:56 -06:00
Wyatt Johnson 0e0c3f19aa Fixed tests 2017-03-23 11:11:57 -06:00
Wyatt Johnson 63ef49d595 Initial subscription impl 2017-03-23 11:09:14 -06:00
129 changed files with 3826 additions and 2516 deletions
+1
View File
@@ -19,5 +19,6 @@ plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
**/node_modules/*
+2 -1
View File
@@ -13,7 +13,8 @@ EXPOSE 5000
COPY . /usr/src/app
# Install app dependencies and build static assets.
RUN yarn install --frozen-lockfile && \
RUN yarn global add node-gyp && \
yarn install --frozen-lockfile && \
cli plugins reconcile && \
yarn build && \
yarn install --production && \
+1 -1
View File
@@ -243,7 +243,7 @@ file under the `scripts` key including:
# Setup
Once you've installed Talk (either via Docker or source), you still need to
setup the application. If you are unfamiliar with any terminoligy used in the
setup the application. If you are unfamiliar with any terminology used in the
setup process, refer to the `TERMINOLOGY.md` document.
## Via Web
+32
View File
@@ -145,6 +145,10 @@ type RootMutation {
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
```
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
@@ -259,6 +263,23 @@ If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
#### Field: `setupFunctions`
```js
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
},
}),
}
```
Setup functions allow you to create filters that control which pubsub.publish() events
send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
#### Field: `router`
```js
@@ -375,6 +396,10 @@ module.exports = {
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
`,
context: {
Slack: () => ({
@@ -430,6 +455,13 @@ module.exports = {
}
}
}
},
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
}
}
}
};
+4 -33
View File
@@ -5,13 +5,11 @@ const path = require('path');
const helmet = require('helmet');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const session = require('express-session');
const enabled = require('debug').enabled;
const RedisStore = require('connect-redis')(session);
const redis = require('./services/redis');
const csrf = require('csurf');
const errors = require('./errors');
const graph = require('./graph');
const session = require('./services/session');
const {createGraphOptions} = require('./graph');
const apollo = require('graphql-server-express');
const app = express();
@@ -43,34 +41,7 @@ app.set('view engine', 'ejs');
// SESSION MIDDLEWARE
//==============================================================================
const session_opts = {
secret: process.env.TALK_SESSION_SECRET,
httpOnly: true,
rolling: true,
saveUninitialized: true,
resave: true,
unset: 'destroy',
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 8.64e+7, // 24 hours for session token expiry
},
store: new RedisStore({
client: redis.createClient(),
})
};
if (app.get('env') === 'production') {
// Enable the secure cookie when we are in production mode.
session_opts.cookie.secure = true;
} else if (app.get('env') === 'test') {
// Add in the secret during tests.
session_opts.secret = 'keyboard cat';
}
app.use(session(session_opts));
app.use(session);
//==============================================================================
// PASSPORT MIDDLEWARE
@@ -96,7 +67,7 @@ app.use(passport.session());
//==============================================================================
// GraphQL endpoint.
app.use('/api/v1/graph/ql', apollo.graphqlExpress(graph.createGraphOptions));
app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
// Only include the graphiql tool if we aren't in production mode.
if (app.get('env') !== 'production') {
+20 -9
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env node
const app = require('../app');
const program = require('./commander');
const http = require('http');
const app = require('../app');
const {createServer} = require('http');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('./util');
const {createSubscriptionManager} = require('../graph/subscriptions');
/**
* Get port from environment and store in Express.
@@ -20,7 +21,7 @@ app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
const server = createServer(app);
/**
* Event listener for HTTP server "error" event.
@@ -76,20 +77,29 @@ function normalizePort(val) {
function onListening() {
let addr = server.address();
let bind = typeof addr === 'string'
? `pipe ${ addr}`
: `port ${ addr.port}`;
console.log(`Listening on ${ bind}`);
? `pipe ${addr}`
: `port ${addr.port}`;
console.log(`API Server Listening on ${bind}`);
}
/**
* Start the app.
*/
function startApp() {
function startApp(program) {
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.listen(port, () => {
// Mount the websocket server if requested.
if (program.websockets) {
console.log(`Websocket Server Listening on ${port}`);
// Mount the subscriptions server on the application server.
createSubscriptionManager(server);
}
});
server.on('error', onError);
server.on('listening', onListening);
}
@@ -100,10 +110,11 @@ function startApp() {
program
.option('-j, --jobs', 'enable job processing on this thread')
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
.parse(process.argv);
// Start the application serving.
startApp();
startApp(program);
// Enable job processing on the thread if enabled.
if (program.jobs) {
+6
View File
@@ -39,6 +39,12 @@ const routes = (
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<Route path='all' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
<Route path='accepted' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
<Route path='premod' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
@@ -6,3 +6,15 @@ export const singleView = () => ({type: actions.SINGLE_VIEW});
// Ban User Dialog
export const showBanUserDialog = (user, commentId, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, showRejectedNote});
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
// hide shortcuts note
export const hideShortcutsNote = () => {
try {
window.localStorage.setItem('coral:shortcutsNote', 'hide');
} catch (e) {
// above will fail in Safari private mode
}
return {type: actions.HIDE_SHORTCUTS_NOTE};
};
@@ -1,17 +1,30 @@
import React from 'react';
import React, {PropTypes} from 'react';
import styles from './ModerationList.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
const ActionButton = ({type = '', ...props}) => {
const ActionButton = ({type = '', active, ...props}) => {
const typeName = type.toLowerCase();
let text = menuActionsMap[type].text;
if (text === 'Approve' && active) {
text = 'Approved';
} else if (text === 'Reject' && active) {
text = 'Rejected';
}
return (
<Button
className={`${type.toLowerCase()} ${styles.actionButton}`}
cStyle={type.toLowerCase()}
className={`${typeName} ${styles.actionButton} ${active ? styles[`${typeName}__active`] : ''}`}
cStyle={typeName}
icon={menuActionsMap[type].icon}
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
>{menuActionsMap[type].text}</Button>
>{text}</Button>
);
};
ActionButton.propTypes = {
active: PropTypes.bool
};
export default ActionButton;
@@ -18,3 +18,47 @@
border-radius: 4px;
border: 1px solid #999;
}
.callToAction {
position: fixed;
left: 10px;
bottom: 10px;
width: 280px;
height: 200px;
background: white;
padding: 15px;
box-sizing: border-box;
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
.ctaHeader {
font-size: 16px;
margin-bottom: 5px;
}
ul {
padding-left: 0;
list-style: none;
margin: 0 0 10px 0;
}
p, li {
font-size: 12px;
margin-bottom: 5px;
}
li span:first-child, p:last-child span:first-child {
display: inline-block;
width: 120px;
}
}
.closeButton {
float: right;
font-size: 20px;
cursor: pointer;
}
.smallKey {
padding: 5px;
border: 1px solid #ccc;
}
@@ -1,9 +1,11 @@
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import React from 'react';
import React, {PropTypes} from 'react';
import Modal from 'components/Modal';
import styles from './ModerationKeysModal.css';
const lang = new I18n(translations);
const shortcuts = [
{
title: 'modqueue.navigation',
@@ -23,29 +25,52 @@ const shortcuts = [
}
];
export default ({open, onClose}) => (
<Modal open={open} onClose={onClose}>
<h3>{lang.t('modqueue.shortcuts')}</h3>
<div className={styles.container}>
{shortcuts.map((shortcut, i) => (
<table className={styles.table} key={i}>
<thead>
<tr>
<th>{lang.t(shortcut.title)}</th>
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key }tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td>{lang.t(shortcut.shortcuts[key])}</td>
</tr>
))}
</tbody>
</table>
))}
</div>
</Modal>
);
export default class ModerationKeysModal extends React.Component {
const lang = new I18n(translations);
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired
}
render () {
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
return (
<div>
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
<p className={styles.ctaHeader}>{lang.t('modqueue.mod-faster')}</p>
<p><strong>{lang.t('modqueue.try-these')}:</strong></p>
<ul>
<li><span>{lang.t('modqueue.approve')}</span> <span className={styles.smallKey}>t</span></li>
<li><span>{lang.t('modqueue.reject')}</span> <span className={styles.smallKey}>r</span></li>
</ul>
<p><span>{lang.t('modqueue.view-more-shortcuts')}</span> <span className={styles.smallKey}>{lang.t('modqueue.shift-key')}</span> + <span className={styles.smallKey}>/</span></p>
</div>
<Modal open={open} onClose={onClose}>
<h3>{lang.t('modqueue.shortcuts')}</h3>
<div className={styles.container}>
{shortcuts.map((shortcut, i) => (
<table className={styles.table} key={i}>
<thead>
<tr>
<th>{lang.t(shortcut.title)}</th>
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key }tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td>{lang.t(shortcut.shortcuts[key])}</td>
</tr>
))}
</tbody>
</table>
))}
</div>
</Modal>
</div>
);
}
}
@@ -188,3 +188,15 @@
margin: 0;
width: 140px;
}
.approve__active {
box-shadow: none;
color: white;
background-color: #519954;
}
.reject__active, .rejected__active {
color: white;
background-color: #D03235;
box-shadow: none;
}
@@ -6,7 +6,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import {Logo} from './Logo';
const CoralHeader = ({handleLogout, restricted = false}) => (
const CoralHeader = ({handleLogout, showShortcuts = () => {}, restricted = false}) => (
<Header className={styles.header}>
<Logo className={styles.logo} />
{
@@ -55,7 +55,8 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
<MenuItem onClick={() => showShortcuts(true)}>{lang.t('configure.shortcuts')}</MenuItem>
<MenuItem onClick={handleLogout}>{lang.t('configure.sign-out')}</MenuItem>
</Menu>
</div>
</li>
@@ -72,6 +73,7 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
);
CoralHeader.propTypes = {
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool // hide elemnts from a user that's logged out
};
@@ -4,9 +4,13 @@ import Header from './Header';
import Drawer from './Drawer';
import styles from './Layout.css';
const Layout = ({children, handleLogout = () => {}, restricted = false, ...props}) => (
const Layout = ({children, handleLogout = () => {}, toggleShortcutModal, restricted = false, ...props}) => (
<LayoutMDL fixedDrawer>
<Header handleLogout={handleLogout} restricted={restricted} {...props} />
<Header
handleLogout={handleLogout}
showShortcuts={toggleShortcutModal}
restricted={restricted}
{...props} />
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
<div className={styles.layout}>
{children}
@@ -16,6 +20,7 @@ const Layout = ({children, handleLogout = () => {}, restricted = false, ...props
Layout.propTypes = {
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
};
@@ -2,3 +2,4 @@ export const TOGGLE_MODAL = 'TOGGLE_MODAL';
export const SINGLE_VIEW = 'SINGLE_VIEW';
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
@@ -4,7 +4,6 @@ import translations from 'coral-admin/src/translations.json';
import styles from './Community.css';
import Table from './Table';
import Loading from './Loading';
import {Pager, Icon} from 'coral-ui';
import EmptyCard from '../../components/EmptyCard';
@@ -29,8 +28,8 @@ const tableHeaders = [
}
];
const People = ({isFetching, commenters, searchValue, onSearchChange, ...props}) => {
const hasResults = !isFetching && !!commenters.length;
const People = ({commenters, searchValue, onSearchChange, ...props}) => {
const hasResults = !!commenters.length;
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
@@ -47,7 +46,6 @@ const People = ({isFetching, commenters, searchValue, onSearchChange, ...props})
</div>
</div>
<div className={styles.mainContent}>
{ isFetching && <Loading /> }
{
hasResults
? <Table
@@ -21,7 +21,7 @@ const FlagWidget = ({assets}) => {
? assets.map(asset => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
flagSummary = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary');
}
return (
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
import {connect} from 'react-redux';
import Layout from '../components/ui/Layout';
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {fetchConfig} from '../actions/config';
import {FullLoading} from '../components/FullLoading';
import AdminLogin from '../components/AdminLogin';
@@ -23,7 +24,7 @@ class LayoutContainer extends Component {
passwordRequestSuccess
} = this.props.auth;
const {handleLogout, TALK_RECAPTCHA_PUBLIC} = this.props;
const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props;
if (loadingUser) { return <FullLoading />; }
if (!isAdmin) {
return <AdminLogin
@@ -34,7 +35,9 @@ class LayoutContainer extends Component {
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError} />;
}
if (isAdmin && loggedIn) { return <Layout handleLogout={handleLogout} {...this.props} />; }
if (isAdmin && loggedIn) {
return <Layout handleLogout={handleLogout} toggleShortcutModal={toggleShortcutModal} {...this.props} />;
}
return <FullLoading />;
}
}
@@ -49,6 +52,7 @@ const mapDispatchToProps = dispatch => ({
fetchConfig: () => dispatch(fetchConfig()),
handleLogin: (username, password, recaptchaResponse) => dispatch(handleLogin(username, password, recaptchaResponse)),
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
toggleShortcutModal: toggle => dispatch(toggleShortcutModal(toggle)),
handleLogout: () => dispatch(logout())
});
@@ -5,12 +5,12 @@ import key from 'keymaster';
import isEqual from 'lodash/isEqual';
import styles from './components/styles.css';
import {modQueueQuery} from '../../graphql/queries';
import {modQueueQuery, getQueueCounts} from '../../graphql/queries';
import {banUser, setCommentStatus} from '../../graphql/mutations';
import {fetchSettings} from 'actions/settings';
import {updateAssets} from 'actions/assets';
import {toggleModal, singleView, showBanUserDialog, hideBanUserDialog} from 'actions/moderation';
import {toggleModal, singleView, showBanUserDialog, hideBanUserDialog, hideShortcutsNote} from 'actions/moderation';
import {Spinner} from 'coral-ui';
import BanUserDialog from '../../components/BanUserDialog';
@@ -135,6 +135,12 @@ class ModerationContainer extends Component {
const comments = data[activeTab];
let activeTabCount;
switch(activeTab) {
case 'all':
activeTabCount = data.allCount;
break;
case 'accepted':
activeTabCount = data.acceptedCount;
break;
case 'premod':
activeTabCount = data.premodCount;
break;
@@ -151,6 +157,8 @@ class ModerationContainer extends Component {
<ModerationHeader asset={asset} />
<ModerationMenu
asset={asset}
allCount={data.allCount}
acceptedCount={data.acceptedCount}
premodCount={data.premodCount}
rejectedCount={data.rejectedCount}
flaggedCount={data.flaggedCount}
@@ -183,6 +191,8 @@ class ModerationContainer extends Component {
rejectComment={props.rejectComment}
/>
<ModerationKeysModal
hideShortcutsNote={props.hideShortcutsNote}
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
open={moderation.modalOpen}
onClose={onClose}/>
</div>
@@ -204,11 +214,13 @@ const mapDispatchToProps = dispatch => ({
fetchSettings: () => dispatch(fetchSettings()),
showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
hideShortcutsNote: () => dispatch(hideShortcutsNote()),
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
setCommentStatus,
getQueueCounts,
modQueueQuery,
banUser
)(ModerationContainer);
@@ -9,52 +9,67 @@ import translations from 'coral-admin/src/translations';
import LoadMore from './components/LoadMore';
const lang = new I18n(translations);
const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loadMore, activeTab, sort, ...props}) => {
return (
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
<ul style={{paddingLeft: 0}}>
{
comments.length
? comments.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
key={i}
index={i}
comment={comment}
commentType={activeTab}
selected={i === selectedIndex}
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
/>;
})
: <EmptyCard>{lang.t('modqueue.emptyqueue')}</EmptyCard>
}
</ul>
<LoadMore
comments={comments}
loadMore={loadMore}
sort={sort}
tab={activeTab}
showLoadMore={comments.length < commentCount}
assetId={props.assetId}
/>
</div>
);
};
class ModerationQueue extends React.Component {
ModerationQueue.propTypes = {
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
comments: PropTypes.array.isRequired
};
static propTypes = {
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
comments: PropTypes.array.isRequired
}
componentDidUpdate (prev) {
const {loadMore, comments, commentCount, sort, activeTab: tab, assetId: asset_id} = this.props;
// if the user just moderated the last (visible) comment
// AND there are more comments available on the server,
// go ahead and load more comments
if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) {
loadMore({sort, tab, asset_id});
}
}
render () {
const {comments, selectedIndex, commentCount, singleView, loadMore, activeTab, sort, ...props} = this.props;
return (
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
<ul style={{paddingLeft: 0}}>
{
comments.length
? comments.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
key={i}
index={i}
comment={comment}
selected={i === selectedIndex}
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
/>;
})
: <EmptyCard>{lang.t('modqueue.emptyqueue')}</EmptyCard>
}
</ul>
<LoadMore
comments={comments}
loadMore={loadMore}
sort={sort}
tab={activeTab}
showLoadMore={comments.length < commentCount}
assetId={props.assetId}
/>
</div>
);
}
}
export default ModerationQueue;
@@ -23,9 +23,15 @@ const Comment = ({actions = [], comment, ...props}) => {
const linkText = links ? links.map(link => link.raw) : [];
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction');
let commentType = '';
if (comment.status === 'PREMOD') {
commentType = 'premod';
} else if (flagActions && flagActions.length) {
commentType = 'flagged';
}
return (
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
@@ -36,7 +42,7 @@ const Comment = ({actions = [], comment, ...props}) => {
{timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
<CommentType type={props.commentType} />
<CommentType type={commentType} />
</div>
{comment.user.status === 'banned' ?
<span className={styles.banned}>
@@ -60,14 +66,17 @@ const Comment = ({actions = [], comment, ...props}) => {
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={comment.user}
acceptComment={() => props.acceptComment({commentId: comment.id})}
rejectComment={() => props.rejectComment({commentId: comment.id})}
/>
)}
{actions.map((action, i) => {
const active = (action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return <ActionButton key={i}
type={action}
user={comment.user}
status={comment.status}
active={active}
acceptComment={() => props.acceptComment({commentId: comment.id})}
rejectComment={() => props.rejectComment({commentId: comment.id})} />;
})}
</div>
</div>
</div>
@@ -1,9 +1,25 @@
import React, {PropTypes} from 'react';
import styles from './CommentCount.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
const CommentCount = props => (
<span className={styles.count}>{props.count}</span>
);
const CommentCount = ({count}) => {
let number = count;
// shorten large counts to abbreviations
if (number / 1e9 > 1) {
number = `${(number / 1e9).toFixed(1)}${lang.t('modqueue.billion')}`;
} else if (number / 1e6 > 1) {
number = `${(number / 1e6).toFixed(1)}${lang.t('modqueue.million')}`;
} else if (number / 1e3 > 1) {
number = `${(number / 1e3).toFixed(1)}${lang.t('modqueue.thousand')}`;
}
return (
<span className={styles.count}>{number}</span>
);
};
CommentCount.propTypes = {
count: PropTypes.number.isRequired
@@ -7,13 +7,16 @@ const LoadMore = ({comments, loadMore, sort, tab, assetId, showLoadMore}) =>
{
showLoadMore && <Button
className={styles.loadMore}
onClick={() =>
loadMore({
cursor: comments[comments.length - 1].created_at,
onClick={() => {
const lastComment = comments[comments.length - 1];
const cursor = lastComment ? lastComment.created_at : null;
return loadMore({
cursor,
sort,
tab,
asset_id: assetId
})}>
});
}}>
Load More
</Button>
}
@@ -23,7 +26,7 @@ LoadMore.propTypes = {
comments: PropTypes.array.isRequired,
loadMore: PropTypes.func.isRequired,
sort: PropTypes.oneOf(['CHRONOLOGICAL', 'REVERSE_CHRONOLOGICAL']).isRequired,
tab: PropTypes.oneOf(['rejected', 'premod', 'flagged']).isRequired,
tab: PropTypes.oneOf(['rejected', 'premod', 'flagged', 'all', 'accepted']).isRequired,
assetId: PropTypes.string,
showLoadMore: PropTypes.bool.isRequired
};
@@ -4,22 +4,18 @@ import styles from './styles.css';
import {SelectField, Option} from 'react-mdl-selectfield';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
import {Icon} from 'coral-ui';
import {Link} from 'react-router';
const lang = new I18n(translations);
const ModerationMenu = (
{asset, premodCount, rejectedCount, flaggedCount, selectSort, sort}
{asset, allCount, acceptedCount, premodCount, rejectedCount, flaggedCount, selectSort, sort}
) => {
const premodPath = asset
? `/admin/moderate/premod/${asset.id}`
: '/admin/moderate/premod';
const rejectPath = asset
? `/admin/moderate/rejected/${asset.id}`
: '/admin/moderate/rejected';
const flagPath = asset
? `/admin/moderate/flagged/${asset.id}`
: '/admin/moderate/flagged';
function getPath (type) {
return asset ? `/admin/moderate/${type}/${asset.id}` : `/admin/moderate/${type}`;
}
return (
<div className="mdl-tabs">
@@ -27,22 +23,34 @@ const ModerationMenu = (
<div className={styles.tabBarPadding} />
<div>
<Link
to={premodPath}
to={getPath('all')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.premod')} <CommentCount count={premodCount} />
<Icon name='question_answer' className={styles.tabIcon} /> {lang.t('modqueue.all')} <CommentCount count={allCount} />
</Link>
<Link
to={flagPath}
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
<Icon name='check' className={styles.tabIcon} /> {lang.t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={rejectPath}
to={getPath('premod')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
<Icon name='access_time' className={styles.tabIcon} /> {lang.t('modqueue.premod')} <CommentCount count={premodCount} />
</Link>
<Link
to={getPath('flagged')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='flag' className={styles.tabIcon} /> {lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
<Link
to={getPath('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='close' className={styles.tabIcon} /> {lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
</Link>
</div>
<SelectField
@@ -59,6 +67,7 @@ const ModerationMenu = (
};
ModerationMenu.propTypes = {
allCount: PropTypes.number.isRequired,
premodCount: PropTypes.number.isRequired,
rejectedCount: PropTypes.number.isRequired,
flaggedCount: PropTypes.number.isRequired,
@@ -418,3 +418,8 @@ span {
.loadMore:hover {
background-color: #4399FF;
}
.tabIcon {
position: relative;
top: 7px;
}
@@ -6,7 +6,6 @@ fragment metrics on Asset {
created_at
commentCount
action_summaries {
type: __typename
actionCount
actionableItemCount
}
@@ -44,6 +44,7 @@ export const suspendUser = graphql(SUSPEND_USER, {
})
});
const views = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
props: ({mutate}) => ({
acceptComment: ({commentId}) => {
@@ -54,6 +55,21 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
},
updateQueries: {
ModQueue: (oldData) => {
const comment = views.reduce((comment, view) => {
return comment ? comment : oldData[view].find(c => c.id === commentId);
}, null);
let accepted;
let acceptedCount = oldData.acceptedCount;
// if the comment was already in the Approved queue, don't re-add it
if (comment.status === 'ACCEPTED') {
accepted = [...oldData.accepted];
} else {
comment.status = 'ACCEPTED';
acceptedCount++;
accepted = [comment, ...oldData.accepted];
}
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const rejected = oldData.rejected.filter(c => c.id !== commentId);
@@ -63,11 +79,13 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
return {
...oldData,
premodCount,
flaggedCount,
rejectedCount,
premodCount: Math.max(0, premodCount),
flaggedCount: Math.max(0, flaggedCount),
acceptedCount: Math.max(0, acceptedCount),
rejectedCount: Math.max(0, rejectedCount),
premod,
flagged,
accepted,
rejected,
};
}
@@ -82,21 +100,37 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
},
updateQueries: {
ModQueue: (oldData) => {
const comment = oldData.premod.concat(oldData.flagged).filter(c => c.id === commentId)[0];
const rejected = [comment].concat(oldData.rejected);
const comment = views.reduce((comment, view) => {
return comment ? comment : oldData[view].find(c => c.id === commentId);
}, null);
let rejected;
let rejectedCount = oldData.rejectedCount;
// if the item was already in the Rejected queue, don't put it in again
if (comment.status === 'REJECTED') {
rejected = oldData.rejected;
} else {
comment.status = 'REJECTED';
rejectedCount++;
rejected = [comment, ...oldData.rejected];
}
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const accepted = oldData.accepted.filter(c => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = oldData.rejectedCount + 1;
const acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
return {
...oldData,
premodCount,
flaggedCount,
rejectedCount,
premodCount: Math.max(0, premodCount),
flaggedCount: Math.max(0, flaggedCount),
acceptedCount: Math.max(0, acceptedCount),
rejectedCount: Math.max(0, rejectedCount),
premod,
flagged,
accepted,
rejected
};
}
@@ -0,0 +1,22 @@
query Counts ($asset_id: ID) {
allCount: commentCount(query: {
asset_id: $asset_id
})
acceptedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
premodCount: commentCount(query: {
statuses: [PREMOD],
asset_id: $asset_id
})
rejectedCount: commentCount(query: {
statuses: [REJECTED],
asset_id: $asset_id
})
flaggedCount: commentCount(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD]
})
}
+30 -12
View File
@@ -4,6 +4,7 @@ import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
import METRICS from './metricsQuery.graphql';
import GET_QUEUE_COUNTS from './getQueueCounts.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: ({params: {id = null}}) => {
@@ -33,28 +34,34 @@ export const getMetrics = graphql(METRICS, {
}
});
export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => {
let statuses;
export const loadMore = (fetchMore) => ({limit = 10, cursor, sort, tab, asset_id}) => {
let variables = {
limit,
cursor,
sort,
asset_id
};
switch(tab) {
case 'all':
variables.statuses = null;
break;
case 'accepted':
variables.statuses = ['ACCEPTED'];
break;
case 'premod':
statuses = ['PREMOD'];
variables.statuses = ['PREMOD'];
break;
case 'flagged':
statuses = ['NONE', 'PREMOD'];
variables.statuses = ['NONE', 'PREMOD'];
variables.action_type = 'FLAG';
break;
case 'rejected':
statuses = ['REJECTED'];
variables.statuses = ['REJECTED'];
break;
}
return fetchMore({
query: MOD_QUEUE_LOAD_MORE,
variables: {
limit,
cursor,
sort,
statuses,
asset_id
},
variables,
updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
return {
...oldData,
@@ -87,3 +94,14 @@ export const modQueueResort = (id, fetchMore) => (sort) => {
updateQuery: (oldData, {fetchMoreResult:{data}}) => data
});
};
export const getQueueCounts = graphql(GET_QUEUE_COUNTS, {
options: ({params: {id = null}}) => {
return {
pollInterval: 5000,
variables: {
asset_id: id
}
};
}
});
@@ -1,7 +1,7 @@
#import "../fragments/commentView.graphql"
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort}) {
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
...commentView
action_summaries {
count
@@ -1,6 +1,20 @@
#import "../fragments/commentView.graphql"
query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
all: comments(query: {
statuses: [NONE, PREMOD, ACCEPTED, REJECTED],
asset_id: $asset_id,
sort: $sort
}) {
...commentView
}
accepted: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id,
sort: $sort
}) {
...commentView
}
premod: comments(query: {
statuses: [PREMOD],
asset_id: $asset_id,
@@ -28,6 +42,13 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
title
url
}
allCount: commentCount(query: {
asset_id: $asset_id
})
acceptedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
premodCount: commentCount(query: {
statuses: [PREMOD],
asset_id: $asset_id
@@ -6,7 +6,8 @@ const initialState = Map({
modalOpen: false,
user: Map({}),
commentId: null,
banDialog: false
banDialog: false,
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show'
});
export default function moderation (state = initialState, action) {
@@ -31,6 +32,9 @@ export default function moderation (state = initialState, action) {
case actions.SINGLE_VIEW:
return state
.set('singleView', !state.get('singleView'));
case actions.HIDE_SHORTCUTS_NOTE:
return state
.set('shortcutsNoteVisible', 'hide');
default :
return state;
}
@@ -7,12 +7,37 @@ const fm = new IntrospectionFragmentMatcher({
introspectionQueryResultData: {
__schema: {
types: [
{
kind: 'INTERFACE',
name: 'UserError',
possibleTypes: [
{name: 'GenericUserError'},
{name: 'ValidationUserError'}
]
},
{
kind: 'INTERFACE',
name: 'Response',
possibleTypes: [
{name: 'CreateCommentResponse'},
{name: 'CreateFlagResponse'},
{name: 'CreateDontAgreeResponse'},
{name: 'DeleteActionResponse'},
{name: 'SetUserStatusResponse'},
{name: 'SuspendUserResponse'},
{name: 'SetCommentStatusResponse'},
{name: 'AddCommentTagResponse'},
{name: 'RemoveCommentTagResponse'},
{name: 'IgnoreUserResponse'},
{name: 'StopIgnoringUserResponse'}
]
},
{
kind: 'INTERFACE',
name: 'Action',
possibleTypes: [
{name: 'DefaultAction'},
{name: 'FlagAction'},
{name: 'LikeAction'},
{name: 'DontAgreeAction'}
],
},
@@ -20,10 +45,18 @@ const fm = new IntrospectionFragmentMatcher({
kind: 'INTERFACE',
name: 'ActionSummary',
possibleTypes: [
{name: 'DefaultActionSummary'},
{name: 'FlagActionSummary'},
{name: 'LikeActionSummary'},
{name: 'DontAgreeActionSummary'}
],
},
{
kind: 'INTERFACE',
name: 'AssetActionSummary',
possibleTypes: [
{name: 'DefaultAssetActionSummary'},
{name: 'FlagAssetActionSummary'},
]
}
],
},
+23 -2
View File
@@ -36,6 +36,7 @@
"modqueue": {
"likes": "likes",
"all": "all",
"approved": "approved",
"premod": "pre-mod",
"rejected": "rejected",
"flagged": "flagged",
@@ -44,6 +45,10 @@
"close": "Close",
"actions": "Actions",
"navigation": "Navigation",
"mod-faster": "Moderate faster with keyboard shortcuts",
"try-these": "Try these",
"view-more-shortcuts": "View more shortcuts",
"shift-key": "⇧",
"approve": "Approve comment",
"reject": "Reject comment",
"nextcomment": "Go to the next comment",
@@ -58,7 +63,10 @@
"impersonating": "Impersonating",
"offensive": "Offensive",
"spam/ads": "Spam/Ads",
"other": "Other"
"other": "Other",
"thousand": "k",
"million": "M",
"billion": "B"
},
"comment": {
"flagged": "flagged",
@@ -75,6 +83,8 @@
"copy": "Copy to Clipboard"
},
"configure": {
"sign-out": "Sign Out",
"shortcuts": "Shortcuts",
"closed-stream-settings": "Closed Stream Message",
"open-stream-configuration": "This comment stream is currently open. By closing this comment stream, no new comments may be submitted and all previous comments will still be displayed.",
"close-stream-configuration": "This comment stream is currently closed. By opening this comment stream, new comments may be submitted and displayed",
@@ -221,11 +231,17 @@
"loading": "Cargando resultados"
},
"modqueue": {
"all": "todos",
"approved": "aprobado",
"likes": "gustos",
"premod": "pre-mod",
"rejected": "rechazado",
"flagged": "marcado",
"shortcuts": "Atajos de teclado",
"mod-faster": "Moderar más rápido con atajos del teclado",
"try-these": "Intenta estos",
"view-more-shortcuts": "Ver más atajos",
"shift-key": "⇧",
"close": "Cerrar",
"emptyqueue": "No se encontro ningún usuario. Están escondidos.",
"showshortcuts": "Mostrar atajos",
@@ -235,7 +251,10 @@
"impersonating": "Suplantación",
"offensive": "Ofensivo",
"spam/ads": "Spam/Propaganda",
"other": "Otros"
"other": "Otros",
"thousand": "m",
"million": "M",
"billion": "B"
},
"comment": {
"flagged": "marcado",
@@ -249,6 +268,8 @@
"username_flags": "marcas para este nombre de usuario"
},
"configure": {
"sign-out": "Desconectar",
"shortcuts": "Atajos",
"closed-stream-settings": "Mensaje a enviar cuando los comentarios están cerrados en el artículo",
"open-stream-configuration": "Este hilo de comentarios esta abierto. Al cerrarlo, ningún nuevo comentario será publicado y todos los comentarios anteriores serán mostrados.",
"close-stream-configuration": "Este hilo de comentario está en este momento cerrado. Al abrirlo, nuevos comentarios serán publicaods y mostrados.",
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import {Router, Route, browserHistory} from 'react-router';
import Embed from './Embed';
import Embed from './containers/Embed';
import SignInContainer from 'coral-sign-in/containers/SignInContainer';
const routes = (
-348
View File
@@ -1,348 +0,0 @@
import React from 'react';
import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import isEqual from 'lodash/isEqual';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui';
const {logout, showSignInDialog, requestConfirmEmail, openSignInPopUp, checkLogin} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset';
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import {ModerationLink} from 'coral-plugin-moderation';
import Count from 'coral-plugin-comment-count/CommentCount';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import UserBox from 'coral-sign-in/components/UserBox';
import SuspendedAccount from 'coral-framework/components/SuspendedAccount';
import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUsernameContainer';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
import HighlightedComment from './Comment';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
class Embed extends React.Component {
constructor(props) {
super(props);
this.state = {
activeTab: 0,
showSignInDialog: false,
activeReplyBox: ''
};
}
changeTab = (tab) => {
// Everytime the comes from another tab, the Stream needs to be updated.
if (tab === 0) {
this.props.viewAllComments();
this.props.data.refetch();
}
this.setState({
activeTab: tab
});
}
static propTypes = {
data: React.PropTypes.shape({
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
}
componentDidMount () {
pym.sendMessage('childReady');
this.props.checkLogin();
}
componentWillUnmount () {
clearInterval(this.state.countPoll);
}
componentWillReceiveProps (nextProps) {
const {loadAsset} = this.props;
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
loadAsset(nextProps.data.asset);
const {getCounts, updateCountCache, asset: {countCache}} = this.props;
const {asset} = nextProps.data;
if (!countCache) {
updateCountCache(asset.id, asset.commentCount);
}
this.setState({
countPoll: setInterval(() => {
const {asset} = this.props.data;
getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
});
}, NEW_COMMENT_COUNT_POLL_INTERVAL)
});
}
}
componentDidUpdate(prevProps) {
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
}
}
setActiveReplyBox = (reactKey) => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
} else {
this.setState({activeReplyBox: reactKey});
}
}
render () {
const {activeTab} = this.state;
const {closedAt, countCache = {}} = this.props.asset;
const {asset, refetch, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog} = this.props.auth;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
const highlightedComment = comment && comment.parent ? comment.parent : comment;
const openStream = closedAt === null;
const banned = user && user.status === 'BANNED';
const hasOlderComments = !!(
asset &&
asset.lastComment &&
asset.lastComment.id !== asset.comments[asset.comments.length - 1].id
);
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 200
} : {};
if (!asset) {
return <Spinner />;
}
// Find the created_at date of the first comment. If no comments exist, set the date to a week ago.
const firstCommentDate = asset.comments[0]
? asset.comments[0].created_at
: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
const userBox = <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>;
// TODO: This is a quickfix and will be replaced after our refactor.
const ignoredUsers = this.props.userData.ignoredUsers;
const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id);
return (
<div style={expandForLogin}>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.totalCommentCount}/></Tab>
<Tab>{lang.t('myProfile')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
highlightedComment &&
<Button
cStyle='darkGrey'
style={{float: 'right'}}
onClick={() => {
this.props.viewAllComments();
this.props.data.refetch();
}}>{lang.t('showAllComments')}</Button>
}
<TabContent show={activeTab === 0}>
{ loggedIn ? userBox : null }
{
openStream
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<QuestionBox
content={asset.settings.questionBoxContent}
enable={asset.settings.questionBoxEnable}
/>
<RestrictedContent restricted={banned} restrictedComp={
<SuspendedAccount
canEditName={user && user.canEditName}
editName={this.props.editName}
/>
}>
{
user
? <CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
updateCountCache={this.props.updateCountCache}
countCache={countCache[asset.id]}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
currentUser={this.props.auth.user}
authorId={user.id}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount} />
: null
}
</RestrictedContent>
</div>
: <p>{asset.settings.closedMessage}</p>
}
{!loggedIn && <Button id='coralSignInButton' onClick={this.props.showSignInDialog} full>Sign in to comment</Button>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} user={user} />}
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
{/* the highlightedComment is isolated after the user followed a permalink */}
{
highlightedComment
? <HighlightedComment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
addNotification={this.props.addNotification}
depth={0}
postItem={this.props.postItem}
asset={asset}
currentUser={user}
highlighted={comment.id}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
commentIsIgnored={commentIsIgnored}
key={highlightedComment.id}
reactKey={highlightedComment.id}
comment={highlightedComment} />
: <div>
<NewCount
commentCount={asset.commentCount}
countCache={countCache[asset.id]}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
updateCountCache={this.props.updateCountCache}
/>
<div className="embed__stream">
<Stream
open={openStream}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
ignoreUser={this.props.ignoreUser}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments}
maxCharCount={asset.settings.charCount}
charCountEnable={asset.settings.charCountEnable}
ignoredUsers={this.props.userData.ignoredUsers} />
</div>
<LoadMore
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={hasOlderComments}
loadMore={this.props.loadMore} />
</div>
}
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
loggedIn={loggedIn}
userData={this.props.userData}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
{ loggedIn ? userBox : null }
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
</div>
</div>
);
}
}
const mapStateToProps = state => ({
auth: state.auth.toJS(),
userData: state.user.toJS(),
asset: state.asset.toJS(),
});
const mapDispatchToProps = dispatch => ({
requestConfirmEmail: () => dispatch(requestConfirmEmail()),
loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)),
addNotification: (type, text) => addNotification(type, text),
clearNotification: () => dispatch(clearNotification()),
editName: (username) => dispatch(editName(username)),
showSignInDialog: () => dispatch(showSignInDialog()),
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
viewAllComments: () => dispatch(viewAllComments()),
logout: () => dispatch(logout()),
openSignInPopUp: cb => dispatch(openSignInPopUp(cb)),
checkLogin: () => dispatch(checkLogin()),
dispatch: d => dispatch(d),
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
postComment,
postFlag,
postLike,
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
queryStream,
)(Embed);
-103
View File
@@ -1,103 +0,0 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
class Stream extends React.Component {
static propTypes = {
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
asset: PropTypes.object.isRequired,
open: PropTypes.bool.isRequired,
comments: PropTypes.array.isRequired,
currentUser: PropTypes.shape({
username: PropTypes.string,
id: PropTypes.string
}),
charCountEnable: PropTypes.bool.isRequired,
maxCharCount: PropTypes.number,
// dispatch action to add a tag to a comment
addCommentTag: PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
// list of user ids that should be rendered as ignored
ignoredUsers: React.PropTypes.arrayOf(React.PropTypes.string)
}
constructor(props) {
super(props);
this.state = {activeReplyBox: '', countPoll: null};
}
render () {
const {
comments,
currentUser,
asset,
postItem,
addNotification,
postFlag,
postLike,
open,
postDontAgree,
loadMore,
deleteAction,
showSignInDialog,
addCommentTag,
removeCommentTag,
pluginProps,
ignoreUser,
ignoredUsers,
charCountEnable,
maxCharCount,
} = this.props;
const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id);
return (
<div id='stream'>
{
comments.map(comment =>
commentIsIgnored(comment)
? <IgnoredCommentTombstone
key={comment.id}
/>
: <Comment
disableReply={!open}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
maxCharCount={maxCharCount}
charCountEnable={charCountEnable}
pluginProps={pluginProps}
/>
)
}
</div>
);
}
}
export default Stream;
@@ -0,0 +1,10 @@
import * as actions from '../constants/embed';
import {viewAllComments} from './stream';
export const setActiveTab = (tab) => (dispatch, getState) => {
dispatch({type: actions.SET_ACTIVE_TAB, tab});
if (getState().stream.commentId) {
dispatch(viewAllComments());
}
};
@@ -0,0 +1,40 @@
import {pym} from 'coral-framework';
import * as actions from '../constants/stream';
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
export const setCommentCountCache = (amount) => ({type: actions.SET_COMMENT_COUNT_CACHE, amount});
function removeParam(key, sourceURL) {
let rtn = sourceURL.split('?')[0];
let param;
let params_arr = [];
let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : '';
if (queryString !== '') {
params_arr = queryString.split('&');
for (let i = params_arr.length - 1; i >= 0; i -= 1) {
param = params_arr[i].split('=')[0];
if (param === key) {
params_arr.splice(i, 1);
}
}
rtn = `${rtn}?${params_arr.join('&')}`;
}
return rtn;
}
export const viewAllComments = () => {
// remove the comment_id url param
const modifiedUrl = removeParam('comment_id', location.href);
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, modifiedUrl);
// also change the parent url
pym.sendMessage('coral-view-all-comments');
} catch (e) { /* not sure if we're worried about old browsers */ }
return {type: actions.VIEW_ALL_COMMENTS};
};
@@ -1,11 +1,3 @@
// this component will
// render its children
// render a like button
// render a permalink button
// render a reply button
// render a flag button
// translate things?
import React, {PropTypes} from 'react';
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
@@ -16,25 +8,33 @@ import Content from 'coral-plugin-commentcontent/CommentContent';
import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
import {
BestButton,
IfUserCanModifyBest,
BEST_TAG,
commentIsBest,
BestIndicator
} from 'coral-plugin-best/BestButton';
import Slot from 'coral-framework/components/Slot';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu';
import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils';
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
import styles from './Comment.css';
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
const isStaff = tags => !tags.every(t => t.name !== 'STAFF');
// hold actions links (e.g. Like, Reply) along the comment footer
// hold actions links (e.g. Reply) along the comment footer
const ActionButton = ({children}) => {
return <span className="comment__action-button comment__action-button--nowrap">{ children }</span>;
return (
<span className="comment__action-button comment__action-button--nowrap">
{children}
</span>
);
};
class Comment extends React.Component {
constructor(props) {
super(props);
this.state = {replyBoxVisible: false};
@@ -49,7 +49,6 @@ class Comment extends React.Component {
setActiveReplyBox: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
postFlag: PropTypes.func.isRequired,
postLike: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
parentId: PropTypes.string,
highlighted: PropTypes.string,
@@ -80,7 +79,8 @@ class Comment extends React.Component {
PropTypes.shape({
body: PropTypes.string.isRequired,
id: PropTypes.string.isRequired
})),
})
),
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
@@ -97,10 +97,10 @@ class Comment extends React.Component {
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
}
ignoreUser: React.PropTypes.func
};
render () {
render() {
const {
comment,
parentId,
@@ -110,7 +110,6 @@ class Comment extends React.Component {
postItem,
addNotification,
showSignInDialog,
postLike,
highlighted,
postFlag,
postDontAgree,
@@ -124,12 +123,14 @@ class Comment extends React.Component {
disableReply,
commentIsIgnored,
maxCharCount,
charCountEnable,
charCountEnable
} = this.props;
const likeSummary = getActionSummary('LikeActionSummary', comment);
const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary('DontAgreeActionSummary', comment);
const dontAgreeSummary = getActionSummary(
'DontAgreeActionSummary',
comment
);
let myFlag = null;
if (iPerformedThisAction('FlagActionSummary', comment)) {
myFlag = flagSummary.find(s => s.current_user);
@@ -137,91 +138,115 @@ class Comment extends React.Component {
myFlag = dontAgreeSummary.find(s => s.current_user);
}
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
let commentClass = parentId
? `reply ${styles.Reply}`
: `comment ${styles.Comment}`;
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async function (...args) {
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
try {
return await fn(...args);
} catch (error) {
addNotification('error', errorToMessage(error));
throw error;
}
};
const notifyOnError = (fn, errorToMessage) =>
async function(...args) {
if (typeof errorToMessage !== 'function') {
errorToMessage = error => error.message;
}
try {
return await fn(...args);
} catch (error) {
addNotification('error', errorToMessage(error));
throw error;
}
};
const addBestTag = notifyOnError(() => addCommentTag({
id: comment.id,
tag: BEST_TAG,
}), () => 'Failed to tag comment as best');
const addBestTag = notifyOnError(
() =>
addCommentTag({
id: comment.id,
tag: BEST_TAG
}),
() => 'Failed to tag comment as best'
);
const removeBestTag = notifyOnError(() => removeCommentTag({
id: comment.id,
tag: BEST_TAG,
}), () => 'Failed to remove best comment tag');
const removeBestTag = notifyOnError(
() =>
removeCommentTag({
id: comment.id,
tag: BEST_TAG
}),
() => 'Failed to remove best comment tag'
);
return (
<div
className={commentClass}
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}>
style={{marginLeft: depth * 30}}
>
<hr aria-hidden={true} />
<div className={highlighted === comment.id ? 'highlighted-comment' : ''}>
<AuthorName
author={comment.user}/>
{ isStaff(comment.tags)
? <TagLabel>Staff</TagLabel>
: null }
<div
className={highlighted === comment.id ? 'highlighted-comment' : ''}
>
<AuthorName author={comment.user} />
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
{ commentIsBest(comment)
{commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel>
: null }
: null}
<PubDate created_at={comment.created_at} />
<Slot fill="commentInfoBar" comment={comment} commentId={comment.id} inline/>
{ (currentUser && (comment.user.id !== currentUser.id))
<Slot
fill="commentInfoBar"
data={this.props.data}
root={this.props.root}
comment={comment}
commentId={comment.id}
inline
/>
{currentUser && comment.user.id !== currentUser.id
? <span className={styles.topRightMenu}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
addNotification={addNotification}
/>
</span>
: null
}
: null}
<Content body={comment.body} />
<Slot fill="commentContent" />
<div className="commentActionsLeft comment__action-container">
<ActionButton>
{/* TODO implmement iPerformedThisAction for the like */}
<LikeButton
totalLikes={getTotalActionCount('LikeActionSummary', comment)}
like={likeSummary[0]}
id={comment.id}
postLike={postLike}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</ActionButton>
{
!disableReply &&
<Slot
fill="commentReactions"
data={this.props.data}
root={this.props.root}
comment={comment}
commentId={comment.id}
inline
/>
{!disableReply &&
<ActionButton>
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false} />
</ActionButton>
}
banned={false}
/>
</ActionButton>}
<ActionButton>
<IfUserCanModifyBest user={currentUser}>
<BestButton
isBest={commentIsBest(comment)}
addBest={addBestTag}
removeBest={removeBestTag} />
removeBest={removeBestTag}
/>
</IfUserCanModifyBest>
</ActionButton>
<Slot fill="commentDetail" comment={comment} commentId={comment.id} inline/>
<Slot
fill="commentActions"
data={this.props.data}
root={this.props.root}
comment={comment}
commentId={comment.id}
inline
/>
</div>
<div className="commentActionsRight comment__action-container">
<ActionButton>
@@ -237,12 +262,12 @@ class Comment extends React.Component {
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
currentUser={currentUser}
/>
</ActionButton>
</div>
</div>
{
activeReplyBox === comment.id
{activeReplyBox === comment.id
? <ReplyBox
commentPostedHandler={() => {
setActiveReplyBox('');
@@ -254,15 +279,16 @@ class Comment extends React.Component {
addNotification={addNotification}
authorId={currentUser.id}
postItem={postItem}
assetId={asset.id} />
: null
}
{
comment.replies &&
assetId={asset.id}
/>
: null}
{comment.replies &&
comment.replies.map(reply => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
@@ -273,7 +299,6 @@ class Comment extends React.Component {
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
@@ -284,12 +309,11 @@ class Comment extends React.Component {
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply} />;
})
}
{
comment.replies &&
<div className='coral-load-more-replies'>
comment={reply}
/>;
})}
{comment.replies &&
<div className="coral-load-more-replies">
<LoadMore
assetId={asset.id}
comments={comment.replies}
@@ -297,9 +321,9 @@ class Comment extends React.Component {
topLevel={false}
replyCount={comment.replyCount}
moreComments={comment.replyCount > comment.replies.length}
loadMore={loadMore}/>
</div>
}
loadMore={loadMore}
/>
</div>}
</div>
);
}
@@ -0,0 +1,87 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
import {TabBar, Tab, TabContent, Button} from 'coral-ui';
import Stream from '../containers/Stream';
import Count from 'coral-plugin-comment-count/CommentCount';
import UserBox from 'coral-sign-in/components/UserBox';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
export default class Embed extends React.Component {
changeTab = (tab) => {
switch(tab) {
case 0:
this.props.setActiveTab('stream');
break;
case 1:
this.props.setActiveTab('profile');
// TODO: move data fetching to profile container.
this.props.data.refetch();
break;
case 2:
this.props.setActiveTab('config');
// TODO: move data fetching to config container.
this.props.data.refetch();
break;
}
}
handleShowProfile = () => this.props.setActiveTab('profile');
render () {
const {activeTab, logout, viewAllComments, commentId} = this.props;
const {asset: {totalCommentCount}} = this.props.root;
const {loggedIn, isAdmin, user} = this.props.auth;
const userBox = <UserBox user={user} onLogout={logout} onShowProfile={this.handleShowProfile}/>;
return (
<div>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={totalCommentCount}/></Tab>
<Tab>{lang.t('myProfile')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
commentId &&
<Button
cStyle='darkGrey'
style={{float: 'right'}}
onClick={viewAllComments}
>
{lang.t('showAllComments')}
</Button>
}
<TabContent show={activeTab === 'stream'}>
{ loggedIn ? userBox : null }
<Stream data={this.props.data} root={this.props.root} />
</TabContent>
<TabContent show={activeTab === 'profile'}>
<ProfileContainer />
</TabContent>
<TabContent show={activeTab === 'config'}>
<RestrictedContent restricted={!loggedIn}>
{ loggedIn ? userBox : null }
<ConfigureStreamContainer />
</RestrictedContent>
</TabContent>
</div>
</div>
);
}
}
Embed.propTypes = {
data: React.PropTypes.shape({
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired,
};
@@ -1,7 +1,7 @@
import React, {PropTypes} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments';
import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {Button} from 'coral-ui';
const lang = new I18n(translations);
@@ -3,23 +3,23 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, updateCountCache}) => (e) => {
const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, setCommentCountCache}) => (e) => {
e.preventDefault();
updateCountCache(assetId, commentCount);
setCommentCountCache(commentCount);
loadMore({
asset_id: assetId,
limit: 500,
cursor: firstCommentDate,
assetId,
sort: 'CHRONOLOGICAL'
}, true);
};
const NewCount = (props) => {
const newComments = props.commentCount - props.countCache;
const newComments = props.commentCount - props.commentCountCache;
return <div className='coral-new-comments coral-load-more'>
{
props.countCache && newComments > 0 ?
props.commentCountCache && newComments > 0 ?
<button onClick={onLoadMoreClick(props)}>
{newComments === 1
? lang.t('newCount', newComments, lang.t('comment'))
@@ -32,7 +32,7 @@ const NewCount = (props) => {
NewCount.propTypes = {
commentCount: PropTypes.number.isRequired,
countCache: PropTypes.number,
commentCountCache: PropTypes.number,
loadMore: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired,
firstCommentDate: PropTypes.string.isRequired
@@ -0,0 +1,211 @@
import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
import Comment from '../containers/Comment';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import {ModerationLink} from 'coral-plugin-moderation';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import SuspendedAccount from 'coral-framework/components/SuspendedAccount';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ChangeUsernameContainer
from 'coral-sign-in/containers/ChangeUsernameContainer';
class Stream extends React.Component {
setActiveReplyBox = reactKey => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
} else {
this.props.setActiveReplyBox(reactKey);
}
};
render() {
const {
root: {asset, asset: {comments}, comment, myIgnoredUsers},
postItem,
addNotification,
postFlag,
postDontAgree,
loadMore,
deleteAction,
showSignInDialog,
addCommentTag,
removeCommentTag,
pluginProps,
ignoreUser,
auth: {loggedIn, isAdmin, user},
commentCountCache,
editName
} = this.props;
const open = asset.closedAt === null;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
const highlightedComment = comment && comment.parent
? comment.parent
: comment;
const banned = user && user.status === 'BANNED';
const hasOlderComments = !!(asset &&
asset.lastComment &&
asset.lastComment.id !== asset.comments[asset.comments.length - 1].id);
// Find the created_at date of the first comment. If no comments exist, set the date to a week ago.
const firstCommentDate = asset.comments[0]
? asset.comments[0].created_at
: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
const commentIsIgnored = comment =>
myIgnoredUsers && myIgnoredUsers.includes(comment.user.id);
return (
<div id="stream">
{open
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<QuestionBox
content={asset.settings.questionBoxContent}
enable={asset.settings.questionBoxEnable}
/>
<RestrictedContent
restricted={banned}
restrictedComp={
<SuspendedAccount
canEditName={user && user.canEditName}
editName={editName}
/>
}
>
{user
? <CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
setCommentCountCache={this.props.setCommentCountCache}
commentCountCache={commentCountCache}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
authorId={user.id}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>
: null}
</RestrictedContent>
</div>
: <p>{asset.settings.closedMessage}</p>}
{!loggedIn &&
<Button
id="coralSignInButton"
onClick={this.props.showSignInDialog}
full
>
Sign in to comment
</Button>}
{loggedIn &&
user &&
<ChangeUsernameContainer loggedIn={loggedIn} user={user} />}
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
{/* the highlightedComment is isolated after the user followed a permalink */}
{highlightedComment
? <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={this.props.postItem}
asset={asset}
currentUser={user}
highlighted={comment.id}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
key={highlightedComment.id}
commentIsIgnored={commentIsIgnored}
reactKey={highlightedComment.id}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>
: <div>
<NewCount
commentCount={asset.commentCount}
commentCountCache={commentCountCache}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
setCommentCountCache={this.props.setCommentCountCache}
/>
<div className="embed__stream">
{comments.map(
comment =>
(commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
data={this.props.data}
root={this.props.root}
disableReply={!open}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
pluginProps={pluginProps}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>)
)}
</div>
<LoadMore
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={hasOlderComments}
loadMore={this.props.loadMore}
/>
</div>}
</div>
);
}
}
Stream.propTypes = {
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
// dispatch action to add a tag to a comment
addCommentTag: PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func
};
export default Stream;
@@ -0,0 +1 @@
export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB';
@@ -0,0 +1,5 @@
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const SET_COMMENT_COUNT_CACHE = 'SET_COMMENT_COUNT_CACHE';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
@@ -0,0 +1,48 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
const pluginFragments = getSlotsFragments([
'streamQuestionArea',
'commentInputArea',
'commentInputDetailArea',
'commentInfoBar',
'commentActions',
'commentContent',
'commentReactions'
]);
export default withFragments({
root: gql`
fragment Comment_root on RootQuery {
__typename
${pluginFragments.spreads('root')}
}
${pluginFragments.definitions('root')}
`,
comment: gql`
fragment Comment_comment on Comment {
id
body
created_at
status
tags {
name
}
user {
id
name: username
}
action_summaries {
__typename
count
current_user {
id
}
}
${pluginFragments.spreads('comment')}
}
${pluginFragments.definitions('comment')}
`
})(Comment);
@@ -0,0 +1,118 @@
import React from 'react';
import {compose, gql, graphql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import isEqual from 'lodash/isEqual';
import branch from 'recompose/branch';
import renderComponent from 'recompose/renderComponent';
import {Spinner} from 'coral-ui';
import {authActions, assetActions, pym} from 'coral-framework';
import {getDefinitionName, separateDataAndRoot} from 'coral-framework/utils';
import Embed from '../components/Embed';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
import {setActiveTab} from '../actions/embed';
import Stream from './Stream';
const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
componentDidMount() {
pym.sendMessage('childReady');
}
componentWillReceiveProps(nextProps) {
if(this.props.root.me && !nextProps.root.me) {
// Refetch because on logout `excludeIgnored` becomes `false`.
// TODO: logout via mutation and obsolete this?
this.props.data.refetch();
}
const {fetchAssetSuccess} = this.props;
if(!isEqual(nextProps.root.asset, this.props.root.asset)) {
// TODO: remove asset data from redux store.
fetchAssetSuccess(nextProps.root.asset);
const {setCommentCountCache, commentCountCache} = this.props;
const {asset} = nextProps.root;
if (commentCountCache === -1) {
setCommentCountCache(asset.commentCount);
}
}
}
componentDidUpdate(prevProps) {
if(!isEqual(prevProps.root.comment, this.props.root.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
}
}
render() {
if (!this.props.root.asset) {
return <Spinner />;
}
return <Embed {...this.props} />;
}
}
const EMBED_QUERY = gql`
query EmbedQuery($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) {
asset(id: $assetId, url: $assetUrl) {
totalCommentCount(excludeIgnored: $excludeIgnored)
}
me {
status
}
...${getDefinitionName(Stream.fragments.root)}
}
${Stream.fragments.root}
`;
export const withQuery = graphql(EMBED_QUERY, {
options: ({auth, commentId, assetId, assetUrl}) => ({
variables: {
assetId,
assetUrl,
commentId,
hasComment: commentId !== '',
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
},
}),
props: ({data}) => separateDataAndRoot(data),
});
const mapStateToProps = state => ({
auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({
fetchAssetSuccess,
checkLogin,
setCommentCountCache,
viewAllComments,
logout,
setActiveTab,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch(
props => !props.auth.checkedInitialLogin,
renderComponent(Spinner),
),
withQuery,
)(EmbedContainer);
@@ -0,0 +1,247 @@
import React from 'react';
import {gql, compose} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream';
import {postComment, postFlag, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user';
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getDefinitionName} from 'coral-framework/utils';
const {showSignInDialog} = authActions;
const {addNotification} = notificationActions;
class StreamContainer extends React.Component {
getCounts = (variables) => {
return this.props.data.fetchMore({
query: LOAD_COMMENT_COUNTS_QUERY,
variables,
// Apollo requires this, even though we don't use it...
updateQuery: data => data,
});
};
// handle paginated requests for more Comments pertaining to the Asset
loadMore = ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit, // how many comments are we returning
cursor, // the date of the first/last comment depending on the sort order
parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
asset_id, // the id of the asset we're currently on
sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => {
let updatedAsset;
if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment
let comment = {};
if (oldData.comment && oldData.comment.parent) {
// put comments (replies) onto the oldData.comment.parent object
// the initial comment permalinked was a reply
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id');
comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')};
} else if (oldData.comment) {
// put the comments (replies) directly onto oldData.comment
// the initial comment permalinked was a top-level comment
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id');
comment.replies = sortBy(uniqReplies, 'created_at');
}
updatedAsset = {
...oldData,
comment: {
...oldData.comment,
...comment
}
};
} else if (parent_id) { // If loading more replies
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: oldData.asset.comments.map(comment => {
// since the dipslayed replies and the returned replies can overlap,
// pull out the unique ones.
const uniqueReplies = uniqBy([...new_top_level_comments, ...comment.replies], 'id');
// since we just gave the returned replies precedence, they're now out of order.
// resort according to date.
return comment.id === parent_id
? {...comment, replies: sortBy(uniqueReplies, 'created_at')}
: comment;
})
}
};
} else { // If loading more top-level comments
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: newComments ? [...new_top_level_comments.reverse(), ...oldData.asset.comments]
: [...oldData.asset.comments, ...new_top_level_comments]
}
};
}
return updatedAsset;
}
});
};
componentDidMount() {
if (this.props.previousTab) {
this.props.data.refetch();
}
this.countPoll = setInterval(() => {
this.getCounts(this.props.data.variables);
}, NEW_COMMENT_COUNT_POLL_INTERVAL);
}
componentWillUnmount() {
clearInterval(this.countPoll);
}
render() {
return <Stream {...this.props} loadMore={this.loadMore}/>;
}
}
const LOAD_COMMENT_COUNTS_QUERY = gql`
query LoadCommentCounts($assetUrl: String, $assetId: ID, $excludeIgnored: Boolean) {
asset(id: $assetId, url: $assetUrl) {
id
commentCount(excludeIgnored: $excludeIgnored)
comments(limit: 10) {
id
replyCount(excludeIgnored: $excludeIgnored)
}
}
}
`;
const LOAD_MORE_QUERY = gql`
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3) {
...${getDefinitionName(Comment.fragments.comment)}
}
}
}
${Comment.fragments.comment}
`;
const fragments = {
root: gql`
fragment Stream_root on RootQuery {
comment(id: $commentId) @include(if: $hasComment) {
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies {
...${getDefinitionName(Comment.fragments.comment)}
}
parent {
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies {
...${getDefinitionName(Comment.fragments.comment)}
}
}
}
asset(id: $assetId, url: $assetUrl) {
id
title
url
closedAt
created_at
settings {
moderation
infoBoxEnable
infoBoxContent
premodLinksEnable
questionBoxEnable
questionBoxContent
closeTimeout
closedMessage
charCountEnable
charCount
requireEmailConfirmation
}
lastComment {
id
}
commentCount(excludeIgnored: $excludeIgnored)
totalCommentCount(excludeIgnored: $excludeIgnored)
comments(limit: 10, excludeIgnored: $excludeIgnored) {
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3, excludeIgnored: $excludeIgnored) {
...${getDefinitionName(Comment.fragments.comment)}
}
}
}
myIgnoredUsers {
id,
username,
}
me {
status
}
...${getDefinitionName(Comment.fragments.root)}
}
${Comment.fragments.root}
${Comment.fragments.comment}
`,
};
const mapStateToProps = state => ({
auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache,
activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
previousTab: state.embed.previousTab,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({
showSignInDialog,
addNotification,
setActiveReplyBox,
editName,
setCommentCountCache,
}, dispatch);
export default compose(
withFragments(fragments),
connect(mapStateToProps, mapDispatchToProps),
postComment,
postFlag,
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
)(StreamContainer);
+10 -1
View File
@@ -3,12 +3,21 @@ import {render} from 'react-dom';
import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/services/client';
import localStore from 'coral-framework/services/store';
import {checkLogin} from 'coral-framework/actions/auth';
import reducers from './reducers';
import localStore, {injectReducers} from 'coral-framework/services/store';
import AppRouter from './AppRouter';
injectReducers(reducers);
const store = (window.opener && window.opener.coralStore) ? window.opener.coralStore : localStore;
// Don't run this in the popup.
if (store === localStore) {
store.dispatch(checkLogin());
}
render(
<ApolloProvider client={client} store={store}>
<AppRouter />
@@ -0,0 +1,19 @@
import * as actions from '../constants/embed';
const initialState = {
activeTab: 'stream',
previousTab: '',
};
export default function stream(state = initialState, action) {
switch (action.type) {
case actions.SET_ACTIVE_TAB:
return {
...state,
activeTab: action.tab,
previousTab: state.activeTab,
};
default:
return state;
}
}
@@ -0,0 +1,7 @@
import stream from './stream';
import embed from './embed';
export default {
stream,
embed,
};
@@ -0,0 +1,45 @@
import * as actions from '../constants/stream';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
let vars = query.split('&');
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) === variable) {
return decodeURIComponent(pair[1]);
}
}
// If not found, return null.
return null;
}
const initialState = {
activeReplyBox: '',
commentCountCache: -1,
assetId: getQueryVariable('asset_id'),
assetUrl: getQueryVariable('asset_url'),
commentId: getQueryVariable('comment_id'),
};
export default function stream(state = initialState, action) {
switch (action.type) {
case actions.SET_ACTIVE_REPLY_BOX:
return {
...state,
activeReplyBox: action.id,
};
case actions.SET_COMMENT_COUNT_CACHE:
return {
...state,
commentCountCache: action.amount,
};
case actions.VIEW_ALL_COMMENTS:
return {
...state,
commentId: '',
};
default:
return state;
}
}
+1 -11
View File
@@ -352,17 +352,6 @@ button.comment__action-button[disabled],
/* Flag Styles */
.coral-plugin-flags-container {
position: relative;
}
.coral-plugin-flags-popup span {
min-width: 280px;
bottom: 36px;
position: absolute;
right: 10px;
}
.coral-plugin-flags-popup-form {
margin-bottom: 10px;
}
@@ -399,6 +388,7 @@ button.comment__action-button[disabled],
margin-top: 5px;
width: 75%;
font-size: 16px;
border: 1px solid #ccc;
}
/* Close comments */
+4
View File
@@ -57,6 +57,10 @@ function configurePymParent(pymParent) {
window.document.body.appendChild(snackbar);
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
pymParent.el.firstChild.style.width = '1px';
pymParent.el.firstChild.style.minWidth = '100%';
// Resize parent iframe height when child height changes
pymParent.onMessage('height', function(height) {
if (height !== cachedHeight) {
-35
View File
@@ -1,7 +1,6 @@
import * as actions from '../constants/asset';
import coralApi from '../helpers/response';
import {addNotification} from '../actions/notification';
import {pym} from 'coral-framework';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
@@ -39,7 +38,6 @@ export const updateOpenStream = closedBody => (dispatch, getState) => {
const openStream = () => ({type: actions.OPEN_COMMENTS});
const closeStream = () => ({type: actions.CLOSE_COMMENTS});
export const updateCountCache = (id, count) => ({type: actions.UPDATE_COUNT_CACHE, id, count});
export const updateOpenStatus = status => dispatch => {
if (status === 'open') {
@@ -51,36 +49,3 @@ export const updateOpenStatus = status => dispatch => {
}
};
function removeParam(key, sourceURL) {
let rtn = sourceURL.split('?')[0];
let param;
let params_arr = [];
let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : '';
if (queryString !== '') {
params_arr = queryString.split('&');
for (let i = params_arr.length - 1; i >= 0; i -= 1) {
param = params_arr[i].split('=')[0];
if (param === key) {
params_arr.splice(i, 1);
}
}
rtn = `${rtn}?${params_arr.join('&')}`;
}
return rtn;
}
export const viewAllComments = () => {
// remove the comment_id url param
const modifiedUrl = removeParam('comment_id', location.href);
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, modifiedUrl);
// also change the parent url
pym.sendMessage('coral-view-all-comments');
} catch (e) { /* not sure if we're worried about old browsers */ }
return {type: actions.VIEW_ALL_COMMENTS};
};
+21 -4
View File
@@ -39,10 +39,21 @@ export const showSignInDialog = () => dispatch => {
'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
);
signInPopUp.onbeforeunload = () => {
dispatch(checkLogin());
fetchMe();
// Workaround odd behavior in older WebKit versions, where
// onunload is called twice. (Encountered in IOS 8.3)
let loaded = false;
signInPopUp.onload = () => {
loaded = true;
};
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
signInPopUp.onunload = () => {
if (loaded) {
dispatch(checkLogin());
fetchMe();
}
};
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
};
export const hideSignInDialog = () => dispatch => {
@@ -177,7 +188,13 @@ export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
dispatch(signUpSuccess(user));
})
.catch(error => {
dispatch(signUpFailure(lang.t(`error.${error.message}`)));
let errorMessage = lang.t(`error.${error.message}`);
// if there is no translation defined, just show the error string
if (errorMessage === `error.${error.message}`) {
errorMessage = error.message;
}
dispatch(signUpFailure(errorMessage));
});
};
+9 -13
View File
@@ -1,20 +1,16 @@
import React, {Component} from 'react';
import {getSlotElements} from 'coral-framework/helpers/plugins';
import React from 'react';
import cn from 'classnames';
import styles from './Slot.css';
import {getSlotElements} from 'coral-framework/helpers/plugins';
class Slot extends Component {
render() {
const {fill, inline = false, ...rest} = this.props;
return (
<div className={inline ? styles.inline : ''}>
{getSlotElements(fill, rest)}
</div>
);
}
export default function Slot ({fill, inline = false, ...rest}) {
return (
<div className={cn({[styles.inline]: inline})}>
{getSlotElements(fill, rest)}
</div>
);
}
Slot.propTypes = {
fill: React.PropTypes.string
};
export default Slot;
@@ -8,6 +8,4 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE';
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
@@ -1,2 +0,0 @@
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000;
@@ -1,7 +1,6 @@
import {graphql} from 'react-apollo';
import POST_COMMENT from './postComment.graphql';
import POST_FLAG from './postFlag.graphql';
import POST_LIKE from './postLike.graphql';
import POST_DONT_AGREE from './postDontAgree.graphql';
import DELETE_ACTION from './deleteAction.graphql';
import ADD_COMMENT_TAG from './addCommentTag.graphql';
@@ -9,10 +8,6 @@ import REMOVE_COMMENT_TAG from './removeCommentTag.graphql';
import IGNORE_USER from './ignoreUser.graphql';
import STOP_IGNORING_USER from './stopIgnoringUser.graphql';
import MY_IGNORED_USERS from '../queries/myIgnoredUsers.graphql';
import STREAM_QUERY from '../queries/streamQuery.graphql';
import {variablesForStreamQuery} from '../queries';
import commentView from '../fragments/commentView.graphql';
export const postComment = graphql(POST_COMMENT, {
@@ -45,7 +40,7 @@ export const postComment = graphql(POST_COMMENT, {
}
},
updateQueries: {
AssetQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => {
EmbedQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => {
if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return oldData;
@@ -61,7 +56,7 @@ export const postComment = graphql(POST_COMMENT, {
...oldData.asset,
comments: oldData.asset.comments.map((oldComment) => {
return oldComment.id === parent_id
? {...oldComment, replies: [...oldComment.replies, comment]}
? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1}
: oldComment;
})
}
@@ -87,17 +82,6 @@ export const postComment = graphql(POST_COMMENT, {
}),
});
export const postLike = graphql(POST_LIKE, {
props: ({mutate}) => ({
postLike: (like) => {
return mutate({
variables: {
like
}
});
}}),
});
export const postFlag = graphql(POST_FLAG, {
props: ({mutate}) => ({
postFlag: (flag) => {
@@ -155,6 +139,7 @@ export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, {
}}),
});
// TODO: don't rely on refetching.
export const ignoreUser = graphql(IGNORE_USER, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
@@ -162,15 +147,16 @@ export const ignoreUser = graphql(IGNORE_USER, {
variables: {
id,
},
refetchQueries: [{
query: MY_IGNORED_USERS,
}]
refetchQueries: [
'EmbedQuery', 'myIgnoredUsers',
]
});
}}),
});
// TODO: don't rely on refetching.
export const stopIgnoringUser = graphql(STOP_IGNORING_USER, {
props: ({mutate, ownProps}) => {
props: ({mutate}) => {
return {
stopIgnoringUser: ({id}) => {
return mutate({
@@ -178,13 +164,7 @@ export const stopIgnoringUser = graphql(STOP_IGNORING_USER, {
id,
},
refetchQueries: [
{
query: MY_IGNORED_USERS,
},
{
query: STREAM_QUERY,
variables: variablesForStreamQuery(ownProps),
}
'EmbedQuery', 'myIgnoredUsers',
]
});
}
@@ -1,10 +0,0 @@
mutation CreateLike ($like: CreateLikeInput!) {
createLike(like:$like) {
like {
id
}
errors {
translation_key
}
}
}
@@ -1,13 +0,0 @@
#import "../fragments/commentView.graphql"
query commentQuery($id: ID!) {
comment(id: $id) {
...commentView
parent {
...commentView
replies {
...commentView
}
}
}
}
@@ -1,10 +0,0 @@
query LoadCommentCounts($asset_id: ID, $limit: Int = 5, $sort: SORT_ORDER) {
asset(id: $asset_id) {
id
commentCount
comments(sort: $sort, limit: $limit) {
id
replyCount
}
}
}
@@ -1,153 +1,6 @@
import {graphql} from 'react-apollo';
import STREAM_QUERY from './streamQuery.graphql';
import LOAD_MORE from './loadMore.graphql';
import GET_COUNTS from './getCounts.graphql';
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
import MY_IGNORED_USERS from './myIgnoredUsers.graphql';
import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
let vars = query.split('&');
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) === variable) {
return decodeURIComponent(pair[1]);
}
}
// If not found, return null.
return null;
}
// get the counts of the top-level comments
export const getCounts = (data) => ({asset_id, limit, sort}) => {
return data.fetchMore({
query: GET_COUNTS,
variables: {
asset_id,
limit,
sort,
excludeIgnored: data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{asset}}) => {
return {
...oldData,
asset: {
...oldData.asset,
commentCount: asset.commentCount
}
};
}
});
};
// handle paginated requests for more Comments pertaining to the Asset
export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => {
return data.fetchMore({
query: LOAD_MORE,
variables: {
limit, // how many comments are we returning
cursor, // the date of the first/last comment depending on the sort order
parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
asset_id, // the id of the asset we're currently on
sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
excludeIgnored: data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => {
let updatedAsset;
if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment
let comment = {};
if (oldData.comment && oldData.comment.parent) {
// put comments (replies) onto the oldData.comment.parent object
// the initial comment permalinked was a reply
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id');
comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')};
} else if (oldData.comment) {
// put the comments (replies) directly onto oldData.comment
// the initial comment permalinked was a top-level comment
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id');
comment.replies = sortBy(uniqReplies, 'created_at');
}
updatedAsset = {
...oldData,
comment: {
...oldData.comment,
...comment
}
};
} else if (parent_id) { // If loading more replies
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: oldData.asset.comments.map(comment => {
// since the dipslayed replies and the returned replies can overlap,
// pull out the unique ones.
const uniqueReplies = uniqBy([...new_top_level_comments, ...comment.replies], 'id');
// since we just gave the returned replies precedence, they're now out of order.
// resort according to date.
return comment.id === parent_id
? {...comment, replies: sortBy(uniqueReplies, 'created_at')}
: comment;
})
}
};
} else { // If loading more top-level comments
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: newComments ? [...new_top_level_comments.reverse(), ...oldData.asset.comments]
: [...oldData.asset.comments, ...new_top_level_comments]
}
};
}
return updatedAsset;
}
});
};
export const variablesForStreamQuery = ({auth}) => {
// where the query string is from the embeded iframe url
let comment_id = getQueryVariable('comment_id');
let has_comment = comment_id != null;
return {
asset_id: getQueryVariable('asset_id'),
asset_url: getQueryVariable('asset_url'),
comment_id: has_comment ? comment_id : 'no-comment',
has_comment,
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
};
};
// load the comment stream.
export const queryStream = graphql(STREAM_QUERY, {
options: (props) => {
return {
variables: variablesForStreamQuery(props)
};
},
props: ({data}) => ({
data,
loadMore: loadMore(data),
getCounts: getCounts(data),
})
});
export const myCommentHistory = graphql(MY_COMMENT_HISTORY, {});
@@ -1,11 +0,0 @@
#import "../fragments/commentView.graphql"
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
...commentView
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3) {
...commentView
}
}
}
@@ -1,53 +0,0 @@
#import "../fragments/commentView.graphql"
query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!, $excludeIgnored: Boolean) {
# the comment here is for loading one comment and it's children, probably after following a permalink
# $has_comment is derived from the comment_id query param in the iframe url,
# which is in turn pulled from the host page url
comment(id: $comment_id) @include(if: $has_comment) {
...commentView
replyCount(excludeIgnored: $excludeIgnored)
replies {
...commentView
}
parent {
...commentView
replyCount(excludeIgnored: $excludeIgnored)
replies {
...commentView
}
}
}
asset(id: $asset_id, url: $asset_url) {
id
title
url
closedAt
created_at
settings {
moderation
infoBoxEnable
infoBoxContent
premodLinksEnable
questionBoxEnable
questionBoxContent
closeTimeout
closedMessage
charCountEnable
charCount
requireEmailConfirmation
}
lastComment {
id
}
commentCount(excludeIgnored: $excludeIgnored)
totalCommentCount(excludeIgnored: $excludeIgnored)
comments(limit: 10, excludeIgnored: $excludeIgnored) {
...commentView
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3, excludeIgnored: $excludeIgnored) {
...commentView
}
}
}
}
+54
View File
@@ -1,7 +1,11 @@
import React from 'react';
import merge from 'lodash/merge';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import uniq from 'lodash/uniq';
import plugins from 'pluginsConfig';
import {gql} from 'react-apollo';
import {getDefinitionName} from 'coral-framework/utils';
export const pluginReducers = merge(
...plugins
@@ -19,3 +23,53 @@ export function getSlotElements(slot, props = {}) {
return components
.map((component, i) => React.createElement(component, {...props, key: i}));
}
function getComponentFragments(components) {
return components
.map(c => c.fragments)
.filter(fragments => fragments)
.reduce((res, fragments) => {
Object.keys(fragments).forEach(key => {
if (!(key in res)) {
res[key] = {spreads: '', definitions: ''};
}
res[key].spreads += `...${getDefinitionName(fragments[key])}\n`;
res[key].definitions = gql`${res[key].definitions}${fragments[key]}`;
});
return res;
}, {});
}
/**
* Returns an object that can be used to compose fragments or queries.
*
* Example:
* const pluginFragments = getSlotsFragments(['commentInfoBar', 'commentActions']);
* const rootFragment = gql`
* fragment Comment_root on RootQuery {
+ ${pluginFragments.spreads('root')}
* }
* ${pluginFragments.definitions('root')}
* `;
*/
export function getSlotsFragments(slots) {
if (!Array.isArray(slots)) {
slots = [slots];
}
const components = uniq(flattenDeep(slots.map(slot => {
return plugins
.filter(o => o.module.slots[slot])
.map(o => o.module.slots[slot]);
})));
const fragments = getComponentFragments(components);
return {
spreads(key) {
return (fragments[key] && fragments[key].spreads) || '';
},
definitions(key) {
return (fragments[key] && fragments[key].definitions) || '';
},
};
}
@@ -0,0 +1,18 @@
import React from 'react';
// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38.
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
export default fragments => WrappedComponent => {
class WithFragments extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
WithFragments.fragments = fragments;
WithFragments.displayName = `WithFragments(${getDisplayName(WrappedComponent)})`;
return WithFragments;
};
-3
View File
@@ -19,9 +19,6 @@ export default function asset (state = initialState, action) {
case actions.UPDATE_ASSET_SETTINGS_SUCCESS:
return state
.setIn(['settings'], action.settings);
case actions.UPDATE_COUNT_CACHE:
return state
.setIn(['countCache', action.id], action.count);
default:
return state;
}
+8 -1
View File
@@ -8,6 +8,7 @@ const initialState = Map({
user: null,
showSignInDialog: false,
showCreateUsernameDialog: false,
checkedInitialLogin: false,
view: 'SIGNIN',
error: '',
passwordRequestSuccess: null,
@@ -71,10 +72,12 @@ export default function auth (state = initialState, action) {
.set('isLoading', true);
case actions.CHECK_LOGIN_FAILURE:
return state
.set('checkedInitialLogin', true)
.set('loggedIn', false)
.set('user', null);
case actions.CHECK_LOGIN_SUCCESS:
return state
.set('checkedInitialLogin', true)
.set('loggedIn', true)
.set('isAdmin', action.isAdmin)
.set('user', purge(action.user));
@@ -114,7 +117,11 @@ export default function auth (state = initialState, action) {
.set('isLoading', false)
.set('successSignUp', true);
case actions.LOGOUT_SUCCESS:
return initialState;
return state
.set('user', null)
.set('isLoading', false)
.set('loggedIn', false)
.set('isAdmin', false);
case actions.INVALID_FORM:
return state
.set('error', action.error);
+13 -1
View File
@@ -1,6 +1,18 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
// TODO: replace absolute reference with something loaded from the store/page.
// const wsClient = new SubscriptionClient('ws://localhost:3000/api/v1/live', {
// reconnect: true
// });
// const networkInterface = addGraphQLSubscriptions(
// getNetworkInterface(),
// wsClient,
// );
const networkInterface = getNetworkInterface();
export const client = new ApolloClient({
connectToDevTools: true,
queryTransformer: addTypename,
@@ -10,7 +22,7 @@ export const client = new ApolloClient({
}
return null;
},
networkInterface: getNetworkInterface()
networkInterface
});
export default client;
+13 -5
View File
@@ -24,14 +24,22 @@ if (window.devToolsExtension) {
middlewares.push(window.devToolsExtension());
}
const store = createStore(
combineReducers({
...mainReducer,
apollo: client.reducer()
}),
let storeReducers = {
...mainReducer,
apollo: client.reducer()
};
export const store = createStore(
combineReducers(storeReducers),
{},
compose(...middlewares)
);
export default store;
export function injectReducers(reducers) {
storeReducers = {...storeReducers, ...reducers};
store.replaceReducer(combineReducers(storeReducers));
}
window.coralStore = store;
+32
View File
@@ -29,3 +29,35 @@ export const getMyActionSummary = (type, comment) => {
export const getActionSummary = (type, comment) => {
return comment.action_summaries.filter(a => a.__typename === type);
};
/**
* Get name of first (or $pos-th) definition
*/
export function getDefinitionName(doc, pos = 0) {
return doc.definitions[pos].name.value;
}
/**
* Separate apollo `data` props into `data` and `root`.
* `data` will contain props like `loading`, `fetchMore`...
* while `root` contains the actual query data.
*/
export function separateDataAndRoot(
{
fetchMore,
loading,
networkStatus,
refetch,
startPolling,
stopPolling,
subscribeToMore,
updateQuery,
variables,
...root,
}) {
return {
data: {fetchMore, loading, networkStatus, refetch, startPolling,
stopPolling, subscribeToMore, updateQuery, variables},
root,
};
}
+17 -15
View File
@@ -1,13 +1,13 @@
import React, {Component, PropTypes} from 'react';
import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import {connect} from 'react-redux';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import {Button} from 'coral-ui';
import Slot from 'coral-framework/components/Slot';
import {connect} from 'react-redux';
const name = 'coral-plugin-commentbox';
class CommentBox extends Component {
class CommentBox extends React.Component {
constructor(props) {
super(props);
@@ -24,14 +24,14 @@ class CommentBox extends Component {
postComment = () => {
const {
commentPostedHandler,
postItem,
setCommentCountCache,
commentCountCache,
isReply,
assetId,
parentId,
postItem,
countCache,
addNotification,
updateCountCache,
commentPostedHandler
} = this.props;
let comment = {
@@ -41,7 +41,7 @@ class CommentBox extends Component {
...this.props.commentBox
};
!isReply && updateCountCache(assetId, countCache + 1);
!isReply && setCommentCountCache(commentCountCache + 1);
// Execute preSubmit Hooks
this.state.hooks.preSubmit.forEach(hook => hook());
@@ -55,18 +55,20 @@ class CommentBox extends Component {
if (postedComment.status === 'REJECTED') {
addNotification('error', lang.t('comment-post-banned-word'));
!isReply && updateCountCache(assetId, countCache);
!isReply && setCommentCountCache(commentCountCache);
} else if (postedComment.status === 'PREMOD') {
addNotification('success', lang.t('comment-post-notif-premod'));
!isReply && updateCountCache(assetId, countCache);
!isReply && setCommentCountCache(commentCountCache);
}
if (commentPostedHandler) {
commentPostedHandler();
}
})
.catch((err) => console.error(err));
.catch((err) => {
console.error(err);
!isReply && setCommentCountCache(commentCountCache);
});
this.setState({body: ''});
}
@@ -149,13 +151,14 @@ class CommentBox extends Component {
id={isReply ? 'replyText' : 'commentText'}
onChange={this.handleChange}
rows={3}/>
<Slot fill='commentInputArea' />
</div>
<div className={`${name}-char-count ${length > maxCharCount ? `${name}-char-max` : ''}`}>
{maxCharCount && `${maxCharCount - length} ${lang.t('characters-remaining')}`}
</div>
<div className={`${name}-button-container`}>
<Slot
fill="commentBoxDetail"
fill="commentInputDetailArea"
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
inline
@@ -196,7 +199,6 @@ CommentBox.propTypes = {
authorId: PropTypes.string.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
currentUser: PropTypes.object
};
const mapStateToProps = ({commentBox}) => ({commentBox});
@@ -3,7 +3,7 @@
"post": "Post",
"cancel": "Cancel",
"reply": "Reply",
"comment": "Post a Comment",
"comment": "Post a comment",
"name": "Name",
"comment-post-notif": "Your comment has been posted.",
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
@@ -14,7 +14,7 @@
"post": "Publicar",
"cancel": "Cancelar",
"reply": "Responder",
"comment": "Publicar un Comentario",
"comment": "Publicar un comentario",
"name": "Nombre",
"comment-post-notif": "Tu comentario ha sido publicado.",
"comment-post-notif-premod": "Gracias por el comentario. Nuestro equipo de moderación va a revisarlo muy pronto.",
+11 -2
View File
@@ -19,6 +19,12 @@ class FlagButton extends Component {
localDelete: false
}
componentDidUpdate () {
if (this.popup) { // this will be defined when the reporting popup is opened
this.popup.firstChild.style.top = `${this.flagButton.offsetTop - this.popup.firstChild.clientHeight - 15}px`;
}
}
// When the "report" button is clicked expand the menu
onReportClick = () => {
const {currentUser, deleteAction, flaggedByCurrentUser, flag} = this.props;
@@ -135,7 +141,10 @@ class FlagButton extends Component {
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
<button onClick={!this.props.banned ? this.onReportClick : null} className={`${name}-button`}>
<button
ref={ref => this.flagButton = ref}
onClick={!this.props.banned ? this.onReportClick : null}
className={`${name}-button`}>
{
flagged
? <span className={`${name}-button-text`}>{lang.t('reported')}</span>
@@ -147,7 +156,7 @@ class FlagButton extends Component {
</button>
{
this.state.showMenu &&
<div className={`${name}-popup`}>
<div className={`${name}-popup`} ref={ref => this.popup = ref}>
<PopupMenu>
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
{
-71
View File
@@ -1,71 +0,0 @@
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-likes';
class LikeButton extends Component {
static propTypes = {
like: PropTypes.shape({
current: PropTypes.object,
count: PropTypes.number
}),
id: PropTypes.string,
postLike: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
currentUser: PropTypes.shape({
banned: PropTypes.boolean
}),
}
state = {
localPost: null, // Set to the ID of an action if one is posted
localDelete: false // Set to true is the user deletes an action, unless localPost is already set.
}
render() {
const {like, id, postLike, deleteAction, showSignInDialog, currentUser} = this.props;
let {totalLikes: count} = this.props;
const {localPost, localDelete} = this.state;
const liked = (like && like.current_user && !localDelete) || localPost;
if (localPost) {count += 1;}
if (localDelete) {count -= 1;}
const onLikeClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (currentUser.banned) {
return;
}
if (!liked) { // this comment has not yet been liked by this user.
this.setState({localPost: 'temp'});
postLike({
item_id: id,
item_type: 'COMMENTS'
}).then(({data}) => {
this.setState({localPost: data.createLike.like.id});
});
} else {
this.setState((prev) => prev.localPost ? {...prev, localPost: null} : {...prev, localDelete: true});
deleteAction(localPost || like.current_user.id);
}
};
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked ? 'likedButton' : ''}`}>
<span className={`${name}-button-text`}>{lang.t(liked ? 'liked' : 'like')}</span>
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
<span className={`${name}-like-count`}>{count > 0 && count}</span>
</button>
</div>;
}
}
export default LikeButton;
const lang = new I18n(translations);
@@ -1,5 +1,6 @@
import React from 'react';
const packagename = 'coral-plugin-questionbox';
import Slot from 'coral-framework/components/Slot';
const QuestionBox = ({enable, content}) =>
<div className={`${packagename}-info ${enable ? null : 'hidden'}` }>
@@ -10,6 +11,7 @@ const QuestionBox = ({enable, content}) =>
<div className={`${packagename}-content`}>
{content}
</div>
<Slot fill="streamQuestionArea" />
</div>;
export default QuestionBox;
@@ -12,7 +12,6 @@ import NotLoggedIn from '../components/NotLoggedIn';
import IgnoredUsers from '../components/IgnoredUsers';
import {Spinner} from 'coral-ui';
import CommentHistory from 'coral-plugin-history/CommentHistory';
import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth';
import translations from '../translations';
@@ -35,14 +34,14 @@ class ProfileContainer extends Component {
}
render() {
const {loggedIn, asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props;
const {asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props;
const {me} = this.props.data;
if (data.loading) {
return <Spinner/>;
}
if (!loggedIn || !me) {
if (!me) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
@@ -51,7 +50,7 @@ class ProfileContainer extends Component {
return (
<div>
<h2>{this.props.userData.username}</h2>
<h2>{this.props.user.username}</h2>
{ emailAddress
? <p>{ emailAddress }</p>
: null
@@ -1,5 +1,5 @@
import React from 'react';
import styles from 'coral-embed-stream/src/Comment.css';
import styles from 'coral-embed-stream/src/components/Comment.css';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import Content from 'coral-plugin-commentcontent/CommentContent';
+3 -3
View File
@@ -4,11 +4,11 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const UserBox = ({className, user, logout, changeTab}) => (
const UserBox = ({className, user, onLogout, onShowProfile}) => (
<div className={`${styles.userBox} ${className ? className : ''}`}>
{lang.t('signIn.loggedInAs')}
<a onClick={() => changeTab(1)}>{user.username}</a>. {lang.t('signIn.notYou')}
<a className={styles.logout} onClick={logout} id='logout'>{lang.t('signIn.logout')}</a>
<a onClick={onShowProfile}>{user.username}</a>. {lang.t('signIn.notYou')}
<a className={styles.logout} onClick={onLogout} id='logout'>{lang.t('signIn.logout')}</a>
</div>
);
+1 -1
View File
@@ -149,7 +149,7 @@
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 128px;
&:hover {
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
+5 -1
View File
@@ -1,8 +1,12 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {Icon as IconMDL} from 'react-mdl';
const Icon = ({className = '', name}) => (
<IconMDL className={className} name={name} />
);
Icon.propTypes = {
name: PropTypes.string.isRequired
};
export default Icon;
+7 -3
View File
@@ -1,13 +1,17 @@
.popupMenu {
display: inline-block;
width: inherit;
white-space: normal;
display: block;
position: absolute;
max-width: 98%;
min-width: 50%;
border: solid 1px #999;
box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.3);
box-sizing: border-box;
background: white;
border-radius: 3px;
padding: 20px 10px;
z-index: 3;
z-index: 300;
right: 1%;
}
.popupMenu:before{
+1 -1
View File
@@ -2,5 +2,5 @@ import React from 'react';
import styles from './PopupMenu.css';
export default ({children}) => (
<span className={styles.popupMenu}>{children}</span>
<div className={styles.popupMenu}>{children}</div>
);
+1 -1
View File
@@ -106,7 +106,7 @@ class ErrAuthentication extends APIError {
// ErrContainsProfanity is returned in the event that the middleware detects
// profanity/wordlisted words in the payload.
const ErrContainsProfanity = new APIError('Suspected profanity. If you think this in error, please let us know!', {
const ErrContainsProfanity = new APIError('This username contains elements which are not permitted in our community. If you think this is in error, please contact us or try again.', {
translation_key: 'PROFANITY_ERROR',
status: 400
});
+8 -1
View File
@@ -1,5 +1,6 @@
const loaders = require('./loaders');
const mutators = require('./mutators');
const uuid = require('uuid');
const plugins = require('../services/plugins');
const debug = require('debug')('talk:graph:context');
@@ -31,7 +32,10 @@ const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduc
* Stores the request context.
*/
class Context {
constructor({user = null}) {
constructor({user = null}, pubsub) {
// Generate a new context id for the request.
this.id = uuid.v4();
// Load the current logged in user to `user`, otherwise this'll be null.
if (user) {
@@ -46,6 +50,9 @@ class Context {
// Decorate the plugin context.
this.plugins = decorateContextPlugins(this, contextPlugins);
// Bind the publish/subscribe to the context.
this.pubsub = pubsub;
}
}
+5 -2
View File
@@ -1,5 +1,7 @@
const schema = require('./schema');
const Context = require('./context');
const pubsub = require('./pubsub');
const {createSubscriptionManager} = require('./subscriptions');
module.exports = {
createGraphOptions: (req) => ({
@@ -9,6 +11,7 @@ module.exports = {
// Load in the new context here, this'll create the loaders + mutators for
// the lifespan of this request.
context: new Context(req)
})
context: new Context(req, pubsub)
}),
createSubscriptionManager
};
+7 -1
View File
@@ -16,7 +16,7 @@ const Wordlist = require('../../services/wordlist');
* @param {String} [status='NONE'] the status of the new comment
* @return {Promise} resolves to the created comment
*/
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
// Building array of tags
tags = tags.map(tag => ({name: tag}));
@@ -47,6 +47,12 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id =
Comments.parentCountByAssetID.incr(asset_id);
}
Comments.countByAssetID.incr(asset_id);
if (pubsub) {
// Publish the newly added comment via the subscription.
pubsub.publish('commentAdded', comment);
}
}
return comment;
+5
View File
@@ -0,0 +1,5 @@
const {RedisPubSub} = require('graphql-redis-subscriptions');
const {connectionOptions} = require('../services/redis');
module.exports = new RedisPubSub(connectionOptions);
-2
View File
@@ -5,8 +5,6 @@ const Action = {
return 'DontAgreeAction';
case 'FLAG':
return 'FlagAction';
case 'LIKE':
return 'LikeAction';
}
},
-2
View File
@@ -3,8 +3,6 @@ const ActionSummary = {
switch (action_type) {
case 'FLAG':
return 'FlagActionSummary';
case 'LIKE':
return 'LikeActionSummary';
case 'DONTAGREE':
return 'DontAgreeActionSummary';
}
+4
View File
@@ -5,6 +5,10 @@ module.exports = new GraphQLScalarType({
name: 'Date',
description: 'Date represented as an ISO8601 string',
serialize(value) {
if (typeof value === 'string') {
return value;
}
return value.toISOString();
},
parseValue(value) {

Some files were not shown because too many files have changed in this diff Show More