Compare commits

...
709 Commits
Author SHA1 Message Date
Kim Gardner 8a52290793 Merge pull request #826 from coralproject/featured-adm
Feature Comments for Moderators / Admins
2017-08-02 13:13:13 -04:00
Chi Vinh Le e6b7fcbd9d Show success notification 2017-08-02 23:41:26 +07:00
Chi Vinh Le 29209f968b Merge branch 'featured-adm' of ssh://github.com/coralproject/talk into featured-adm 2017-08-02 22:29:53 +07:00
Chi Vinh Le 2e974ac82f Live update unfeatured 2017-08-02 22:29:01 +07:00
Kim Gardner 948fce9efb Merge branch 'master' into featured-adm 2017-08-02 10:59:08 -04:00
Kim Gardner 61690be021 Merge pull request #829 from coralproject/namespace-portugese-translation
Namespace portugese translations
2017-08-02 10:54:27 -04:00
Chi Vinh Le da0d0349dd Implement approve and live updates 2017-08-02 21:34:48 +07:00
Chi Vinh Le b6b7570bcc Remove subscription code 2017-08-02 18:14:52 +07:00
Chi Vinh Le eb02031354 Namespace portugese translations 2017-08-02 15:21:46 +07:00
Belen Curcio 94901d40f3 Merge branch 'featured-adm' of github.com:coralproject/talk into featured-adm 2017-08-01 18:27:38 -03:00
Belen Curcio 145910c4a6 Merge branch 'master' of github.com:coralproject/talk into featured-adm 2017-08-01 18:27:12 -03:00
Belen Curcio 4e5f1528f9 BE Subscription for Featured Comments 2017-08-01 18:27:02 -03:00
Kim Gardner 975a616ce7 Merge branch 'master' into featured-adm 2017-08-01 16:19:52 -04:00
Kim Gardner de7d5e542b Merge pull request #828 from coralproject/portuguese-translation
Portuguese translations
2017-08-01 16:18:10 -04:00
Belén Curcio ddf15df8a6 Merge branch 'master' into featured-adm 2017-08-01 15:10:31 -03:00
Kim Gardner 9429fe1afe Fix some tenses 2017-08-01 13:50:37 -04:00
Kim Gardner 8157a223d0 Merge branch 'master' into portuguese-translation 2017-08-01 13:38:45 -04:00
Kim Gardner 0039c05a78 Merge pull request #825 from coralproject/provide-valid-optimistic-status
Provide a valid comment status in optimistic response
2017-08-01 13:38:31 -04:00
Kim Gardner b03223b67b Merge branch 'master' into portuguese-translation 2017-08-01 13:37:48 -04:00
Kim Gardner c93c7b6ba1 Update text 2017-08-01 13:34:26 -04:00
Kim Gardner 4ad055197b Missing translation 2017-08-01 13:33:00 -04:00
Kim Gardner eac8c63045 Fix language files 2017-08-01 13:28:59 -04:00
Kim Gardner d300025c00 Update i18n config 2017-08-01 12:55:05 -04:00
Kim Gardner fdda5e5bef Add Portuguese translation of Talk core 2017-08-01 12:53:51 -04:00
Belen Curcio 779260c245 Working featured comments on the admin side 2017-08-01 13:06:21 -03:00
Kim Gardner faa6971961 Merge branch 'master' into provide-valid-optimistic-status 2017-08-01 11:59:34 -04:00
Chi Vinh Le 26c70bcee8 Provide a valid comment status in optimistic response 2017-08-01 22:44:53 +07:00
Belen Curcio 5266bfa647 Components, UI/UX and Styling 2017-08-01 12:38:41 -03:00
Kim Gardner 08a491b3a0 Merge pull request #824 from coralproject/fix-popup-safari-errors
Fix Safari issues for popups
2017-08-01 11:11:14 -04:00
Kim Gardner 1487a454e9 Merge branch 'master' into fix-popup-safari-errors 2017-08-01 11:05:17 -04:00
Kim Gardner c1a0273e84 Merge pull request #813 from coralproject/new-queue
New Queue
2017-08-01 11:04:17 -04:00
Belen Curcio d40242f92f Merge branch 'master' of github.com:coralproject/talk into featured-adm 2017-08-01 11:28:10 -03:00
Chi Vinh Le 5b751b7ba4 Fix Safari issues for popups 2017-08-01 20:32:49 +07:00
Kiwi 270d9b3e22 Merge branch 'master' into new-queue 2017-08-01 14:34:56 +02:00
Wyatt Johnson a4815ff927 Merge pull request #818 from coralproject/babel
Adds support for disabling the babel-polyfill on the parent embed.js
2017-08-01 22:32:20 +10:00
Kiwi fa451a8240 Merge branch 'master' into new-queue 2017-08-01 14:31:18 +02:00
Wyatt Johnson 8672dea500 Merge branch 'master' into babel 2017-08-01 22:26:40 +10:00
Wyatt Johnson 1a470957ab Merge pull request #823 from coralproject/cache-count-expiry
Added support for reconfiguring the comment count cache expiry
2017-08-01 22:26:28 +10:00
Wyatt Johnson dbeb31f0a7 removed more babel-runtime 2017-08-01 22:22:03 +10:00
Wyatt Johnson a99ffa1228 removed all traces of transform-runtime 2017-08-01 22:18:56 +10:00
Wyatt Johnson 2ac6a9d829 changes to babelrc 2017-08-01 22:12:02 +10:00
Chi Vinh Le 9929046536 Fix reported queue take 2 2017-08-01 18:06:06 +07:00
Chi Vinh Le cb874f8670 Change moderate link in user detail 2017-08-01 17:29:36 +07:00
Chi Vinh Le d2c8019408 Change moderate link 2017-08-01 17:28:51 +07:00
Chi Vinh Le 33a30e4f8d Remove unnessecary query fields 2017-08-01 17:24:48 +07:00
Chi Vinh Le 59ea17119d Remove unnessecary arrow function 2017-08-01 17:20:29 +07:00
Chi Vinh Le 2a78ce2bf1 Restore flagged queue 2017-08-01 17:18:52 +07:00
Chi Vinh Le b1b24d0f9d Change admin index route 2017-08-01 17:18:41 +07:00
Chi Vinh Le 3ae6dff098 Change href of moderate stream 2017-08-01 17:16:53 +07:00
Wyatt Johnson de97ad59ec Added configuration for the babel polyfill 2017-08-01 19:14:26 +10:00
Wyatt Johnson 83762b880f Added support for reconfiguring the comment count cache expiry 2017-08-01 09:52:18 +10:00
Kim Gardner 57ff4f10f3 Merge branch 'master' into babel 2017-07-31 19:22:51 -04:00
Kim Gardner bb5f245df8 Merge branch 'master' into new-queue 2017-07-31 19:21:57 -04:00
Kim Gardner b568f1f8a8 Merge pull request #820 from coralproject/comment-context-label
Label for inactive comments
2017-07-31 19:09:31 -04:00
Kim Gardner 4d9227840b Merge branch 'master' into babel 2017-07-31 18:44:11 -04:00
Belen Curcio 78e74336db Moderator Featured Tag/Button 2017-07-31 18:07:37 -03:00
Belen Curcio f3f441eb40 Merge branch 'master' of github.com:coralproject/talk into new-queue 2017-07-31 16:44:54 -03:00
Kim Gardner ef22909c6c Merge branch 'master' into comment-context-label 2017-07-31 15:29:28 -04:00
Kim Gardner a5790ca4df Merge pull request #819 from coralproject/comment-stream-tag-uniformity
create uniformity on tag label styling
2017-07-31 15:25:32 -04:00
Kim Gardner 109ef0cea4 Merge branch 'master' into comment-stream-tag-uniformity 2017-07-31 15:16:17 -04:00
Kim Gardner 1626721622 Merge pull request #821 from coralproject/dont-agree-action
"Don't agree" is not a flag action
2017-07-31 15:15:16 -04:00
Belen Curcio 694669bb85 Removing withRouter 2017-07-31 15:51:32 -03:00
Belen Curcio 4831d2d6a7 Merge branch 'new-queue' of github.com:coralproject/talk into new-queue 2017-07-31 15:47:42 -03:00
Belen Curcio d330bf9c25 Merge branch 'master' of github.com:coralproject/talk into new-queue 2017-07-31 15:47:12 -03:00
Belen Curcio 91d9d65372 Removing logic and adding active tabs 2017-07-31 15:46:06 -03:00
Chi Vinh Le 62a559b6fc "Don't agree" is not a flag action 2017-08-01 00:37:18 +07:00
Kim Gardner d606f4d094 Merge branch 'master' into new-queue 2017-07-31 12:55:53 -04:00
Belen Curcio 344d1840f0 lingin 2017-07-31 13:41:13 -03:00
Belen Curcio 4079a74ab8 Updated componentWillReceiveProps 2017-07-31 13:18:54 -03:00
Kiwi ba1dc3446b Merge branch 'master' into comment-context-label 2017-07-31 17:57:17 +02:00
Chi Vinh Le 3954b7a268 Label for inactive comments 2017-07-31 22:53:35 +07:00
Belen Curcio 31112b3a20 Passing to the container 2017-07-31 12:36:58 -03:00
Kim Gardner 337e6cc963 Merge branch 'master' into comment-stream-tag-uniformity 2017-07-31 11:01:00 -04:00
Kim Gardner 665bf5b102 Merge pull request #817 from coralproject/docs-update
Docs Updates
2017-07-31 11:00:39 -04:00
Samantha Hankins 70dcf2162f create uniformity on tag label styling 2017-07-31 10:52:26 -04:00
Belen Curcio 84391d9d69 Routes taking :id 2017-07-31 10:45:18 -03:00
Belen Curcio dbc8f87683 IndexRoute 2017-07-31 10:23:34 -03:00
Chi Vinh Le 97c75be604 Comments explaining why we have inactive comments in the stream 2017-07-31 20:17:06 +07:00
Belen Curcio 7f85b842f0 Adding fallback for separate assets 2017-07-31 10:09:21 -03:00
Chi Vinh Le fc3acecf61 Refactor isCommentActive logic 2017-07-31 20:09:16 +07:00
Belen Curcio 371348ced5 changes 2017-07-31 10:03:58 -03:00
Belen Curcio 33a75aa4d0 Moving flag to the bottom 2017-07-31 09:46:51 -03:00
Belen Curcio 1b017daf62 changes 2017-07-31 09:42:35 -03:00
Belen Curcio 4f34827ec8 accepted to approved 2017-07-31 09:38:39 -03:00
Belen Curcio c9df0e5e40 Changes 2017-07-31 09:36:00 -03:00
Belen Curcio ea17c068dd Remove Index and Redirect 2017-07-31 09:33:29 -03:00
Wyatt Johnson 08edddf0df adds support for babel-plugin-transform-runtime 2017-07-31 15:51:34 +10:00
Wyatt Johnson 12e48efa9f some docs updates 2017-07-31 14:11:40 +10:00
Wyatt Johnson 2217c89aa7 Merge pull request #815 from coralproject/embed-control-object
Embed Control
2017-07-31 11:27:20 +10:00
Wyatt Johnson b8eacf43a6 Merge branch 'master' into embed-control-object 2017-07-31 10:25:43 +10:00
Wyatt Johnson c1660030cd Merge pull request #816 from coralproject/router-fix
Added more router fixes
2017-07-31 10:25:30 +10:00
Wyatt Johnson 299bb35c9d added more path fixes 2017-07-31 09:52:46 +10:00
Belen Curcio 1b583fb945 Reported has priority over premod queue or new queue 2017-07-28 15:51:21 -03:00
Belen Curcio 9bfdfc86b9 Refactor menu too 2017-07-28 15:42:59 -03:00
Belen Curcio 3e6e991609 Merge branch 'new-queue' of github.com:coralproject/talk into new-queue 2017-07-28 15:32:39 -03:00
Belen Curcio 7ada3106d2 Default routing working 2017-07-28 15:23:40 -03:00
Kim Gardner 2af0f2bf65 Merge branch 'master' into new-queue 2017-07-28 14:03:05 -04:00
Kiwi b9583dd55d Merge branch 'master' into embed-control-object 2017-07-28 19:31:31 +02:00
Chi Vinh Le 85e478cf3f Return an object on Talk.render to control the embed 2017-07-29 00:17:23 +07:00
Kim Gardner 993688837a Merge pull request #814 from coralproject/optimize-permalink-queries
Optimize query during permalink view
2017-07-28 12:28:32 -04:00
Belén Curcio 8aa086af02 Merge branch 'master' into optimize-permalink-queries 2017-07-28 13:21:59 -03:00
Kim Gardner f5369802e6 Merge branch 'master' into new-queue 2017-07-28 11:39:12 -04:00
Kim Gardner 7f38c70e58 Merge pull request #812 from coralproject/view-context-non-public
Allow admins / mods to view context of rejected and premod comments
2017-07-28 11:04:30 -04:00
Kim Gardner 7dd20d740d Merge branch 'master' into view-context-non-public 2017-07-28 10:58:54 -04:00
Kim Gardner 50c8d3f314 Merge pull request #811 from coralproject/user-detail-everywhere
User detail everywhere
2017-07-28 10:56:59 -04:00
Belen Curcio 0f41165751 New Queue names 2017-07-28 11:16:49 -03:00
Belen Curcio 5ce9c950b8 Query search goes to the specific queue 2017-07-28 10:44:59 -03:00
Chi Vinh Le 91bc7eaa53 Make condition more clear 2017-07-28 20:10:03 +07:00
Belén Curcio 58a6333e02 Merge branch 'master' into user-detail-everywhere 2017-07-28 09:57:34 -03:00
Belen Curcio 4b0eeca08b Merge branch 'new-queue' of github.com:coralproject/talk into new-queue 2017-07-28 09:54:55 -03:00
Belen Curcio 0111e08bc1 lintin 2017-07-28 09:48:34 -03:00
Belén Curcio 0767c680fc Merge branch 'master' into new-queue 2017-07-28 09:44:29 -03:00
Chi Vinh Le 7323c92b26 Optimize query during permalink view 2017-07-28 19:43:33 +07:00
Belen Curcio c0ff056354 Old file 2017-07-28 09:39:12 -03:00
Chi Vinh Le 1f3d97fd6b Allow admins / mods to view context of rejected or premod comments 2017-07-28 18:03:35 +07:00
Wyatt Johnson 4ed68259c6 Merge pull request #807 from coralproject/basepath
Added support for hosting Talk under a different path
2017-07-28 08:44:29 +10:00
Kiwi d5b7fd0413 Merge branch 'master' into user-detail-everywhere 2017-07-27 22:12:52 +02:00
Chi Vinh Le 389ff0ca0d Change reducer property names 2017-07-28 03:03:19 +07:00
Chi Vinh Le 7a9aa331c5 viewUserDetail everywhere 2017-07-28 02:50:32 +07:00
Kim Gardner 673cb0ed04 Merge branch 'master' into basepath 2017-07-27 15:21:31 -04:00
Kim Gardner 3f8b2b947c Merge pull request #806 from coralproject/docs-update
Docs update
2017-07-27 15:09:15 -04:00
Belen Curcio dd8167a204 Check premod by asset 2017-07-27 14:42:36 -03:00
Belen Curcio fba4b23dd0 Reordering Menu 2017-07-27 14:20:15 -03:00
Belen Curcio 200d41f2a5 New Queue 2017-07-27 14:15:49 -03:00
Kim Gardner c9442c7412 Merge branch 'master' into docs-update 2017-07-27 13:02:50 -04:00
Kim Gardner 1994e44e94 Merge pull request #810 from coralproject/fix-viewing-options-plugin
Fix viewing options plugin
2017-07-27 13:02:25 -04:00
Belen Curcio 3c960db93d Merge branch 'master' of github.com:coralproject/talk into new-queue 2017-07-27 14:01:53 -03:00
Belen Curcio 47d37e31d8 prefixing translations 2017-07-27 13:50:14 -03:00
Belen Curcio 447c3aa7cc Wrong reducer name 2017-07-27 13:48:03 -03:00
Belen Curcio b3d1ff8003 Renaming flag by reported 2017-07-27 13:43:40 -03:00
Chi Vinh Le ce2f8f97e8 Move userDetail out of Moderation route 2017-07-27 23:23:40 +07:00
Kim Gardner cf8528dcf1 Remove file 2017-07-27 12:23:18 -04:00
Kim Gardner 9bd3e6b541 Fix translation file 2017-07-27 12:20:07 -04:00
Kim Gardner f8758bd755 Fix folders 2017-07-27 11:58:36 -04:00
Kim Gardner 46a2699ecb Merge branch 'master' into docs-update 2017-07-27 11:05:06 -04:00
Kim Gardner ce0479ae5f Merge pull request #779 from coralproject/renaming-talk
Renaming Talk Plugins
2017-07-27 11:04:31 -04:00
Chi Vinh Le 57fb77008a Make UserDetail work independently 2017-07-27 21:44:29 +07:00
Kim Gardner 433c4fcd03 Merge branch 'master' into renaming-talk 2017-07-27 10:06:17 -04:00
Kim Gardner 0f84586e1e Merge pull request #805 from coralproject/redis-fixes
Fixed bug with Kue
2017-07-27 10:05:55 -04:00
Wyatt Johnson f4b765ccc5 template updates 2017-07-27 16:50:49 +10:00
Wyatt Johnson 0d7e4d6541 Added support for hosting Talk under a different path 2017-07-27 16:37:26 +10:00
Wyatt Johnson 30b3e91fdb documented more env vars 2017-07-27 15:34:04 +10:00
Wyatt Johnson a54a240ae6 fixed bug with job init 2017-07-27 15:14:11 +10:00
Wyatt Johnson b482cf0a0f updated styles 2017-07-27 12:03:25 +10:00
Kim Gardner 3a1a5e4e37 Fix merge conflicts 2017-07-26 21:49:50 -04:00
Wyatt Johnson 1a1d28c915 Merge pull request #788 from coralproject/docs-update
Updated docs, theme, jekyll
2017-07-27 11:33:56 +10:00
Kim Gardner 1359dfdc0b Merge branch 'master' into renaming-talk 2017-07-26 21:29:19 -04:00
Kim Gardner e7bc3002ef Add translations file for viewing options plugin 2017-07-26 21:21:42 -04:00
Wyatt Johnson 7eee40368a Merge branch 'master' into docs-update 2017-07-27 11:21:40 +10:00
Wyatt Johnson 8d59678c8c Merge pull request #802 from coralproject/no-yarn-generate-introspection
Don't use `yarn generate-introspection`
2017-07-27 11:21:17 +10:00
Kim Gardner 7058e7e7e8 Merge branch 'master' into renaming-talk 2017-07-26 21:16:18 -04:00
Wyatt Johnson 0d7acbd44d Merge branch 'master' into no-yarn-generate-introspection 2017-07-27 11:12:22 +10:00
Wyatt Johnson 9c425c371d Merge pull request #801 from coralproject/jwt
Secret Rotation + Improvements
2017-07-27 11:10:19 +10:00
Wyatt Johnson 8cee74ae06 Merge branch 'master' into jwt 2017-07-27 11:05:26 +10:00
Wyatt Johnson ea16d50516 docs update for jwt 2017-07-27 11:04:47 +10:00
Kim Gardner 98a638cb4d Merge branch 'master' into no-yarn-generate-introspection 2017-07-26 21:02:28 -04:00
Wyatt Johnson 59a1435089 Updated config 2017-07-27 11:01:31 +10:00
Wyatt Johnson 4733233cae Added new cookie config params 2017-07-27 10:58:12 +10:00
Wyatt Johnson 9b37767674 refactored secret parsing 2017-07-27 10:49:33 +10:00
Kim Gardner 22b3a59351 Merge branch 'master' into renaming-talk 2017-07-26 15:17:19 -04:00
Kim Gardner fdb3a27616 Merge pull request #804 from coralproject/offtopic-style-change
changes to off-topic badge colors
2017-07-26 15:07:06 -04:00
Samantha Hankins 67f1ee1e39 changes to off-topic badge colors 2017-07-26 15:00:48 -04:00
Kim Gardner 13434556fd Merge pull request #798 from coralproject/live-reactions-no-comment
Live Reactions should not always expect the comment to be there.
2017-07-26 14:52:08 -04:00
Belén Curcio 2713254358 Merge branch 'master' into live-reactions-no-comment 2017-07-26 15:22:20 -03:00
Belén Curcio 605cefaeac Merge pull request #799 from coralproject/prefix-redux-actions-plugins
Prefix redux constants in plugins
2017-07-26 15:21:35 -03:00
Chi Vinh Le b00435bfe3 Click on flagged user shows correct drawer 2017-07-26 18:43:10 +07:00
Chi Vinh Le b1b4e9fc57 Don't use yarn generate-introspection 2017-07-26 16:34:07 +07:00
Kiwi cd3adb93dd Merge branch 'master' into prefix-redux-actions-plugins 2017-07-26 11:15:44 +02:00
Kiwi 0ea3422126 Merge branch 'master' into live-reactions-no-comment 2017-07-26 11:15:34 +02:00
Wyatt Johnson e848df0976 docs update for jwt + move code of conduct 2017-07-26 18:09:04 +10:00
Wyatt Johnson 6a12ab28db fixes for tests 2017-07-26 15:58:52 +10:00
Wyatt Johnson a91624bf4b Expanded JWT Capabilities 2017-07-26 15:24:16 +10:00
Wyatt Johnson 2dbcb0fe45 Updates to docs, fix bug with jwt 2017-07-26 11:23:09 +10:00
Wyatt Johnson 557bda6444 Merge branch 'master' into docs-update 2017-07-26 08:55:32 +10:00
Kim Gardner f22350c92e Merge pull request #794 from coralproject/fix-vc
Mobile and Touch Events for Featured Tags/ Viewing Options
2017-07-25 16:45:56 -04:00
Kim Gardner 772e68f72e Merge branch 'master' into fix-vc 2017-07-25 14:26:04 -04:00
Kim Gardner 49943ddac0 Merge pull request #785 from coralproject/event-emitter
Implement Events
2017-07-25 14:11:18 -04:00
Kim Gardner 11a735f82e Merge branch 'master' into event-emitter 2017-07-25 14:04:22 -04:00
Kim Gardner c7515ff126 Merge pull request #800 from coralproject/fix-docker-deploy
Exclude generateIntrospectionResult from dockerignore
2017-07-25 13:55:50 -04:00
Chi Vinh Le 21d19d170d Exclude generateIntrospectionResult from dockerignore 2017-07-26 00:49:16 +07:00
Belen Curcio 608ad26e88 deleting unused utils 2017-07-25 14:18:40 -03:00
Belen Curcio 17e6064b9e Adding react touch events 2017-07-25 14:15:52 -03:00
Chi Vinh Le 8ea8bb521a Merge branch 'event-emitter' of ssh://github.com/coralproject/talk into event-emitter 2017-07-26 00:06:20 +07:00
Belen Curcio 9aa5c1ec46 Viewing Options for mobile 2017-07-25 14:04:13 -03:00
Belen Curcio 432afd2d8a Merge branch 'fix-vc' of github.com:coralproject/talk into fix-vc 2017-07-25 14:03:53 -03:00
Belen Curcio 48dadd8571 Viewing Options for mobile 2017-07-25 14:00:30 -03:00
Chi Vinh Le 1473c778a5 Refactor listener 2017-07-25 23:26:23 +07:00
Chi Vinh Le b8c5e56404 Rename action names to match the new constant names :-D 2017-07-25 23:04:32 +07:00
Belén Curcio a7adf3469d Merge branch 'master' into event-emitter 2017-07-25 13:04:08 -03:00
Belen Curcio 5e9791af9c Merge branch 'master' of github.com:coralproject/talk into fix-vc 2017-07-25 12:57:20 -03:00
Chi Vinh Le 84a20042f2 Prefix redux constants in plugins 2017-07-25 22:53:26 +07:00
Chi Vinh Le 8ecf55610c More fetchMore events and more data for graphql events 2017-07-25 22:28:28 +07:00
Kim Gardner 589f5129c9 Merge branch 'master' into renaming-talk 2017-07-25 10:25:00 -04:00
Chi Vinh Le d4bf5fd423 Add subscription data events 2017-07-25 21:20:04 +07:00
Chi Vinh Le ceb83834a7 Remove subscribeToMore Event because it's incomplete 2017-07-25 21:19:42 +07:00
Kim Gardner fefbec032c Merge pull request #790 from coralproject/commentsid
Search Comment in an Asset
2017-07-25 10:19:33 -04:00
Kim Gardner 788a35d9bb Merge branch 'master' into commentsid 2017-07-25 10:12:23 -04:00
Kim Gardner fc1b69681e Merge pull request #772 from coralproject/next
Generate and integrate introspection query results
2017-07-25 10:12:12 -04:00
Kim Gardner 1077bfbec2 Merge branch 'master' into next 2017-07-25 10:07:13 -04:00
Kim Gardner 7ab171a813 Merge branch 'master' into renaming-talk 2017-07-25 10:07:03 -04:00
Kim Gardner 9b5bbe9971 Merge pull request #791 from coralproject/bump-version-3-0-0
Update version to 3.0.0
2017-07-25 10:06:41 -04:00
Kim Gardner 00597378d6 Merge branch 'master' into bump-version-3-0-0 2017-07-25 10:01:14 -04:00
Kim Gardner 3bb0370a5a Merge pull request #797 from coralproject/jwt-alg
JWT Changes
2017-07-25 10:01:00 -04:00
Chi Vinh Le 3c2229f3a7 Remove redundant networkStatus 2017-07-25 20:48:26 +07:00
Chi Vinh Le fe9d0eb8e2 Merge branch 'event-emitter' of ssh://github.com/coralproject/talk into event-emitter 2017-07-25 20:45:03 +07:00
Chi Vinh Le 45a2e2a8a3 Activate wildcard matching 2017-07-25 20:40:51 +07:00
Chi Vinh Le 039b620ecf Live Reactions should not always expect the comment to be there. 2017-07-25 19:26:44 +07:00
Chi Vinh Le d9049ab602 Merge branch 'commentsid' of ssh://github.com/coralproject/talk into commentsid 2017-07-25 19:16:01 +07:00
Chi Vinh Le ef759e068b Adapt query helpers 2017-07-25 19:14:03 +07:00
Chi Vinh Le f10d1a9f0d Adapt auto scroll when loading a permalinked comment 2017-07-25 19:11:11 +07:00
Kiwi 374b99ddc2 Merge branch 'master' into commentsid 2017-07-25 12:55:24 +02:00
Kiwi f60e580f14 Merge branch 'master' into next 2017-07-25 12:17:50 +02:00
Chi Vinh Le 0c05da9285 Add comment 2017-07-25 17:16:59 +07:00
Chi Vinh Le b4244b8671 More integration with the introspection query result
- Auto generate introspection on any change when using  `dev-start`
- Port client to use introspection matcher
2017-07-25 16:51:41 +07:00
Wyatt Johnson 4d6271d255 Added log about result 2017-07-25 17:31:28 +10:00
Wyatt Johnson 2e74e2fe62 Added introspection results to build pipeline 2017-07-25 17:29:51 +10:00
Wyatt Johnson 3f304823ca Added new config for jwt alg + added query parsing of access token 2017-07-25 15:55:40 +10:00
Wyatt Johnson b92516ec40 Merge branch 'master' into docs-update 2017-07-25 12:59:08 +10:00
Wyatt Johnson 0c8def722b Refined task singleton to factory 2017-07-25 12:53:52 +10:00
Belén Curcio 34e484abf3 Merge pull request #795 from coralproject/plugin-seed-fix
Fix for the plugin seed builder
2017-07-24 23:48:43 -03:00
Wyatt Johnson 07114aed1b removed unused var 2017-07-25 12:40:54 +10:00
Wyatt Johnson f13e6b0e16 Fixes to impl 2017-07-25 12:38:49 +10:00
Wyatt Johnson c2f6b9aa09 Suggestions for refactoring 2017-07-25 12:32:15 +10:00
Belen Curcio fac63099c7 Using path.resolve instead 2017-07-24 23:26:18 -03:00
Belen Curcio 3a7353e4fe fix for plugin seed builder 2017-07-24 23:15:30 -03:00
Belén Curcio 148a87033a Merge branch 'master' into fix-vc 2017-07-24 23:09:05 -03:00
Wyatt Johnson aeeafb6284 Merge branch 'master' into next 2017-07-25 11:52:53 +10:00
Wyatt Johnson c4d79dd215 Merge branch 'master' into event-emitter 2017-07-25 11:52:42 +10:00
Wyatt Johnson 79be92a8bd Merge branch 'master' into renaming-talk 2017-07-25 11:52:34 +10:00
Wyatt Johnson ed3b26b100 Merge branch 'master' into docs-update 2017-07-25 11:52:24 +10:00
Wyatt Johnson c3bb399782 Merge branch 'master' into commentsid 2017-07-25 11:52:17 +10:00
Wyatt Johnson 2af0a2309e Merge branch 'master' into bump-version-3-0-0 2017-07-25 11:52:12 +10:00
Andrew Losowsky ef5e70f179 Merge pull request #793 from coralproject/remove-cache
Removed yarn cache
2017-07-24 21:51:49 -04:00
Wyatt Johnson b8bd997fed removed yarn cache 2017-07-25 11:39:36 +10:00
Kim Gardner 6013b4b136 Merge branch 'master' into renaming-talk 2017-07-24 21:22:10 -04:00
Kim Gardner 712c7e2ad2 Merge branch 'master' into commentsid 2017-07-24 21:22:03 -04:00
Kim Gardner 6c43a8accd Merge branch 'master' into bump-version-3-0-0 2017-07-24 21:21:54 -04:00
Wyatt Johnson 4b5942dab8 Merge branch 'master' into docs-update 2017-07-25 11:21:27 +10:00
Wyatt Johnson 9e1d22b988 Merge pull request #792 from coralproject/version-bump
Bumped node version, replaced bcrypt -> bcryptjs
2017-07-25 11:20:56 +10:00
Wyatt Johnson 9ffe542234 added some forcing logic to circle 2017-07-25 10:51:29 +10:00
Wyatt Johnson e292475f1e Added yarn global add node-gyp back 2017-07-25 10:20:54 +10:00
Wyatt Johnson c8f3be4bf5 fixed docker version number 2017-07-25 10:03:50 +10:00
Wyatt Johnson 3e09d28308 Bumped node version, replaced bcrypt -> bcryptjs 2017-07-25 10:01:05 +10:00
Belen Curcio 85a10ba693 Touch events for mobile :) 2017-07-24 18:49:51 -03:00
Kim Gardner be46ac826b Update version to 3.0.0 2017-07-24 16:51:35 -04:00
Belen Curcio a036a051cb Using async await 2017-07-24 16:40:08 -03:00
Belen Curcio 36df7cb784 Extended Asset with comment 2017-07-24 16:31:24 -03:00
Belen Curcio 4b74c3312f typo 2017-07-24 15:36:00 -03:00
Chi Vinh Le c68cca0e5b Change some remaining coral-plugin-* strings 2017-07-24 22:45:20 +07:00
Wyatt Johnson 2ef219cc7c fixed link 2017-07-24 19:44:32 +10:00
Wyatt Johnson f35cce866a Updated docs, theme, jekyll 2017-07-24 19:41:51 +10:00
Kim Gardner 6383abd445 Merge branch 'master' into renaming-talk 2017-07-23 21:03:38 -04:00
Wyatt Johnson 7e917c9611 Merge branch 'master' into next 2017-07-23 18:35:53 -06:00
Kim Gardner 661354998f Merge branch 'master' into event-emitter 2017-07-22 18:54:35 -04:00
Kim Gardner db183124c9 Merge pull request #787 from coralproject/long-title
Fix for long titles in the All Streams Queue
2017-07-21 21:18:44 -04:00
Kim Gardner ee220ca936 Merge branch 'master' into long-title 2017-07-21 21:13:24 -04:00
Kim Gardner 907e9bf226 Merge pull request #786 from coralproject/featured-comments-info
Featured Comments Tooltips
2017-07-21 15:18:15 -04:00
Belen Curcio 25bccde40f fix 2017-07-21 16:08:18 -03:00
Belen Curcio 6343a0d621 Adding tooltip to badges too 2017-07-21 16:01:43 -03:00
Chi Vinh Le 1db61941ed Comment WithEmit 2017-07-22 00:10:45 +07:00
Chi Vinh Le 7b1c0dfbaa Docs & stuff 2017-07-22 00:05:36 +07:00
Chi Vinh Le 6ea7e9d564 Use correct name 2017-07-21 23:58:41 +07:00
Chi Vinh Le 92d1fc5759 Comment out events listener 2017-07-21 23:55:15 +07:00
Chi Vinh Le 8b582e85f8 No redux prefix 2017-07-21 23:42:10 +07:00
Chi Vinh Le 970fe6fe04 Provide event emitter on admin side 2017-07-21 23:39:28 +07:00
Chi Vinh Le f5d4b7216e Merge branch 'master' into event-emitter 2017-07-21 22:57:51 +07:00
Chi Vinh Le cb95e9a308 Implement support for events 2017-07-21 22:54:45 +07:00
Kim Gardner 537311704d Merge branch 'master' into renaming-talk 2017-07-21 10:33:13 -04:00
Chi Vinh Le 0f82fa8bc5 Get store from context 2017-07-21 20:36:07 +07:00
Kim Gardner 6e6caaf5a9 Merge pull request #782 from coralproject/post-comment-no-alias
Remove remaining username alias in post comment mutation
2017-07-21 09:35:58 -04:00
Kim Gardner 8261c40c8d Merge branch 'master' into post-comment-no-alias 2017-07-21 09:31:01 -04:00
Kim Gardner 316b0a8808 Merge pull request #781 from coralproject/featured-tag-on-replies
Show featured tag on replies too
2017-07-21 09:30:45 -04:00
Kim Gardner ebce930b8c Merge branch 'master' into featured-tag-on-replies 2017-07-21 09:19:21 -04:00
Kim Gardner 8c8a2fcee0 Merge pull request #780 from coralproject/incosistent-state-load-more
Quick fix inconsistent state
2017-07-21 09:18:46 -04:00
Kim Gardner be63ee86b6 Merge branch 'master' into renaming-talk 2017-07-21 09:17:22 -04:00
Kim Gardner 10a82f277d Merge branch 'master' into next 2017-07-21 09:17:11 -04:00
Kim Gardner 1733ab8690 Merge branch 'master' into post-comment-no-alias 2017-07-21 09:16:54 -04:00
Kim Gardner 9624a12e39 Merge branch 'master' into featured-tag-on-replies 2017-07-21 09:15:31 -04:00
Kim Gardner 143050cbd5 Merge branch 'master' into incosistent-state-load-more 2017-07-21 09:14:19 -04:00
Kim Gardner 0fd7d9f020 Merge pull request #784 from coralproject/featured-comments-info
Featured Comments Info Tooltip
2017-07-21 09:14:00 -04:00
Belén Curcio 7ac3a325f7 Merge branch 'master' into featured-comments-info 2017-07-21 05:43:05 -03:00
Belen Curcio 149389057d Translations and style 2017-07-21 05:34:28 -03:00
Belen Curcio 3b01218338 Adding translations 2017-07-21 05:34:04 -03:00
Belen Curcio 2d8e36e511 Removing innecesary connects, and classes 2017-07-21 05:27:34 -03:00
Belen Curcio 9c9f088ac3 Containers, reducer and constants 2017-07-21 05:24:45 -03:00
Belen Curcio 392d82e30a Info Icon, and Tooltip 2017-07-21 05:24:27 -03:00
Belen Curcio f86d210bbb Adding actions 2017-07-21 05:23:54 -03:00
Belen Curcio 11ea45ef69 typo with white space 2017-07-21 05:21:13 -03:00
Belén Curcio b0cbddd313 Merge pull request #783 from coralproject/disable-fragment-warnings
Disable false-positive warnings
2017-07-20 19:32:57 -03:00
Chi Vinh Le 1f8f293594 Disable false-positive warnings 2017-07-21 04:17:47 +07:00
Chi Vinh Le 7ca7166c7a Add clarification 2017-07-21 04:08:43 +07:00
Chi Vinh Le 65500eda64 Remove remaining username alias in post comment mutation 2017-07-21 03:57:32 +07:00
Chi Vinh Le 0e37f36e02 Show featured tag on replies too 2017-07-21 02:36:04 +07:00
Chi Vinh Le abed842c23 Quick fix inconsistent state 2017-07-21 02:27:59 +07:00
Belen Curcio 1e9fb24f87 Renaming old reference plugin 2017-07-20 12:12:02 -03:00
Belen Curcio f17b4a318a Missing plugins 2017-07-20 12:04:00 -03:00
Belen Curcio 6ec33a0225 Plugins renaming 2017-07-20 11:52:36 -03:00
Belen Curcio eb3a9431f9 Renaming 2017-07-20 11:50:13 -03:00
Kim Gardner 8d7dc67544 Merge branch 'master' into next 2017-07-20 10:46:36 -04:00
Kim Gardner e1d1549c93 Merge pull request #736 from coralproject/featured-comments
Featured Comments
2017-07-20 10:45:56 -04:00
Belen Curcio 8f820ace5b Merge branch 'master' of github.com:coralproject/talk 2017-07-20 11:45:54 -03:00
Belen Curcio 1069498323 Merge branch 'master' of github.com:coralproject/talk 2017-07-20 11:31:25 -03:00
Kim Gardner c34daacd8c Merge branch 'master' into featured-comments 2017-07-19 20:07:28 -04:00
Kim Gardner f7f5b42b71 Merge pull request #778 from coralproject/make-permalink-readonly
Make permalink readonly
2017-07-19 20:07:12 -04:00
Chi Vinh Le f8a3b84e94 Make permalink readonly 2017-07-20 04:04:25 +07:00
Chi Vinh Le 44e89bc96c Remove unused style 2017-07-20 04:01:39 +07:00
Chi Vinh Le 2342934f48 Reset star button css 2017-07-20 03:58:22 +07:00
Chi Vinh Le 5328374b73 Make goTo link accessible 2017-07-20 03:51:34 +07:00
Chi Vinh Le 6f64eac5de Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-20 03:45:01 +07:00
Chi Vinh Le 0b884a7664 Harmonize classnames 2017-07-20 03:44:25 +07:00
Kim Gardner 64602f1731 Merge branch 'master' into featured-comments 2017-07-19 16:42:37 -04:00
Kim Gardner 3bc26cad80 Merge pull request #776 from coralproject/bump-version-2-5-0
Bump version 2.5.0
2017-07-19 16:41:26 -04:00
Kim Gardner def49d42aa Merge branch 'master' into bump-version-2-5-0 2017-07-19 16:34:43 -04:00
Chi Vinh Le 3d4c296837 Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-20 03:31:44 +07:00
Chi Vinh Le 64571d3848 Subtract ignored comments correctly 2017-07-20 03:30:37 +07:00
Kim Gardner ad669b8399 Merge branch 'master' into featured-comments 2017-07-19 16:28:20 -04:00
Kim Gardner 1c8e009366 Merge pull request #777 from coralproject/perma-link-plugin-commentId
Replace deprecated commentId in talk-plugin-permalink
2017-07-19 16:27:59 -04:00
Chi Vinh Le 5d8528fdb5 Replace deprecated commentId 2017-07-20 03:16:05 +07:00
Kim Gardner 6cab2634bf Add space before Go to conversation text 2017-07-19 16:05:28 -04:00
Chi Vinh Le d6adff288c Fix typo 2017-07-20 02:51:09 +07:00
Chi Vinh Le ad4b03bfcd Merge branch 'master' into featured-comments 2017-07-20 02:39:29 +07:00
Kim Gardner 99b8b4e90b Bump version 2.5.0 2017-07-19 15:34:04 -04:00
Chi Vinh Le 3fa475009a Use getDefinition name from plugin-api 2017-07-20 02:32:17 +07:00
Kim Gardner 139802381f Merge pull request #775 from coralproject/recursively-resolve-fragments
Recursively resolve fragments
2017-07-19 15:29:06 -04:00
Chi Vinh Le fd0143da5d Use connect from framework 2017-07-20 02:17:31 +07:00
Chi Vinh Le baa4808364 Allow setting default stream tab 2017-07-20 02:16:19 +07:00
Kim Gardner 0a4000328b Merge branch 'master' into next 2017-07-19 15:15:02 -04:00
Chi Vinh Le 673f0cd232 Hide featured comments tab when empty 2017-07-20 02:07:40 +07:00
Kim Gardner fa02d44270 Merge branch 'master' into recursively-resolve-fragments 2017-07-19 14:32:02 -04:00
Chi Vinh Le 84edd8c6c2 Merge branch 'master' into featured-comments
Conflicts:
	client/coral-embed-stream/src/components/Stream.js
2017-07-20 01:30:22 +07:00
Chi Vinh Le da2bc69d19 Translation 2017-07-20 01:27:06 +07:00
Chi Vinh Le b769e95898 Implement pagination 2017-07-20 01:19:45 +07:00
Kim Gardner 2b7f46efc7 Merge pull request #769 from coralproject/excludeIf-api
Conditionally exclude a Plugin Component from a Slot
2017-07-19 13:03:49 -04:00
Chi Vinh Le 7005175fc0 Simplify file namings 2017-07-19 23:55:35 +07:00
Chi Vinh Le 8f64229dee Implement FeaturedComment container 2017-07-19 23:50:39 +07:00
Chi Vinh Le 47a5b84a74 Pass correct slot props 2017-07-19 23:37:35 +07:00
Chi Vinh Le cc1026233f Merge branch 'recursively-resolve-fragments' into featured-comments 2017-07-19 23:32:04 +07:00
Chi Vinh Le 6f1b0b55ec Merge branch 'master' into featured-comments 2017-07-19 23:31:54 +07:00
Chi Vinh Le dcdfbb691a Use lazy loading for slot fragments 2017-07-19 23:30:51 +07:00
Chi Vinh Le 55ff0bcf8c Recursively resolve fragments 2017-07-19 22:58:27 +07:00
Belén Curcio d61374993a Merge branch 'master' into excludeIf-api 2017-07-19 11:47:51 -03:00
Kim Gardner 980dc2515e Merge pull request #774 from coralproject/resolve-slot-fragments-lazy
Support lazily resolving slot fragments
2017-07-19 10:45:19 -04:00
Chi Vinh Le 4511b9ca57 Merge branch 'resolve-slot-fragments-lazy' into featured-comments
Conflicts:
	client/coral-embed-stream/src/containers/Stream.js
	client/coral-framework/utils/index.js
	plugin-api/beta/client/utils/index.js
2017-07-19 21:15:35 +07:00
Chi Vinh Le 52cd7bcce4 Expose getSlotFragmentSpread in plugin-api 2017-07-19 20:59:03 +07:00
Chi Vinh Le 5fee26da0e Document getSlotFragmentSpreads 2017-07-19 20:50:00 +07:00
Chi Vinh Le d99f6d8923 Support lazily resolving slot fragments 2017-07-19 20:44:22 +07:00
Chi Vinh Le f051ee1d58 Don't create callback in render 2017-07-19 00:48:33 +07:00
Chi Vinh Le cd1735e1d6 Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-19 00:34:12 +07:00
Chi Vinh Le 3b10aa3945 Implement 'Go to conversation' 2017-07-19 00:33:01 +07:00
Kim Gardner 3b0b6bd73c Merge branch 'master' into featured-comments 2017-07-18 12:03:12 -04:00
Chi Vinh Le 760d078705 Embed scroll to correct element 2017-07-18 23:00:12 +07:00
Chi Vinh Le 46473142ba Use generic version of addTag, removeTag in framework 2017-07-18 21:23:39 +07:00
Chi Vinh Le 8a619a219f Refactor isFeatured / isTagged 2017-07-18 21:10:59 +07:00
Kim Gardner 73d15f34d7 Merge branch 'master' into excludeIf-api 2017-07-18 10:10:20 -04:00
Chi Vinh Le cf56ca8263 Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-18 21:07:09 +07:00
Kim Gardner add1b67a61 Merge branch 'master' into next 2017-07-18 10:06:02 -04:00
Kim Gardner 1183550e14 Merge pull request #768 from coralproject/docs-microservices
Write microservice docs
2017-07-18 10:05:37 -04:00
Chi Vinh Le 6652368221 Don't export internals of embed stream in plugin-api (for now) 2017-07-18 21:04:30 +07:00
Kim Gardner 9cbcb902c5 Merge branch 'master' into next 2017-07-18 10:00:27 -04:00
Kim Gardner d488e168c4 Merge branch 'master' into featured-comments 2017-07-18 09:59:49 -04:00
Kim Gardner 9990142740 Merge branch 'master' into docs-microservices 2017-07-18 09:58:54 -04:00
Kim Gardner 33bd2f576a Merge branch 'master' into excludeIf-api 2017-07-18 09:58:44 -04:00
Chi Vinh Le 19929c97d8 Fix layout issues in comment 2017-07-18 20:17:01 +07:00
Chi Vinh Le 99fc67d0d9 Rename insertSorted to insertCommentSorted 2017-07-18 19:38:52 +07:00
Chi Vinh Le 636d1f4788 Use withFragments from plugin-api 2017-07-18 19:37:30 +07:00
Chi Vinh Le d45038ccc5 Adapt package.json 2017-07-18 19:37:24 +07:00
Chi Vinh Le 9746422b16 Remove unused actions 2017-07-18 18:31:35 +07:00
Chi Vinh Le 4ee727c86a Fix inconsistent query state 2017-07-18 18:27:58 +07:00
Chi Vinh Le ad09ce03a1 Prevent double execution of mutation 2017-07-18 17:09:48 +07:00
Chi Vinh Le afeeffc262 Show notification upon failure 2017-07-18 17:08:14 +07:00
Chi Vinh Le 043d5fdcec Handle ignore user 2017-07-18 16:39:06 +07:00
Chi Vinh Le 8c7c564a30 Allow comment slots to extend the asset fragment + harmonize slot props 2017-07-18 15:23:02 +07:00
Wyatt Johnson 13cac731b9 Merge pull request #773 from icco/patch-1
Fix email address env var in docs
2017-07-17 15:29:40 -06:00
Nat Welch ab87500303 Fix email address env var in docs 2017-07-17 17:02:46 -04:00
Andrew Losowsky 244d421eda Merge pull request #770 from coralproject/autoflag-suspect
Added new TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS option
2017-07-17 13:28:04 -07:00
Wyatt Johnson 745c579b82 Adjust redis to not start during webpack build
- Added new WEBPACK env var which is enabled during yarn build scripts
- Defered redis starting until listen is called
- Moved pubsub to a factory pattern init
- Async/Await'ed the routes
- Moved pubsub handle for routes into middleware
- Adjusted redis cache and job processors to have lazy connection
starting
- Disabled mongo from auto-connecting on require
- Adjusted package redis clients to act as factory singletons instead
2017-07-17 13:34:04 -06:00
Wyatt Johnson d5dfd89d94 review of docs 2017-07-17 10:54:54 -06:00
Wyatt Johnson d82735b2b5 Adjusted docs 2017-07-17 10:50:40 -06:00
Wyatt Johnson b091cf5221 Added new TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS option 2017-07-17 10:43:28 -06:00
Chi Vinh Le d90f87428c Remove changes in featured 2017-07-17 19:37:01 +07:00
Chi Vinh Le 41f5297d78 Deprecate isSlotEmpty in the plugins api 2017-07-17 19:32:48 +07:00
Chi Vinh Le 0e9d9067f9 Use plugin-api and port plugins to it 2017-07-17 19:28:02 +07:00
Chi Vinh Le d2e6a98d46 connect aware "excludeIf" using our own hoc 2017-07-17 18:06:51 +07:00
Chi Vinh Le 4dd03508ee Merge branch 'master' into excludeIf-api 2017-07-17 17:15:16 +07:00
Chi Vinh Le fb2662572a Implement excludeIf for the plugins API 2017-07-17 17:06:12 +07:00
David Erwin 0069f31244 Make it accurate 2017-07-14 16:07:47 -04:00
David Erwin b235b8334e Give some context 2017-07-14 16:06:47 -04:00
David Erwin acb7028471 Write microservice docs 2017-07-14 16:01:57 -04:00
Kim Gardner 9daaa6ffee Merge branch 'master' into featured-comments 2017-07-14 14:27:41 -04:00
David Erwin af3489db9c Merge pull request #767 from coralproject/update-node
Update Node version to 7.10.1
2017-07-14 14:27:08 -04:00
Kim Gardner 012fa3e4aa Merge branch 'master' into featured-comments 2017-07-14 14:13:53 -04:00
Belen Curcio a8467db7aa Handling cache and mutations 2017-07-14 14:57:34 -03:00
Kim Gardner 05e2302794 Update CircleCI node version 2017-07-14 13:31:35 -04:00
Kim Gardner c610c319de Downgrade to 7.10.1 due to dependency issues 2017-07-14 13:16:38 -04:00
Kim Gardner 04d330cf5b Update node version in CircleCI config 2017-07-14 12:41:15 -04:00
Kim Gardner 8cbb56af19 Update default node version for Talk 2017-07-14 12:40:39 -04:00
Kim Gardner d4cb07e448 Update node version in Dockerfile 2017-07-14 12:39:48 -04:00
Belen Curcio 71b4413f92 Using same slot 2017-07-14 13:39:35 -03:00
Kim Gardner 9455eea041 Merge pull request #766 from coralproject/add-code-of-conduct-1
Created Code of Conduct
2017-07-14 11:23:56 -04:00
Andrew Losowsky e8bbd7e4d7 Created Code of Conduct
Merged ours with the Contributor Covenant
2017-07-14 10:34:58 -04:00
Belen Curcio 94032b69fc Missing asset 2017-07-14 11:34:20 -03:00
Belen Curcio 1f199ad3dd Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-14 11:31:58 -03:00
Belen Curcio eb038b9d49 Adding a slot to push like into featured comments 😎 2017-07-13 17:59:09 -03:00
Belen Curcio b8d00ec9ce Adding go to conversation 2017-07-13 17:29:54 -03:00
Belen Curcio b5ce16a33b Styling 2017-07-13 16:56:40 -03:00
Belen Curcio f8a6987476 Styles 2017-07-13 16:36:41 -03:00
Kim Gardner 28e589de21 Merge pull request #765 from coralproject/remove-like-plugin-as-default
Remove "like" from default plugins
2017-07-13 14:32:19 -04:00
Belen Curcio 1b97811cb0 Querying by featured comments 2017-07-13 15:30:06 -03:00
Kim Gardner 31cfddf93c Remove "like" from default plugins 2017-07-13 14:20:26 -04:00
Kim Gardner 562f2350c1 Merge pull request #764 from coralproject/update-readme-recipes
Update README with info about Recipes
2017-07-13 13:10:42 -04:00
Kim Gardner ea6330bf94 Update README with info about Recipes 2017-07-13 13:01:47 -04:00
Kim Gardner 0be3dc2ceb Merge pull request #751 from coralproject/talk-plugin-avatar
Talk Plugin Avatar
2017-07-13 12:54:04 -04:00
Kim Gardner eb98b32d21 Merge branch 'master' into talk-plugin-avatar 2017-07-13 12:13:58 -04:00
Kim Gardner 127ee25d7f Merge pull request #763 from coralproject/docs-trouble
Add install troubleshooting doc
2017-07-13 12:13:40 -04:00
David Erwin ed4a3c3250 Merge branch 'master' into docs-trouble 2017-07-13 12:05:09 -04:00
David Erwin 7b53048a91 Add install troubleshooting doc 2017-07-13 12:04:18 -04:00
Chi Vinh Le 28e94d30b0 Only show seperator on top-level comments 2017-07-13 23:00:52 +07:00
Chi Vinh Le 1c2fec29a4 Merge branch 'master' into talk-plugin-avatar
Conflicts:
	client/coral-embed-stream/src/components/Comment.css
2017-07-13 22:58:35 +07:00
Kim Gardner 56cc4989fd Merge pull request #761 from coralproject/docs-metadata
Docs metadata
2017-07-13 11:53:58 -04:00
Belen Curcio d72df73d6f Implementing first draft of FC 2017-07-13 12:53:01 -03:00
Kim Gardner b32aebd6f1 Merge branch 'master' into docs-metadata 2017-07-13 11:47:40 -04:00
Kim Gardner 244a276e1a Merge pull request #760 from coralproject/styles-fixes-2
Remove some global css, fix some styling issues
2017-07-13 11:46:28 -04:00
Kim Gardner 245561bd52 Merge branch 'master' into talk-plugin-avatar 2017-07-13 11:42:59 -04:00
Kim Gardner d05a63bc2e Merge branch 'master' into docs-metadata 2017-07-13 11:42:53 -04:00
Kim Gardner 5d118c26d1 Merge branch 'master' into styles-fixes-2 2017-07-13 11:42:04 -04:00
Belén Curcio 5ab5b7ca70 Merge pull request #762 from coralproject/reply-error-fix
BUG: Console error when you reply to a comment
2017-07-13 12:41:30 -03:00
Belen Curcio 7ba11cdee9 Merge branch 'master' of github.com:coralproject/talk 2017-07-13 12:36:35 -03:00
Chi Vinh Le b79c990ee5 Only setState once 2017-07-13 22:34:53 +07:00
Belen Curcio 1541a618b5 Reordering divs 2017-07-13 12:29:06 -03:00
Belen Curcio 8d4c0c6721 WIP 2017-07-13 12:15:13 -03:00
Belen Curcio 195592b9ab Merge branch 'master' into featured-comments 2017-07-13 12:08:02 -03:00
Belen Curcio da46d27e5b Merge branch 'featured-comments' of github.com:coralproject/talk into featured-comments 2017-07-13 12:07:23 -03:00
Belen Curcio bfbe8d44aa Merg 2017-07-13 12:05:53 -03:00
Belen Curcio da6965122f Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-13 12:04:04 -03:00
David Erwin 30b200d734 Fix typos 2017-07-13 10:58:31 -04:00
David Erwin dc76d89c47 Fewer, better words 2017-07-13 10:49:38 -04:00
Belen Curcio 3d80df1db6 Support for empty slot 2017-07-13 11:37:54 -03:00
Chi Vinh Le 83832abd72 More fixes 2017-07-13 21:37:08 +07:00
David Erwin 47fd522f68 Add first draft 2017-07-13 10:25:36 -04:00
Chi Vinh Le 1280b16bcd Remove some global css, fix some styling issues 2017-07-13 20:47:30 +07:00
David Erwin 3d666111b0 Add metadata docs 2017-07-13 09:26:37 -04:00
David Erwin 919bdff40a Use less words, say same thing 2017-07-13 09:26:09 -04:00
Belen Curcio 518b7b5358 merge 2017-07-12 15:04:24 -03:00
Belen Curcio 8b9463f501 Merge branch 'master' into talk-plugin-avatar 2017-07-12 15:01:14 -03:00
Belen Curcio f5313c0aed Merge branch 'master' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 15:00:03 -03:00
Kim Gardner c59c09e1f4 Merge pull request #759 from coralproject/styles-quick-fix
Style glitches quick fix
2017-07-12 13:57:43 -04:00
Belen Curcio e98dcb6647 Merge branch 'master' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 14:53:14 -03:00
Belen Curcio 39db11569f Merge branch 'talk-plugin-avatar' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 14:52:18 -03:00
Belen Curcio c3ad47f6f6 Linting 2017-07-12 14:49:46 -03:00
Kiwi b9cb1a783f Merge branch 'master' into styles-quick-fix 2017-07-12 19:42:04 +02:00
Chi Vinh Le ea51fdf6cc Styles glitches quick fix 2017-07-13 00:39:55 +07:00
Kim Gardner 324aa6db32 Merge pull request #757 from coralproject/query-by-tags
Allow query comments and comment counts by tags
2017-07-12 13:35:00 -04:00
Belen Curcio 9065a64d13 Merge branch 'master' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 14:33:00 -03:00
Kim Gardner 9b36fcebe9 Merge branch 'master' into query-by-tags 2017-07-12 12:21:20 -04:00
Kim Gardner 5d9d01d87f Merge pull request #749 from coralproject/extendable-tab-menu
Extendable Stream Tab Panel
2017-07-12 12:20:22 -04:00
Chi Vinh Le 7f85129044 Merge branch 'master' into extendable-tab-menu
Conflicts:
	client/coral-embed-stream/src/components/Embed.js
	client/coral-embed-stream/style/default.css
	client/coral-framework/helpers/plugins.js
	client/coral-plugin-replies/ReplyButton.css
	client/coral-plugin-replies/ReplyButton.js
	plugins/coral-plugin-like/client/LikeButton.js
	plugins/coral-plugin-like/client/styles.css
	plugins/coral-plugin-love/client/LoveButton.js
	plugins/coral-plugin-love/client/styles.css
2017-07-12 22:38:05 +07:00
Chi Vinh Le fc31a2882d Add a comment 2017-07-12 21:05:29 +07:00
Chi Vinh Le 23cc3548d4 Query by tags 2017-07-12 20:55:54 +07:00
Belén Curcio 574f5b7cea Merge branch 'master' into talk-plugin-avatar 2017-07-12 09:12:41 -03:00
Belen Curcio b19e050399 Merge branch 'master' of github.com:coralproject/talk 2017-07-12 09:10:48 -03:00
David Erwin ef3c82fee1 Merge pull request #758 from coralproject/docs-architecture-cli
Create cli docs
2017-07-11 16:57:54 -04:00
Kim Gardner 32211c4e37 Merge branch 'master' into featured-comments 2017-07-11 16:33:34 -04:00
Kim Gardner 202e7a2b10 Merge branch 'master' into talk-plugin-avatar 2017-07-11 16:33:21 -04:00
Kim Gardner 2588f3946d Merge branch 'master' into docs-architecture-cli 2017-07-11 16:32:52 -04:00
Kim Gardner 1c2e850a07 Merge pull request #756 from coralproject/bump-version-2-4-0
Update version to 2.4.0
2017-07-11 16:23:16 -04:00
Kim Gardner 0e8ecb37f3 Merge branch 'master' into bump-version-2-4-0 2017-07-11 16:11:33 -04:00
David Erwin d1b96f5830 Merge branch 'docs-architecture-cli' of https://github.com/coralproject/talk into docs-architecture-cli 2017-07-11 15:06:07 -04:00
David Erwin a9c6014634 Add more info 2017-07-11 15:05:52 -04:00
Kim Gardner 125168c7c8 Merge branch 'master' into docs-architecture-cli 2017-07-11 14:58:41 -04:00
David Erwin eafcadd6a1 Warn users more 2017-07-11 14:43:59 -04:00
David Erwin 211f24476a Create cli docs 2017-07-11 14:39:26 -04:00
David Erwin f55e4ef895 Merge pull request #753 from coralproject/adapt-embed-classnames
Adapt Class Names
2017-07-11 14:20:53 -04:00
Kim Gardner bbf1da88e6 Merge branch 'master' into adapt-embed-classnames 2017-07-11 13:51:18 -04:00
Kim Gardner d688f70c19 Merge pull request #755 from coralproject/docs-qf-register-link
Fix 404
2017-07-11 13:47:11 -04:00
Kim Gardner da7aa8f6d0 Merge branch 'master' into talk-plugin-avatar 2017-07-11 13:46:41 -04:00
Kim Gardner b04a477f6a Merge branch 'master' into adapt-embed-classnames 2017-07-11 13:44:51 -04:00
Kim Gardner 0546f741a5 Bump version to 2.4.0 2017-07-11 13:43:12 -04:00
David Erwin a535baeb31 Merge branch 'master' into docs-qf-register-link 2017-07-11 13:41:56 -04:00
David Erwin 8b125eab48 Fix 404 2017-07-11 13:36:45 -04:00
Kim Gardner 053b687959 Merge pull request #746 from coralproject/create-plugin
Create Plugins - Create plugins through the CLI
2017-07-11 13:33:57 -04:00
Kim Gardner b620ac703b Merge branch 'master' into adapt-embed-classnames 2017-07-11 12:11:33 -04:00
Kim Gardner 3cb0bb052c Merge branch 'master' into create-plugin 2017-07-11 12:06:19 -04:00
Kim Gardner c4985e0007 Merge branch 'master' into talk-plugin-avatar 2017-07-11 12:02:17 -04:00
David Erwin 066d9ce0ec Merge pull request #745 from coralproject/docs-features
Docs Architecture
2017-07-11 11:55:41 -04:00
David Erwin dc6fa74d22 Add more clarity 2017-07-11 11:51:02 -04:00
David Erwin e9e79b4605 Merge branch 'docs-features' of https://github.com/coralproject/talk into docs-features 2017-07-11 11:06:25 -04:00
David Erwin 1d9b627d73 Fix typos and clarify 2017-07-11 11:05:58 -04:00
Chi Vinh Le 40f4419c9d Add flagged unflagged CSS classnames 2017-07-11 21:54:35 +07:00
Chi Vinh Le 8b70dc688d Adapt CSS classnames for Embed 2017-07-11 21:03:27 +07:00
Chi Vinh Le b05a6c00ce Correct aria-controls 2017-07-11 20:37:19 +07:00
Kim Gardner f0bdee415b Merge branch 'master' into featured-comments 2017-07-11 08:18:31 -04:00
Kim Gardner 9063b8adf1 Merge branch 'master' into docs-features 2017-07-11 08:18:25 -04:00
Kim Gardner 01fd667eb4 Merge branch 'master' into talk-plugin-avatar 2017-07-11 08:17:49 -04:00
Kim Gardner 73ac8d16ef Merge branch 'master' into create-plugin 2017-07-11 08:13:59 -04:00
Kim Gardner ced449a148 Merge pull request #747 from coralproject/fix-permalink-live-update
Fix perma view live updates
2017-07-11 08:12:59 -04:00
Kim Gardner ffad851066 Merge branch 'master' into fix-permalink-live-update 2017-07-11 07:49:16 -04:00
Kim Gardner 4281fd590c Merge pull request #750 from coralproject/no-label-on-mobile
Remove labels for comment actions on mobile.
2017-07-11 07:46:11 -04:00
Belen Curcio 0c167fdfc4 Removing talk-plugin-avatar 2017-07-10 17:43:51 -03:00
Belen Curcio 5dfb372c42 Merge branch 'master' of github.com:coralproject/talk 2017-07-10 17:11:06 -03:00
Belen Curcio 932103c204 updated yarn lock 2017-07-10 17:08:49 -03:00
Belen Curcio 666225614b Merge branch 'talk-plugin-avatar' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-10 17:06:57 -03:00
Belen Curcio 636f357186 Reverting changes from old branch 2017-07-10 17:06:40 -03:00
Belén Curcio be6337f17c Merge branch 'master' into talk-plugin-avatar 2017-07-10 17:05:56 -03:00
Belen Curcio 2c7f5ca089 linting 2017-07-10 17:00:01 -03:00
Belen Curcio f09550a4ac Avatar plugin support extending fragments 2017-07-10 16:56:46 -03:00
Belen Curcio 3c3a0523de Avatar support 2017-07-10 16:27:15 -03:00
Chi Vinh Le a2dabcac0e Hide action labels on mobile 2017-07-11 01:26:25 +07:00
Wyatt Johnson a686bc75a7 Some changes to server side stuff + tempalte location 2017-07-10 10:30:20 -06:00
Chi Vinh Le 8960f67a4d Add todo comments 2017-07-10 22:49:06 +07:00
Chi Vinh Le e10c1f1a1f Docs 2017-07-10 22:45:28 +07:00
Chi Vinh Le 55e70dfbec Fix perma view live updates 2017-07-10 22:13:04 +07:00
Chi Vinh Le af46e3a7b1 Merge branch 'master' into extendable-tab-menu
Conflicts:
	client/coral-embed-stream/src/components/Comment.js
2017-07-10 22:03:32 +07:00
Chi Vinh Le 883dfedfab Fix new count styles 2017-07-10 21:57:25 +07:00
Chi Vinh Le ae52b27009 Don't use div inside p 2017-07-10 21:53:55 +07:00
Chi Vinh Le 34a4a005d1 Add margin to highlighted comment 2017-07-10 21:12:52 +07:00
Chi Vinh Le ab72995a2c Implement streamTabs and streamTabPanes slot 2017-07-10 20:46:28 +07:00
Chi Vinh Le aac47c4686 Add margin 2017-07-10 17:57:45 +07:00
Chi Vinh Le 362c072181 If only one tab left make it not clickable 2017-07-10 17:03:07 +07:00
Chi Vinh Le ef172ae5f8 Change streamBox to streamFilter 2017-07-10 17:03:07 +07:00
Chi Vinh Le 1e4e1cb413 Implement Stream TabBar 2017-07-10 17:03:06 +07:00
Belén Curcio e819b96473 Merge branch 'master' into create-plugin 2017-07-09 12:21:49 -03:00
Belen Curcio 1e3c1feadf cta 2017-07-09 12:09:54 -03:00
Belen Curcio 83d7e599c7 pushing client template 2017-07-09 12:09:07 -03:00
David Erwin 52d7ba8965 Add Tags docs 2017-07-07 16:59:18 -04:00
Belen Curcio c3ec772ea2 new cta for questions 2017-07-07 14:43:46 -03:00
David Erwin d2b4c2bcac Add architecture docs 2017-07-07 13:43:05 -04:00
Kim Gardner f738da4dd5 Merge pull request #744 from coralproject/redis-flush
Flush Redis before each test
2017-07-07 16:19:52 +01:00
Kim Gardner b796bf2607 Merge branch 'master' into redis-flush 2017-07-07 16:06:22 +01:00
Kim Gardner 3924ced659 Merge pull request #743 from coralproject/change-org-name
Added new cli-settings
2017-07-07 16:01:55 +01:00
Wyatt Johnson 23573501bd Configured redis to flush before each test and changed default test
redis database
2017-07-07 08:57:56 -06:00
Wyatt Johnson 787c00325d Added new cli-settings 2017-07-07 08:40:18 -06:00
Kim Gardner db371d1c82 Merge pull request #742 from coralproject/bump-version-2-3-0
Update version number
2017-07-07 15:25:36 +01:00
David Erwin b744a98a3d Merge branch 'master' into bump-version-2-3-0 2017-07-07 10:20:44 -04:00
Kim Gardner 37439a0896 Merge pull request #741 from coralproject/rm-commonjs-framework
Remove CommonJS style exports from framework
2017-07-07 15:19:57 +01:00
Kim Gardner af3cd8aa68 Update version number 2017-07-07 15:09:25 +01:00
Kiwi 7baa551a5c Merge branch 'master' into rm-commonjs-framework 2017-07-07 12:39:00 +02:00
Chi Vinh Le b7c4dbd224 Remove CommonJS style exports in framework 2017-07-07 17:27:05 +07:00
Belen Curcio aad2455654 Linting 2017-07-06 15:19:26 -03:00
Belen Curcio a8e5ad451d Plugins CLI :D 2017-07-06 15:08:39 -03:00
Belen Curcio 99425e89ca More questions 2017-07-06 13:09:43 -03:00
Belen Curcio 60608af8b2 Filter out the ones that have slots 2017-07-06 11:40:35 -03:00
Belen Curcio 7c753ee518 create-plugin 2017-07-05 16:41:52 -03:00
Belén Curcio 70d1d5e9de Merge branch 'master' into featured-comments 2017-07-05 12:42:07 -03:00
Belén Curcio b28902312d Merge pull request #740 from coralproject/threading-cleanup
Cleanup
2017-07-05 12:41:28 -03:00
Belen Curcio 28010488be Linting 2017-07-05 12:31:35 -03:00
Belen Curcio 2a9535f88a Featured Comment Styles 2017-07-05 12:28:09 -03:00
Belen Curcio f9bece468f Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-05 12:19:09 -03:00
Kiwi f32c03f002 Merge branch 'master' into threading-cleanup 2017-07-05 17:13:43 +02:00
Chi Vinh Le 693879647e Cleanup 2017-07-05 22:11:55 +07:00
Kim Gardner c01d1f95bc Merge pull request #735 from coralproject/french-translation
French translations
2017-07-05 15:59:36 +01:00
Belen Curcio 38e8bed68a Merge branch 'featured-comments' of github.com:coralproject/talk into featured-comments 2017-07-05 11:36:26 -03:00
Kim Gardner 65d6d3221b Merge branch 'master' into french-translation 2017-07-05 15:36:14 +01:00
Belen Curcio 3ca62cab65 wip 2017-07-05 11:36:03 -03:00
Kim Gardner 15af61de67 Merge pull request #738 from coralproject/threading
Deep Threading Support
2017-07-05 15:35:43 +01:00
Kim Gardner 1adca208dd Merge branch 'master' into french-translation 2017-07-05 15:26:39 +01:00
Kim Gardner 935982cb08 Merge branch 'master' into threading 2017-07-05 15:23:45 +01:00
Belen Curcio da6bd80de0 Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-05 11:07:42 -03:00
Kim Gardner 774e0cc12e Merge branch 'master' into featured-comments 2017-07-05 14:29:17 +01:00
Kim Gardner 4b9f3390d3 Merge pull request #739 from coralproject/bump-version-2-2-1
Update version 2.2.1
2017-07-05 11:57:27 +01:00
Kim Gardner f94c36e9c9 Update version 2.2.1 2017-07-05 11:43:18 +01:00
Kim Gardner c976ba0ea4 Merge branch 'master' into french-translation 2017-07-05 09:17:36 +01:00
Chi Vinh Le 6d71bcbee9 Fix styling issues 2017-07-04 22:56:58 +07:00
Chi Vinh Le 9be2a520db Merge branch 'master' into threading 2017-07-04 22:42:49 +07:00
Chi Vinh Le d155615102 Add pending comment 2017-07-04 22:41:38 +07:00
Kim Gardner 4d71a5a448 Merge pull request #737 from coralproject/fix-plugin-slot-logic
Fix isSlotEmpty logic and handle empty plugin_config
2017-07-04 16:35:05 +01:00
Chi Vinh Le cfa7a74742 Fix isSlotEmpty logic and handle empty plugin_config 2017-07-04 22:22:52 +07:00
Chi Vinh Le 524be1b869 Allow customizing thread levels via TALK_THREADING_LEVEL env 2017-07-04 22:10:40 +07:00
Chi Vinh Le 31ab54d13c First round styling 2017-07-04 21:32:33 +07:00
Belén Curcio dcfbcd287b Merge branch 'master' into featured-comments 2017-07-04 10:29:25 -03:00
David Erwin c5203e3460 Merge pull request #734 from coralproject/dont-show-popup-when-empty
Don't show popup when empty
2017-07-03 14:40:31 -04:00
David Erwin 79c1bd9bdd Merge branch 'master' into dont-show-popup-when-empty 2017-07-03 14:35:44 -04:00
Belen Curcio 4af9c3bee2 Merge branch 'featured-comments' of github.com:coralproject/talk into featured-comments 2017-07-03 15:32:48 -03:00
Belen Curcio ec559f0f23 Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-03 15:32:40 -03:00
Belén Curcio b902695cb7 Merge branch 'master' into featured-comments 2017-07-03 15:32:36 -03:00
David Erwin e2575cb346 Merge pull request #733 from coralproject/disable-plugin-components-via-config
Disable plugin components via plugin_config
2017-07-03 14:28:54 -04:00
Belen Curcio 34dbbb0984 Removing Best from the core 2017-07-03 15:27:08 -03:00
Chi Vinh Le 3a2a4379b8 Change to 4 levels 2017-07-04 01:09:27 +07:00
Chi Vinh Le f1899f2f4a Threading support 2017-07-04 01:09:12 +07:00
Belén Curcio 4d821afa6a Merge branch 'master' into disable-plugin-components-via-config 2017-07-03 15:08:36 -03:00
Belen Curcio 002dc377a6 Featured Comments container, and FeaturedComment component 2017-07-03 13:31:13 -03:00
Belen Curcio fbdb044551 Moving withTags to the plugin api, adding perms to Feature comments 2017-07-03 11:55:09 -03:00
Belen Curcio 23ebe9f8e3 Translations and functionality 2017-07-03 11:46:34 -03:00
David Erwin c16278221c Merge pull request #719 from coralproject/docs-contribution-guide
Rewrite contributor's guide
2017-07-03 10:24:28 -04:00
David Erwin a50b39091f ABC Always Be Clarifying 2017-07-03 10:13:09 -04:00
David Erwin 4c7506e4f5 Merge branch 'docs-contribution-guide' of https://github.com/coralproject/talk into docs-contribution-guide 2017-07-03 10:06:56 -04:00
David Erwin 2f06281439 Move contributor's guide 2017-07-03 10:03:51 -04:00
Belen Curcio e3aecd645e Ádding withTags hoc, and creating withTags buttons 2017-07-03 10:16:35 -03:00
Belen Curcio b8020d5347 Adding talk-plugins-featured-comments to the default json 2017-07-03 10:15:57 -03:00
Chi Vinh Le fd1f8b24c1 Don't show popup when empty 2017-07-03 19:40:26 +07:00
Belen Curcio 95e820e9de Creating Tag Featured 2017-07-03 09:25:18 -03:00
Chi Vinh Le 270c3b7634 Disable plugin components via plugin_config 2017-07-03 19:01:17 +07:00
Kim Gardner e4647cab98 Merge branch 'master' into docs-contribution-guide 2017-07-03 12:12:56 +01:00
Kim Gardner 60b4909395 Merge pull request #732 from coralproject/update-lockfile
Update yarn.lock
2017-06-30 17:53:57 +01:00
Kim Gardner 0973600e68 Update yarn.lock 2017-06-30 17:45:45 +01:00
Kim Gardner e3ead31964 Merge pull request #731 from coralproject/button-states
Support post, reply, edit, load and view more loading states via global css classnames
2017-06-30 17:32:47 +01:00
Chi Vinh Le 7858ccc45d Switch to prop-types 2017-06-30 23:25:11 +07:00
Chi Vinh Le c1807daeaf Correct spanish translation key 2017-06-30 21:10:19 +07:00
Chi Vinh Le e244d4a286 Merge branch 'master' into button-states
Conflicts:
	client/coral-embed-stream/src/components/Stream.js
2017-06-30 17:46:17 +07:00
Chi Vinh Le 9de99a9ef4 Support post, reply, edit, load and view more loading states via global css classnames 2017-06-30 17:00:42 +07:00
Belén Curcio 01883851ea Merge pull request #729 from coralproject/permalink-id
Permalink by Query Param instead of Hash
2017-06-29 14:05:07 -03:00
Belen Curcio c8d9447033 Adding the polifill to the package.json 2017-06-29 13:44:40 -03:00
Kim Gardner 4319bb9da3 Merge branch 'master' into docs-contribution-guide 2017-06-29 17:31:33 +01:00
Belen Curcio 9f8c8e9c83 Merge branch 'permalink-id' of github.com:coralproject/talk into permalink-id 2017-06-29 13:30:49 -03:00
Kim Gardner 997dffd326 Merge branch 'master' into permalink-id 2017-06-29 17:30:38 +01:00
Kim Gardner f6d652df0d Merge pull request #730 from coralproject/fix-build
Update dependencies to fix build
2017-06-29 17:30:20 +01:00
Belen Curcio c3ee442775 Replacing moderation comment and history hashes 2017-06-29 13:30:12 -03:00
Belen Curcio bad869bdfe Merge branch 'permalink-id' of github.com:coralproject/talk into permalink-id 2017-06-29 13:27:08 -03:00
Belen Curcio 2289c405d1 Translatios 2017-06-29 13:25:50 -03:00
Belen Curcio 354295c0ae Adding polyfill 2017-06-29 13:20:27 -03:00
Kim Gardner 737cea6e76 Reinstall modules 2017-06-29 16:59:00 +01:00
Kim Gardner 867f6966ba Update dependencies to fix build 2017-06-29 16:52:01 +01:00
Belén Curcio fbefd7fd20 Merge branch 'master' into permalink-id 2017-06-29 12:35:06 -03:00
Kim Gardner a0a7971866 Merge pull request #725 from StephanieDClark/add-tab-classes
Updates to tab classes
2017-06-29 16:22:52 +01:00
Kim Gardner 16355169ce Merge branch 'master' into add-tab-classes 2017-06-29 16:16:24 +01:00
Kim Gardner a7f4dea74a Merge branch 'master' into permalink-id 2017-06-29 15:58:23 +01:00
Kim Gardner 8aa0c16769 Merge pull request #728 from coralproject/bump-version-2-2-0
Bump version to 2.2.0
2017-06-29 15:57:20 +01:00
Belen Curcio d31044f39e linting 2017-06-29 11:25:52 -03:00
Belén Curcio a804973990 Merge branch 'master' into permalink-id 2017-06-29 11:22:28 -03:00
Belen Curcio db9c3b45c1 Stream Error 2017-06-29 11:06:52 -03:00
Belen Curcio ddac5c2c85 Avoid Error due to invalid queries 2017-06-29 10:48:29 -03:00
Belen Curcio 0312530b86 Modifying permalink plugin 2017-06-29 10:20:33 -03:00
Belen Curcio 87f8ed401f Using CommentId by queryParam 2017-06-29 10:18:40 -03:00
Kim Gardner 06ebd9ccc5 Bump version to 2.2.0 2017-06-29 12:15:00 +01:00
Kim Gardner e615624b62 Merge branch 'master' into add-tab-classes 2017-06-29 11:06:33 +01:00
Kim Gardner 9803ccfaf5 Merge pull request #724 from coralproject/permalink-patch
Permalink plugin now enabled by default
2017-06-29 10:35:11 +01:00
Wyatt Johnson ab84436a78 Update plugins.default.json 2017-06-28 15:32:53 -06:00
StephanieDClark ccaa744a78 Updates to tab classes 2017-06-28 14:26:41 -07:00
Belén Curcio d55fc6b601 Merge branch 'master' into docs-contribution-guide 2017-06-28 12:08:40 -03:00
Kim Gardner 32318567c9 Merge pull request #723 from coralproject/dont-alias-username-2
Don't alias username 2
2017-06-28 15:22:55 +01:00
Kiwi 6cf8c01d61 Merge branch 'master' into dont-alias-username-2 2017-06-28 21:17:41 +07:00
Chi Vinh Le b92afff4e7 Don't alias username 2 2017-06-28 21:14:34 +07:00
Kim Gardner 089ed0f09b Merge pull request #722 from coralproject/edit-name-fix
Allow editing to the same username in the "create username" flow
2017-06-28 14:43:17 +01:00
Belén Curcio 2258125789 Merge branch 'master' into edit-name-fix 2017-06-28 10:22:31 -03:00
Belén Curcio 8de4b475a1 Merge pull request #721 from coralproject/smooth-scroll
Smooth Scroll on Moderation
2017-06-28 10:21:56 -03:00
Belen Curcio 4691ee07de Merge branch 'smooth-scroll' of github.com:coralproject/talk into smooth-scroll 2017-06-28 09:07:19 -03:00
Belen Curcio f767b6a998 Removing old 2017-06-28 09:06:46 -03:00
Kiwi 9b26b0a434 Merge branch 'master' into edit-name-fix 2017-06-28 18:56:34 +07:00
Kim Gardner 7817804bae Merge branch 'master' into smooth-scroll 2017-06-28 12:51:43 +01:00
Kim Gardner b2738afdbe Merge pull request #718 from coralproject/no-username-alias
Don't alias username
2017-06-28 12:51:27 +01:00
Kim Gardner e0a32e5a70 Merge branch 'master' into smooth-scroll 2017-06-28 12:27:40 +01:00
Kiwi b7a981e84b Merge branch 'master' into no-username-alias 2017-06-28 16:37:01 +07:00
Kiwi 52431a4c4c Merge branch 'master' into edit-name-fix 2017-06-28 16:36:20 +07:00
Kim Gardner ca0431054b Merge pull request #720 from coralproject/flag-reason
BUG: Flag Username - Offensive, doesn't show up in the Community Admin
2017-06-28 10:33:52 +01:00
Kim Gardner 3f03994a51 Merge branch 'master' into flag-reason 2017-06-28 10:27:48 +01:00
Kim Gardner 198efd02b4 Merge branch 'master' into smooth-scroll 2017-06-28 10:27:00 +01:00
Kiwi aff486e909 Merge branch 'master' into no-username-alias 2017-06-28 16:22:12 +07:00
Kiwi 55e62b2b22 Merge branch 'master' into edit-name-fix 2017-06-28 16:20:01 +07:00
Kim Gardner 37e6c2c45c Merge pull request #717 from coralproject/live-suspension
Live update on stream when banned user, suspended user or rejected username
2017-06-28 10:16:18 +01:00
Chi Vinh Le 270491dc0f Allow editing to the same username in the "create username" flow 2017-06-28 16:08:46 +07:00
Belén Curcio 04b562543e Merge branch 'master' into flag-reason 2017-06-27 16:53:56 -03:00
Belen Curcio dd76ac1ed4 Now the smooth flag works 2017-06-27 16:51:00 -03:00
Belen Curcio e03e15fb3b renaming 2017-06-27 16:20:08 -03:00
Belen Curcio f13682c125 flagMap added to the admin side 2017-06-27 16:12:13 -03:00
David Erwin 3695ce5cb7 Update links 2017-06-27 15:08:21 -04:00
David Erwin d68169757d Rewrite contributor's guide 2017-06-27 15:04:00 -04:00
Belén Curcio 6d4765b45d Merge branch 'master' into no-username-alias 2017-06-27 16:03:03 -03:00
David Erwin b94824b8ee Merge branch 'master' into live-suspension 2017-06-27 14:14:21 -04:00
Belén Curcio cb1b9a1463 Merge pull request #714 from coralproject/unify-taglists
Refactor TagsInput and improve UX
2017-06-27 14:54:57 -03:00
Belen Curcio 35ccc5070d Flag refactor 2017-06-27 14:52:12 -03:00
Kiwi efb18cdb32 Merge branch 'master' into unify-taglists 2017-06-28 00:28:32 +07:00
Kiwi 2338ff1694 Merge branch 'master' into no-username-alias 2017-06-28 00:15:14 +07:00
Chi Vinh Le b134d4099c More concise and functional subscribing 2017-06-28 00:13:47 +07:00
Kim Gardner 1b40521f88 Merge branch 'master' into live-suspension 2017-06-27 17:53:58 +01:00
Kim Gardner 88473a1c71 Merge pull request #716 from coralproject/coral-plugin-permalink
Talk Plugin Permalink
2017-06-27 17:53:38 +01:00
Chi Vinh Le 97ab338486 Don't alias username 2017-06-27 23:51:49 +07:00
Kiwi 8d4e45c0ed Merge branch 'master' into coral-plugin-permalink 2017-06-27 23:46:55 +07:00
Kim Gardner 53b481dc68 Merge branch 'master' into unify-taglists 2017-06-27 16:25:00 +01:00
Chi Vinh Le b67ac375ba Pass current user to CommentBox 2017-06-27 22:14:06 +07:00
Chi Vinh Le 500d5d582e Fix edit 2017-06-27 21:51:19 +07:00
Kiwi 5b9fe5688f Merge branch 'master' into live-suspension 2017-06-27 21:44:26 +07:00
Chi Vinh Le 175dd30d27 Keep comment box when user is degraded (live) 2017-06-27 21:40:48 +07:00
David Erwin 33990053fb Merge pull request #709 from coralproject/docs-plugins-quickstart
Docs plugins quickstart
2017-06-27 10:19:53 -04:00
Kim Gardner 3f4747c2ec Fix typo 2017-06-27 14:11:04 +01:00
Chi Vinh Le db3187cb56 Merge branch 'master' into live-suspension 2017-06-27 20:08:53 +07:00
Chi Vinh Le c90c177b7c Live update on stream when banned user, suspended user or rejected username 2017-06-27 20:06:27 +07:00
Kim Gardner 933891b2e3 Merge branch 'master' into docs-plugins-quickstart 2017-06-27 13:41:16 +01:00
Kim Gardner 43dd72c715 Merge branch 'master' into unify-taglists 2017-06-27 13:40:24 +01:00
Kim Gardner a7f0de6e62 Merge branch 'master' into coral-plugin-permalink 2017-06-27 13:40:15 +01:00
Kim Gardner 53742934f9 Merge pull request #715 from coralproject/commentCount
Comment Count by Asset URL
2017-06-27 13:01:47 +01:00
Belen Curcio b8aed80f1c Merge branch 'coral-plugin-permalink' of github.com:coralproject/talk into coral-plugin-permalink 2017-06-26 17:35:07 -03:00
Belen Curcio 4e94727749 Merge branch 'master' of github.com:coralproject/talk into coral-plugin-permalink 2017-06-26 17:34:53 -03:00
Belén Curcio 8412fa5613 Merge branch 'master' into coral-plugin-permalink 2017-06-26 16:26:46 -03:00
Belen Curcio bee1cc124c renamed to talk 2017-06-26 16:26:05 -03:00
Belen Curcio 2ac6ad8eb8 Using the plugin api 2017-06-26 16:16:40 -03:00
Belen Curcio fa9a4ecc50 Clearing the timeout 2017-06-26 16:08:32 -03:00
Belen Curcio b00d2f569e Removing styles from default.css 2017-06-26 16:01:07 -03:00
Belen Curcio 5e1450ffb0 Renaming to talk 2017-06-26 15:46:53 -03:00
Belen Curcio a14e4859a9 refactor 2017-06-26 14:49:24 -03:00
Belen Curcio 2745245564 updated 2017-06-26 14:39:10 -03:00
Kim Gardner 3bb1279acf Merge branch 'master' into docs-plugins-quickstart 2017-06-26 16:15:59 +01:00
Belen Curcio fedca113d2 Adding plugin 2017-06-26 11:50:51 -03:00
Belen Curcio 59407e99d7 commentCount by asset_url 2017-06-26 09:59:28 -03:00
Kiwi c7c33dcf13 Merge branch 'master' into unify-taglists 2017-06-26 19:08:38 +07:00
Kim Gardner 0e152797ad Merge pull request #710 from ALawliet/fix-highlight-suspect-banned-words-regex
Make highlight suspect/banned words regex case insensitive
2017-06-26 12:48:11 +01:00
Kim Gardner 58d9f294b7 Merge branch 'master' into fix-highlight-suspect-banned-words-regex 2017-06-26 12:25:30 +01:00
Kim Gardner fddbf2c00f Merge pull request #713 from coralproject/validate-confirm-password
Validate `confirmPassword` in the web installer
2017-06-26 12:25:16 +01:00
Chi Vinh Le 10ae39ba09 Merge branch 'master' into unify-taglists 2017-06-26 17:39:06 +07:00
Chi Vinh Le a025f4ca24 Validate confirmPassword in the web installer 2017-06-26 17:35:45 +07:00
Chi Vinh Le eb4d85fb86 Refactor TagsInput and Improve UX 2017-06-26 17:34:15 +07:00
A Lawliet 5e6a295f11 Make highlight suspect/banned words regex case insensitive 2017-06-24 15:50:44 -04:00
David Erwin b864761ef8 Merge branch 'master' into docs-plugins-quickstart 2017-06-23 16:22:19 -04:00
David Erwin 01f6bfa979 Clean up the quickstart 2017-06-23 16:18:29 -04:00
David Erwin a49a53903d Finish quickstart first draft 2017-06-23 13:29:35 -04:00
David Erwin a3059a9fa3 Update docs menu 2017-06-22 17:42:42 -04:00
David Erwin d57d05fda9 Move plugin docs into new docs 2017-06-22 17:33:16 -04:00
David Erwin 57e0a160a2 Add plugins overview and quickstart 2017-06-22 17:25:01 -04:00
gaba d22f5215ed Adding french translation to the auth plugin. 2017-06-09 16:09:13 -07:00
gaba 19b6dfaa0a Adding a new language is not so straight forward. It needs to change at some point. 2017-06-08 11:39:51 -07:00
gaba b009d7a67a Adding french translations. 2017-06-08 10:47:17 -07:00
680 changed files with 29419 additions and 16842 deletions
+2 -1
View File
@@ -1,9 +1,10 @@
# excluded because we'll likely need to rebuild this.
node_modules
# scripts are used during development and testing, not
# most scripts are used during development and testing, not
# production.
scripts
!scripts/generateIntrospectionResult.js
# documentation should not be visable in production.
docs
+12 -9
View File
@@ -3,13 +3,16 @@ docs
client/lib
**/*.html
plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-auth
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
!plugins/coral-plugin-viewing-options
!plugins/coral-plugin-comment-content
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-auth
!plugins/talk-plugin-respect
!plugins/talk-plugin-offtopic
!plugins/talk-plugin-like
!plugins/talk-plugin-mod
!plugins/talk-plugin-love
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
node_modules
+14 -9
View File
@@ -5,25 +5,30 @@ dist
npm-debug.log*
dump.rdb
client/coral-framework/graphql/introspection.json
.env
*.cfg
.idea/
*.swp
*.DS_STORE
test/e2e/reports
coverage/
plugins.json
plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-auth
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
!plugins/coral-plugin-viewing-options
!plugins/coral-plugin-comment-content
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-auth
!plugins/talk-plugin-respect
!plugins/talk-plugin-offtopic
!plugins/talk-plugin-like
!plugins/talk-plugin-mod
!plugins/talk-plugin-love
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
**/node_modules/*
-77
View File
@@ -1,77 +0,0 @@
# Contributor's Guide
Welcome! We are very excited that you are interested in contributing to Talk.
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
## Product Roadmap
You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625.
If you'd like to see our longer term roadmap, [get in touch.](https://coralproject.net/contact.html)
## Contribute to the documentation
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
We are looking for _documentarians_ to:
* make clarity, grammar and completeness updates,
* create new / missing sections, and
* take the lead in making sections, or the over all structure better.
### But how?
* Our public docs site can be updated [here](https://github.com/coralproject/docs).
* [Let us know](https://github.com/coralproject/talk/wiki/Contact-Us) if you'd like permission to update our wiki.
* Update any of our .md docs files by following [this guide](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging).
* Our API docs need to be kept honest. [Update them here](https://github.com/coralproject/talk/blob/master/docs/swagger.yaml).
## Integrate into your environment
First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes!
Talk is designed to integrate into existing environments in a variety of ways:
* [Auth integrations](https://github.com/coralproject/talk/wiki/Security#authentication-strategies)
* [Push assets into Talk](https://github.com/coralproject/talk/blob/master/routes/api/assets/index.js)
* Monitoring Hooks (coming in 2017)
If you're considering deploying Talk, [please let us know](https://github.com/coralproject/talk/wiki/Contact-Us)! We are quite literally doing this for you and want to help you succeed any way we can.
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
## Write some code
First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes!
### Build a New Feature / Plugin
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
To get an idea of where the Coral Team is going, see:
* our [Slack channel](https://coralprojectslackin.herokuapp.com/), where you can talk directly with the Team and other community members,
* our [current stories](https://www.pivotaltracker.com/n/projects/1863625), and
* our [issues](https://github.com/coralproject/talk/issues).
Examples:
* [Add An Emoji Button to Comments](https://github.com/coralproject/talk/wiki/Add-An-Emoji-Button-to-Comments)
### Work on the Core
There is always more work to be done to make an application more stable, scaleable and secure.
If you see issues in the code or have ideas on how we may improve Talk, please consider:
* [contributing a fix](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging),
* [filing an issue](https://github.com/coralproject/talk/issues), or
* or otherwise [letting us know](https://github.com/coralproject/talk/wiki/Contact-Us).
+1 -2
View File
@@ -1,4 +1,4 @@
FROM node:7.8
FROM node:8
# Create app directory
RUN mkdir -p /usr/src/app
@@ -17,7 +17,6 @@ RUN yarn global add node-gyp && \
yarn install --frozen-lockfile && \
cli plugins reconcile && \
yarn build && \
yarn install --production && \
yarn cache clean
# Ensure the runtime of the container is in production mode.
-275
View File
@@ -1,275 +0,0 @@
## Contents
- [Installation](#installation) - install the application on a machine
- [Via Docker](#installation-from-docker)
- [Via Source](#installation-from-source)
- [Setup](#setup) - setup the application for first use
- [Usage](#usage) - connect the application to a website
# Installation
## Requirements
- Any flavour of Linux, OSX or Windows
- 1GB memory (minimum)
- 5GB storage (minimum)
- [MongoDB](https://www.mongodb.com/) v3.4 or later
- [Redis](https://redis.io/) v3.2 or later
- SSL Certificate
- This application assumes that you will be serving this application in a
production environment, and therefore requires that you serve it behind a
webserver with a valid SSL certificate. This is chosen in order to secure
user's sessions.
## Installation From Docker
We currently support packaging the Talk application via Docker, which automates
the dependency install and asset build process. This is the recommended way to
deploy the application when used in production.
Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Docker Hub.
Images are tagged using the following notation:
- `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting
new features occasionally and all the bug fixes, this is the tag for you.
- `x.y` (where `y` is the minor version number): any patch updates will be
included with this tag. If you like getting fixes and having features change
only when you want, this is the tag for you. **(recommended)**
- `x.y.z` (where `z` is the patch version): this tag never gets updated, and
essentially freezes your version, this should only be used when you are either
extending Talk or are sure of a specific version you want to freeze.
We provide tags with `*-onbuild` that can be used for easy plugin integration and
acts as a customization endpoint. Instructions are provided in the `PLUGINS.md`
document as to how to use it.
### Requirements
There are some runtime requirements for running Talk for Docker:
- [Docker](https://www.docker.com/) v1.13.0 or later
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
_Please be sure to check the versions of these requirements. Incorrect versions
of these may lead to unexpected errors!_
### Installing
An example docker-compose.yml:
```yaml
version: '2'
services:
talk:
image: coralproject/talk:1.5
restart: always
ports:
- "5000:5000"
depends_on:
- mongo
- redis
environment:
- TALK_MONGO_URL=mongodb://mongo/talk
- TALK_REDIS_URL=redis://redis
mongo:
image: mongo:3.2
restart: always
volumes:
- mongo:/data/db
redis:
image: redis:3.2
restart: always
volumes:
- redis:/data
volumes:
mongo:
external: false
redis:
external: false
```
At this stage, you should refer to the `README.md` for configuration variables
that are specific to your installation. Some pre-defined fields have been filled
in the above example which are consistent with Docker Compose naming conventions
for [Docker Links](https://docs.docker.com/compose/networking/#links).
### Scaling
If you are interested in splitting apart services, you can simply adjust the
command being executed in the container to optimize for your use case. An
example would be if you wanted to run the API server and the job processor
on different machines. You can achieve this easily with docker compose:
```yaml
version: '2'
services:
talk-api:
image: coralproject/talk:1.5
command: cli serve
restart: always
ports:
- "5000:5000"
depends_on:
- mongo
- redis
environment:
- TALK_MONGO_URL=mongodb://mongo/talk
- TALK_REDIS_URL=redis://redis
talk-jobs:
image: coralproject/talk:1.5
command: cli jobs process
restart: always
ports:
- "5001:5000"
depends_on:
- mongo
- redis
environment:
- TALK_MONGO_URL=mongodb://mongo/talk
- TALK_REDIS_URL=redis://redis
mongo:
image: mongo:3.2
restart: always
volumes:
- mongo:/data/db
redis:
image: redis:3.2
restart: always
volumes:
- redis:/data
volumes:
mongo:
external: false
redis:
external: false
```
Note that the only difference is in the `command` key. From this, you are able
to discretely control which modules are running in order to have the maximum
flexibility when managing your application.
### Running
If you're using docker compose:
```bash
# Start the services using compose
docker-compose up -d
```
If you're using plain docker:
```bash
docker run -d -P coralproject/talk:latest
```
## Installation From Source
This provides information on how to setup the application from source. Note that
this is not recommended for production deploys, but will work for development
and testing purposes.
### Requirements
There are some runtime requirements for running Talk from source:
- [Node](https://nodejs.org/) ~7.8
- [Yarn](https://yarnpkg.com/) ^0.22.0
_Please be sure to check the versions of these requirements. Incorrect versions
of these may lead to unexpected errors!_
### Installing
#### Download
It is highly recommended that you download a released version as the code
available in `master` may not be stable. You can download the latest release
from the [releases page](https://github.com/coralproject/talk/releases).
You can also clone the git repository via:
```bash
git clone https://github.com/coralproject/talk.git
```
#### Building
We now have to install the dependencies and build the static assets.
```bash
# Install package dependancies
yarn
# Build static files
yarn build
```
After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin
docs) file, you can re-run the following to install their dependencies:
```bash
# Reconcile plugins
./bin/cli plugins reconcile
# Build static files
yarn build
```
### Running
Refer to the `README.md` file for required configuration variables to add to the
environment.
You can start the server after configuring the server using the command:
```bash
yarn start
```
This will setup the server to serve everything on a single node.js process and
is designed to be used in production.
You can see other scripts we've made available by consulting the `package.json`
file under the `scripts` key including:
- `yarn test` run unit tests
- `yarn e2e` run end to end tests
- `yarn build-watch` watch for changes to client files and build static assets
- `yarn dev-start` watch for changes to server files and reload the server while
also sourcing a `.env` file in your local directory for configuration
# Setup
Once you've installed Talk (either via Docker or source), you still need to
setup the application. If you are unfamiliar with any terminology used in the
setup process, refer to the `TERMINOLOGY.md` document.
## Via Web
If you want to perform your setup via the web, you can navigate to your
installation of Talk at the path `/admin/install`. There you will be asked a
series of questions for your installation.
## Via CLI
If you want to perform your setup through the terminal, you can simply run:
```bash
cli setup
```
And follow the instructions to perform initial setup and create your first user
account.
# Usage
After setup is complete, you can then refer to the `/admin/configure` path to
get the embed code that you can copy/paste onto your blog or website in order to
start using Talk.
_In order for the embed to work correctly, you will need to whitelist the domain
that is allowed to embed your site on the `/admin/configure` page, failure to do
so will result in the comment stream not loading._
-527
View File
@@ -1,527 +0,0 @@
# Talk Plugins
Plugins for Talk can take various forms, currently we are only supporting server
side plugins.
## Plugin Registration
The parsing order for the plugin registration is as follows:
- `TALK_PLUGINS_JSON` environment variable
- `plugins.json` file
- `plugins.default.json` file
If you need to "disable all plugins", you can simply provide `{}` as the
contents of `process.env.TALK_PLUGINS_JSON` or the `plugins.json`.
The format for this is thus:
```json
{
"server": [
"people"
]
}
```
Where we have a `server` key with an array of plugins that match the folder
name in the `plugins/` folder. For example, the above config would
require a plugin from `plugins/people`, which must provide a `index.js` file
that returns an object that matches the Plugin Specification.
If the package is external (available on NPM) you can specify the string for
the version by using an object instead, for example:
```json
{
"server": [
{"people": "^1.2.0"}
]
}
```
External plugins can be resolved by running:
```bash
./bin/cli plugins reconcile
```
This achieves two things:
1. It will traverse into local plugin folders and install their dependencies.
_Note that if the plugin is already installed and available in the node_modules folder, it will not be
fetched again unless there is a version mismatch._ This will result in the
project `package.json` and `yarn.lock` files to be modified, this is normal as
this ensures that repeated deployments (with the same config) will have the
same config, these changes should not be committed to source control.
2. It will seek out dependencies that are listed in the object notation and try
to install them from npm.
## Plugin Dependencies
You may also include additional external dependencies in your local packages by
specifying a `package.json` at your plugin root which will result in a
`node_modules` folder being generated at the plugin root with your specific
dependencies.
## Deployment Solutions
Plugins can be deployed with a production instance of Talk.
### Source
Source deployments can just modify the `plugins.json` file and include any
local plugins into the `plugins/` directory. After including the config, you
need to reconcile the plugins and build the static assets:
```bash
# get plugin dependancies and remote plugins
./bin/cli plugins reconcile
# build staic assets (including enabled client side plugins)
yarn build
```
Then the application can be started as is.
### Docker
If you deploy using Docker, you can extend from the `*-onbuild` image, an
example `Dockerfile` for your project could be:
```Dockerfile
FROM coralproject/talk:latest-onbuild
```
Where the directory for your instance would contain a `plugins.json` file
describing the plugin requirements and a `plugins` directory containing any
other local plugins that should be included.
Onbuild triggers will execute when the image is building with your custom
configuration and will ensure that the image is ready to use by building all
assets inside the image as well.
## Server Plugins
### API
You can access any API available inside the talk directory in a plugin by simply
importing the file relative to the talk project root. An example would be if you
wanted to import the `MetadataService`, you would simply write:
```javascript
const MetadataService = require('services/metadata');
```
### Specification
Each plugin should export a single object with all hooks available on it.
_**Note: You will have access to the whole core and other plugin's typeDefs,
context, loaders, mutators, resolvers, hooks. This is intentional, as it
encourages composing plugins to merge functionality, like a Slack plugin which
provides a Slack notify context function as well as having the loader for
comments.**_
The following are the hooks available:
#### Field: `typeDefs`
```graphql
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
```
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
`typeDefs` should be a string that will be _merged_ with the existing type
definitions. `enum`'s will be appended to, types will be appended, and new types
will be added.
#### Field: `context`
```js
{
Slack: (context) => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
}
```
Any property provided here will be added to the context parameter available
inside all resolvers, loaders, mutators, and of course, other context based
plugins.
The top level item must accept a context for the request which it should use to
configure the context plugin before it would be mounted at `context.plugins`.
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
#### Field: `loaders`
```js
(context) => ({
People: {
load: () => db.people.find({user: context.user})
}
})
```
Loaders should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `mutators`
```js
(context) => ({
People: {
create: (name) => {
return db.people.insert({user: context.user, name});
}
}
})
```
Mutators should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `resolvers`
```js
{
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
}
```
Should return a resolver map as described in the
[Apollo Docs](http://dev.apollodata.com/tools/graphql-tools/resolvers.html#Resolver-map).
This will merge with the existing resolvers in core and from previous plugins.
#### Field: `hooks`
```js
{
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
}
```
Hooks here are pretty special, for each resolver field, you can specify a
pre/post hook that will execute pre and post field resolution.
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: `tokenUserNotFound`
```js
tokenUserNotFound: async ({jwt, token}) => {
let profile = await someExternalService(token);
if (!profile) {
return null;
}
let user = await UserModel.findOneAndUpdate({
id: profile.id
}, {
id: profile.id,
username: profile.username,
lowercaseUsername: profile.username.toLowerCase(),
roles: [],
profiles: []
}, {
setDefaultsOnInsert: true,
new: true,
upsert: true
});
return user;
}
```
The `tokenUserNotFound` hook allows auth integrations to hook into the event
when a valid token is provided but a user can't be found in the database that
matches the provided id.
The function is async, and should return the user object that was created in the
database, or null if the user wasn't found. The `jwt` paramenter of the object
is the unpacked token, while `token` is the original jwt token string.
#### Field: `router`
```js
(router) => {
router.get('/api/v1/people', (req, res) => {
res.json({people: [{name: 'Bob'}]});
});
}
```
The Router hook allows you to create a function that accepts the base express
router where you can mount any amount of middleware/routes to do any form of
action needed by external applications. We also provide the authorization
middleware via:
```js
const authorization = require('middleware/authorization');
module.exports = {
router(router) {
router.get('/api/v1/people', authorization.needed('ADMIN'), (req, res) => {
res.json({people: [{name: 'SECRET PEOPLE'}]});
});
}
}
```
#### Field: `tags`
The tags hook allows a plugin to define tags that are code controlled (added
or enabled by code). Below is an example pulled from the core off topic plugin
on how to create a hook for the `OFF_TOPIC` name:
```js
[
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
```
You can refer to `models/schema/tag.js` for the available schema to match when
creating models to enable/disable specific features.
#### Field: `passport`
```js
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
module.exports = {
passport(passport) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err);
}
return ValidateUserLogin(profile, user, done);
}));
},
router(router) {
// Note that we have to import the passport instance here, it is
// instantiated after all the strategies have been mounted.
const {passport} = require('services/passport');
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
});
}
};
```
This is a full example including the routes hook to add the required components
to the application router to support a different auth strategy.
### Full Example
Contents of `plugins.json`:
```json
{
"server": [
"people"
]
}
```
Located in `plugins/people/index.js`:
```js
module.exports = {
typeDefs: `
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
`,
context: {
Slack: () => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
},
loaders: ({user}) => ({
People: {
load: () => db.people.find({user})
}
}),
mutators: ({user}) => ({
People: {
create: (name) => {
return db.people.insert({user, name});
}
}
}),
resolvers: {
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
},
hooks: {
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) => {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
},
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
}
}
}
};
```
+1 -93
View File
@@ -5,101 +5,9 @@ Online comments are broken. Our open-source Talk tool rethinks how moderation, c
Third party licenses are available via the `/client/3rdpartylicenses.txt`
endpoint when the server is running with built assets.
## Contributing to Talk
See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md).
## Documentation
### General
See our [Talk Documentation & Guides](https://coralproject.github.io/talk/index.html).
### Plugins
See our guide to using and building [Talk Plugins](https://github.com/coralproject/talk/blob/master/PLUGINS.md).
## Usage
### Installation
To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md).
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!
### Configuration
The Talk application looks for the following configuration values either as environment variables:
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
available in the format: `<scheme>://<host>` without the path.
- `TALK_JWT_SECRET` (*required*) - a long and cryptographical secure random string which will be used to
sign and verify tokens via a `HS256` algorithm.
- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`)
- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`)
- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`)
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
SMTP provider.
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
- `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
- `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
- `TALK_PLUGINS_JSON` (_optional_) - used to specify the plugin config via the environment
- `TALK_KEEP_ALIVE` (_optional_) - The keepalive timeout that should be used to send keep alive messages through the websocket to keep the socket alive. (Default `30s`)
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
### Running Migrations
We have a migration tool that can be run using `bin/cli migration run`. This will detect new migrations available and prompt you to backup your database before proceeding with the migration. Migrations are required with major version releases.
### Using Trust
Talk ships with core components we call "Trust". This allows Talk to automate certain actions based on previous user behavior.
Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/services/karma.js).
## Supported Browsers
### Web
- Chrome: latest 2 versions
- Firefox: latest 2 versions, and most recent extended support version, if any
- Safari: latest 2 versions
- Internet Explorer: IE Edge, 11
### iOS Devices
- iPad
- iPad Pro
- iPhone 7 Plus
- iPhone 7
- iPhone 6 Plus
- iPhone 6
- iPhone 5
### iOS Browsers
- Chrome for iOS: latest version
- Firefox for iOS: latest version
- Safari for iOS: latest version
### Android Devices
- Galaxy S5
- Nexus 5X
- Nexus 6P
### Android Browsers
- Chrome for Android: latest version
- Firefox for Android: latest version
See our [Talk Documentation & Guides](https://coralproject.github.io/talk/).
## License
-73
View File
@@ -1,73 +0,0 @@
# Product's Terminology
This is a guide to have a common language to talk about "Talk".
## Definitions
* Site - a top level site, aka nytimes.com
* Section - the section of a site, aka, Politics.
* Subsection - the section of a site, aka, Politics.
* Asset - An article/video/etc identified by URL.
* Embed - Things we put on a asset: comment box, ToS, Stream, etc…
* Stream - All the activity on a certain asset. Container for Comments, actions, user
* Thread - defined by a parent and everything below. All replies to a comment and their replies, etc…
* Comment - a kind of user-generated content submitted by a comment author
* A parent comment has replies to it
* A child comments is a reply to another comment
* A comment can be both a parent comment and a child of another comment
* A top-level comment is a comment that is not a reply to any other comment
* A nth-level comment refers to the number of replies away from the top-level comment
* User - an item to represent a person using Talk. It could be a moderator, reader, etc.
* User Roles:
* Active: some who takes action (logged in or not)
* Passive: some who just reads, no actions performed
* Comment Author: The user who wrote the comment
* Staff Member: someone who works for an organization (tagged for leverage in trust)
* Moderator: someone with the ability to access the moderation queue and perform moderation actions
* Administrator: has the ability to change the setup of their coral space
* Public Profile: information about users shown in public
* Private Profile: information about users shown only to user about themselves
* Protected Profile: information about users that only moderators and admins can see
* Queue - Group of items based on a query, aka - moderation queue
* Target - The item/s on which an action is performed
## Actions
Actions are performed by users on items. Actions themselves are items. This requires two relationships: action on item, and user performs action.
### Flag
* A Flagger(user) performs a Flag
* A Flag is performed on a Comment or a username or profile content
## Moderation Actions and Status
Comments contain a field `status`. As moderation actions are peformed, the status changes.
* Initial status is empty.
* When a moderator Approves, the status is set to 'approved'.
* When a moderator Rejects, the status is set to 'reject'.
### Pre and post moderation
Comments can be set to be premoderated or postmoderated.
Premoderation means that moderation has to occur _before_ a comment is shown on the site:
* New comments are shown in the moderator queues immediately.
* The are not shown to users until (aka in streams) until they are approved by a moderator.
Postmoderation means that comments appear on the site _before_ any moderation action is taken.
* New comments appear in comment streams immediately.
* New comments do not appear in moderation queues unless they are flagged by other users.
### Word lists
* Banned words - words that the site never allows in a comment
* Suspect words - words whose usage needs to be approved by a moderator before being shown in the stream
* Approved words - words that are usually Banned or Suspect sitewide, but approved for use in a specific article stream
+22 -1
View File
@@ -6,6 +6,7 @@ const helmet = require('helmet');
const authentication = require('./middleware/authentication');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const pubsub = require('./services/pubsub');
const i18n = require('./services/i18n');
const enabled = require('debug').enabled;
const errors = require('./errors');
@@ -14,6 +15,7 @@ const apollo = require('graphql-server-express');
const accepts = require('accepts');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const {ROOT_URL} = require('./config');
const app = express();
@@ -73,6 +75,12 @@ app.use('/public', express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Set the BASE_URL as the ROOT_URL.
app.locals.BASE_URL = ROOT_URL;
if (app.locals.BASE_URL[app.locals.BASE_URL.length - 1] !== '/') {
app.locals.BASE_URL += '/';
}
//==============================================================================
// PASSPORT MIDDLEWARE
//==============================================================================
@@ -95,6 +103,19 @@ app.use(passport.initialize());
// (if present) the JWT on the request.
app.use('/api', authentication);
const pubsubClient = pubsub.createClientFactory();
// To handle dependancy injection safer, we inject the pubsub handle onto the
// request object.
app.use('/api', (req, res, next) => {
// Attach the pubsub handle to the requests.
req.pubsub = pubsubClient();
// Forward on the request.
next();
});
//==============================================================================
// GraphQL Router
//==============================================================================
@@ -159,7 +180,7 @@ app.use('/', (err, req, res, next) => {
}
i18n.init(req);
if (err instanceof errors.APIError) {
res.status(err.status);
res.render('error', {
+9 -2
View File
@@ -9,13 +9,17 @@ const program = require('./commander');
program
.command('serve', 'serve the application')
.command('settings', 'interact with the application settings')
.command('assets', 'interact with assets')
.command('setup', 'setup the application')
.command('jobs', 'work with the job queues')
.command('token', 'work with the access tokens')
.command('users', 'work with the application auth')
.command('migration', 'provides utilities for migrating the database')
.command('plugins', 'provides utilities for interacting with the plugin system')
.command(
'plugins',
'provides utilities for interacting with the plugin system'
)
.parse(process.argv);
/**
@@ -25,7 +29,10 @@ program
* labled with the PID written out by the parent process.
*/
process.once('exit', () => {
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
if (
program.runningCommand.killed === false &&
program.runningCommand.exitCode === null
) {
program.runningCommand.kill('SIGINT');
}
});
+120 -1
View File
@@ -8,13 +8,14 @@
// https://yarnpkg.com/
const program = require('./commander');
const inquirer = require('inquirer');
// Make things colorful!
require('colors');
const emoji = require('node-emoji');
const dir = process.cwd();
const fs = require('fs');
const fs = require('fs-extra');
const path = require('path');
const spawn = require('cross-spawn');
const semver = require('semver');
@@ -272,10 +273,128 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote
console.log(`✨ Done in ${totalTime}s.`);
}
async function createSeedPlugin() {
const pluginsDir = path.resolve(__dirname, '..', 'plugins');
function pluginNameExists(pluginName) {
const pluginNames = fs.readdirSync(pluginsDir);
return !!pluginNames
.filter((pn) => pn === pluginName).length;
}
let answers = await inquirer.prompt([
{
type: 'input',
name: 'pluginName',
message: 'Plugin Name:',
validate: (input) => {
if (pluginNameExists(input)) {
return 'Please, choose another name. That name already exists';
}
if (input && input.length > 0) {
return true;
}
return 'Plugin Name is required.';
}
},
{
type: 'confirm',
name: 'server',
message: 'Is this plugin extending the server capabilities?'
},
{
type: 'confirm',
name: 'client',
message: 'Is this plugin extending the client capabilities?'
},
{
type: 'confirm',
name: 'addPluginsJson',
message: 'Should we add it to the plugins.json?'
}
]);
//==============================================================================
// Creating plugin seed
//==============================================================================
const seedPlugin = path.join(__dirname, 'templates/plugin');
const newPluginPath = path.join(pluginsDir, answers.pluginName);
if (fs.existsSync(seedPlugin)) {
if (answers.server && answers.client) {
// This is a server-side and client-side plugin!, let's copy the template
fs.copySync(seedPlugin, newPluginPath);
} else {
fs.copySync(seedPlugin, newPluginPath, {filter: (p) => {
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/igm;
if (rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())) {
return true;
}
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
}});
}
// Let's add this to the plugins.json
if (answers.addPluginsJson) {
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
fs.readJson(pluginsJson)
.then((j) => {
// This is a client-side plugin, let's push this.
if (answers.client) {
j.client.push(answers.pluginName);
const output = JSON.stringify(j, null, 2);
fs.writeFileSync(pluginsJson, output);
}
// This is a server-side plugin, let's push this.
if (answers.server) {
j.server.push(answers.pluginName);
const output = JSON.stringify(j, null, 2);
fs.writeFileSync(pluginsJson, output);
}
})
.catch((err) => {
console.error(err);
});
}
console.log(`✨ Yay! Plugin created! Find your plugin: ${answers.pluginName} in the ./plugins folder`);
}
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('create')
.description('creates a seed plugin')
.action(createSeedPlugin);
program
.command('list')
.description('')
+13 -5
View File
@@ -12,6 +12,7 @@ const SetupService = require('../services/setup');
const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('./util');
const cache = require('../services/cache');
const {createSubscriptionManager} = require('../graph/subscriptions');
const {
PORT
@@ -80,12 +81,16 @@ function normalizePort(val) {
* Event listener for HTTP server "listening" event.
*/
function onListening() {
async function onListening() {
// Start the cache instance.
await cache.init();
let addr = server.address();
let bind = typeof addr === 'string'
? `pipe ${addr}`
: `port ${addr.port}`;
console.log(`API Server Listening on ${bind}`);
debug(`API Server Listening on ${bind}`);
}
/**
@@ -135,18 +140,21 @@ async function startApp(program) {
/**
* Listen on provided port, on all network interfaces.
*/
server.on('error', onError);
server.on('listening', onListening);
server.on('listening', () => {
});
server.listen(port, () => {
// Mount the websocket server if requested.
if (program.websockets) {
console.log(`Websocket Server Listening on ${port}`);
debug(`Websocket Server Listening on ${port}`);
// Mount the subscriptions server on the application server.
createSubscriptionManager(server);
}
});
server.on('error', onError);
server.on('listening', onListening);
}
//==============================================================================
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env node
const program = require('./commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingsService = require('../services/settings');
const util = require('./util');
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
/**
* Change the organization name
*/
async function changeOrgName() {
try {
let settings = await SettingsService.retrieve();
let {organizationName} = await inquirer.prompt([
{
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName
}
]);
if (settings.organizationName !== organizationName) {
settings.organizationName = organizationName;
await SettingsService.update(settings);
console.log('Settings were updated.');
} else {
console.log('No update needed, no change was made.');
}
} catch (err) {
console.error(err);
util.shutdown(1);
}
util.shutdown();
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('change-org-name')
.description('change the organization name')
.action(changeOrgName);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
util.shutdown();
}
@@ -0,0 +1,27 @@
.myPluginContainer {
padding: 10px;
background: #f0f0f0;
border: 1px solid #d6d6d6;
margin: 10px 0;
text-align: center;
border-radius: 3px;
}
.logo {
position: block;
animation: spin 2s infinite ease;
animation-delay: 1s;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.description {
color: #444444;
}
@@ -0,0 +1,25 @@
import React from 'react';
import {CoralLogo} from 'plugin-api/beta/client/components/ui';
import styles from './MyPluginComponent.css';
class MyPluginComponent extends React.Component {
render() {
return (
<div className={styles.myPluginContainer}>
<CoralLogo className={styles.logo}/>
<div className={styles.description}>
<h3>Plugin created by Talk CLI</h3>
<small>
To read more about plugins check{' '}
<a href="https://coralproject.github.io/talk/plugins-client.html">
our docs and guides!
</a>
</small>
</div>
</div>
);
}
}
export default MyPluginComponent;
+26
View File
@@ -0,0 +1,26 @@
/**
This is a client index example file and it could look like this:
```
import LoveButton from './components/LoveButton';
export default {
slots: {
commentReactions: [LoveButton]
},
reducer,
translations
};
```
To read more info on how to build client plugins. Please, go to: https://coralproject.github.io/talk/plugins-client.html
*/
import MyPluginComponent from './components/MyPluginComponent';
export default {
slots: {
stream: [MyPluginComponent]
}
};
@@ -0,0 +1,15 @@
#
# This file is for translations and they should look like this:
#
#
# ```
# en:
# talk-plugin-respect:
# respect: Respect
# respected: Respected
# es:
# talk-plugin-respect:
# respect: Respetar
# respected: Respetado
# ```
#
+2 -3
View File
@@ -1,6 +1,6 @@
machine:
node:
version: 7.9
version: 8
services:
- docker
- redis
@@ -21,9 +21,8 @@ dependencies:
# Install node dependencies.
- yarn --version
- yarn global add node-gyp --force
- yarn
cache_directories:
- ~/.cache/yarn
post:
# Build the static assets.
- yarn build
+1 -1
View File
@@ -11,4 +11,4 @@
"transform-async-to-generator",
"transform-react-jsx"
]
}
}
+14 -7
View File
@@ -1,5 +1,6 @@
import React from 'react';
import {Router, Route, IndexRedirect, browserHistory, Redirect} from 'react-router';
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import {history} from 'coral-framework/helpers/router';
import Configure from 'routes/Configure';
import Dashboard from 'routes/Dashboard';
@@ -14,7 +15,7 @@ const routes = (
<div>
<Route exact path="/admin/install" component={Install}/>
<Route path='/admin' component={Layout}>
<IndexRedirect to='/admin/moderate/all' />
<IndexRedirect to='/admin/moderate' />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
<Route path='dashboard' component={Dashboard} />
@@ -34,10 +35,15 @@ const routes = (
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<IndexRoute components={Moderation} />
<Route path='all' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='accepted' components={Moderation}>
<Route path='new' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='approved' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='premod' components={Moderation}>
@@ -46,16 +52,17 @@ const routes = (
<Route path='rejected' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='flagged' components={Moderation}>
<Route path='reported' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Redirect from=':id' to='all/:id' />
<IndexRedirect to='all' />
<Route path=':id' components={Moderation} />
</Route>
</Route>
</div>
);
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
const AppRouter = () => <Router history={history} routes={routes}/>;
export default AppRouter;
+22
View File
@@ -66,6 +66,28 @@ const validation = (formData, dispatch, next) => {
return;
}
// Confirm Validation
const prefixLength = 'confirm'.length;
const confirm = validKeys
.filter((name) => {
if (!name.startsWith('confirm')) {
return false;
}
// Check that 'confirmX' equals 'X'.
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
if (confirm.length) {
dispatch(hasError());
return;
}
dispatch(clearErrors());
next();
};
@@ -15,33 +15,11 @@ export const hideShortcutsNote = () => {
return {type: actions.HIDE_SHORTCUTS_NOTE};
};
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const setSortOrder = (order) => ({
type: actions.SET_SORT_ORDER,
order
});
export const changeUserDetailStatuses = (tab) => {
let statuses;
if (tab === 'all') {
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
} else if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
export const toggleSelectCommentInUserDetail = (id, active) => {
return {
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
id
};
};
export const toggleStorySearch = (active) => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
});
@@ -0,0 +1,24 @@
import * as actions from 'constants/userDetail';
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const changeUserDetailStatuses = (tab) => {
let statuses;
if (tab === 'all') {
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
} else if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
export const toggleSelectCommentInUserDetail = (id, active) => {
return {
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
id
};
};
@@ -1,7 +1,7 @@
import React, {PropTypes} from 'react';
import styles from './ModerationList.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../routes/Moderation/helpers/moderationQueueActionsMap';
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
@@ -0,0 +1,24 @@
.bodyLeave {
position: absolute;
width: 100%;
top: 0;
background-color: white;
opacity: 1.0;
transition: background 400ms, opacity 800ms 1600ms;
pointer-events: none;
}
.bodyLeaveActive {
opacity: 0;
background-color: rgba(255,255,0, 0.2);
}
.bodyEnter {
opacity: 0;
pointer-events: none;
}
.bodyEnterActive {
opacity: 1.0;
transition: opacity 800ms 2400ms;
}
@@ -0,0 +1,24 @@
import React from 'react';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
export default ({children, body}) => {
return (
<CSSTransitionGroup
component={'div'}
transitionName={{
enter: styles.bodyEnter,
enterActive: styles.bodyEnterActive,
leave: styles.bodyLeave,
leaveActive: styles.bodyLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={3600}
transitionLeaveTimeout={2800}
>
{React.cloneElement(React.Children.only(children), {key: murmur3(body)})}
</CSSTransitionGroup>
);
};
@@ -0,0 +1,27 @@
import React from 'react';
import Highlighter from 'react-highlight-words';
import Linkify from 'react-linkify';
const linkify = new Linkify();
export default ({suspectWords, bannedWords, body, ...rest}) => {
const links = linkify.getMatches(body);
const linkText = links ? links.map((link) => link.raw) : [];
// since words are checked against word boundaries on the backend,
// should be the behavior on the front end as well.
// currently the highlighter plugin does not support out of the box.
const searchWords = [...suspectWords, ...bannedWords]
.filter((w) => {
return new RegExp(`(^|\\s)${w}(\\s|$)`, 'i').test(body);
})
.concat(linkText);
return (
<Highlighter
{...rest}
searchWords={searchWords}
textToHighlight={body}
/>
);
};
@@ -1,22 +1,18 @@
.commentType {
position: absolute;
right: 15px;
top: 11px;
display: inline-block;
color: white;
background: grey;
height: 32px;
box-sizing: border-box;
line-height: 29px;
padding: 2px 8px 2px 26px;
padding: 2px 8px;
border-radius: 2px;
font-size: 12px;
height: 28px;
i {
> i {
font-size: 14px;
position: absolute;
left: 6px;
top: 8px;
vertical-align: text-top;
margin: 0;
margin-right: 4px;
}
&.premod {
@@ -1,12 +1,13 @@
import React, {PropTypes} from 'react';
import styles from './CommentType.css';
import {Icon} from 'coral-ui';
import cn from 'classnames';
const CommentType = (props) => {
const typeData = getTypeData(props.type);
return (
<span className={`${styles.commentType} ${styles[typeData.className]}`}>
<span className={cn(styles.commentType, styles[typeData.className], props.className)}>
<Icon name={typeData.icon}/>{typeData.text}
</span>
);
@@ -61,7 +61,7 @@ class FlagBox extends Component {
<ul>
{actionList.map((action, j) =>
<li key={`${i}_${j}`} className={styles.subDetail}>
<a className={styles.username} onClick={viewUserDetail}>
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
{action.user.username}
</a>
{action.message}
@@ -0,0 +1,13 @@
import React from 'react';
import Linkify from 'react-linkify';
const linkify = new Linkify();
export default ({text, children}) => {
const hasLinks = !!linkify.getMatches(text);
if (!hasLinks) {
return null;
}
return React.Children.only(children);
};
@@ -192,7 +192,6 @@
.minimal {
width: 45px;
min-width: 0;
float: left;
}
.approve__active {
@@ -0,0 +1,51 @@
.root {
background-color: #fff;
border: 1px solid #ccc;
overflow: hidden;
padding-left: 5px;
padding-top: 5px;
}
.rootFocuesd {
border-color: rgb(142, 76, 65);
}
.tag {
background-color: rgb(255, 220, 214);
border-radius: 2px;
border: 1px solid rgb(244, 126, 107);
color: rgb(244, 126, 107);
display: inline-block;
font-family: sans-serif;
font-size: 13px;
font-weight: 400;
margin-bottom: 5px;
margin-right: 5px;
padding: 5px;
}
.tagRemove {
cursor: pointer;
font-weight: bold;
color: rgb(101, 24, 23);
&::before {
content: " ×";
}
}
.input > input {
background: transparent;
border: 0;
color: #777;
font-family: sans-serif;
font-size: 13px;
font-weight: 400;
margin-bottom: 6px;
margin-top: 1px;
outline: none;
padding: 5px;
}
@@ -0,0 +1,31 @@
import React from 'react';
import TagsInput from 'react-tagsinput';
import styles from './TagsInput.css';
import AutosizeInput from 'react-input-autosize';
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
export default (props) => {
return (
<TagsInput
addOnBlur={true}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
className={styles.root}
focusedClassName={styles.rootFocus}
renderInput={autosizingRenderInput}
{...props}
tagProps={{
className: styles.tag,
classNameRemove: styles.tagRemove,
...props.tagProps,
}}
inputProps={{
className: styles.input,
...props.inputProps,
}}
/>
);
};
@@ -1,22 +1,20 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import Comment from './UserDetailComment';
import styles from './UserDetail.css';
import {Button, Drawer} from 'coral-ui';
import {Button, Drawer, Spinner} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import {actionsMap} from '../helpers/moderationQueueActionsMap';
import {actionsMap} from '../utils/moderationQueueActionsMap';
import ClickOutside from 'coral-framework/components/ClickOutside';
export default class UserDetail extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
bannedWords: PropTypes.array.isRequired,
suspectWords: PropTypes.array.isRequired,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeStatus: PropTypes.func.isRequired,
@@ -45,7 +43,17 @@ export default class UserDetail extends React.Component {
this.props.changeStatus('rejected');
}
render () {
renderLoading() {
return (
<ClickOutside onClickOutside={this.props.hideUserDetail}>
<Drawer onClose={this.props.hideUserDetail}>
<Spinner />
</Drawer>
</ClickOutside>
);
}
renderLoaded() {
const {
root: {
user,
@@ -53,19 +61,17 @@ export default class UserDetail extends React.Component {
rejectedComments,
comments: {nodes}
},
moderation: {
userDetailActiveTab: tab,
userDetailSelectedIds: selectedIds
},
activeTab,
selectedCommentIds,
bannedWords,
suspectWords,
toggleSelect,
bulkAccept,
bulkReject,
showBanUserDialog,
showSuspendUserDialog,
hideUserDetail
hideUserDetail,
viewUserDetail,
} = this.props;
const localProfile = user.profiles.find((p) => p.provider === 'local');
let profile;
@@ -113,11 +119,11 @@ export default class UserDetail extends React.Component {
</div>
</div>
{
selectedIds.length === 0
selectedCommentIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
@@ -134,34 +140,30 @@ export default class UserDetail extends React.Component {
cStyle='reject'
icon='close'>
</Button>
{`${selectedIds.length} comments selected`}
{`${selectedCommentIds.length} comments selected`}
</div>
)
}
<div>
{
nodes.map((comment, i) => {
nodes.map((comment) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
const selected = selectedIds.indexOf(comment.id) !== -1;
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
index={i}
user={user}
comment={comment}
selected={false}
suspectWords={suspectWords}
bannedWords={bannedWords}
viewUserDetail={() => {}}
actions={actionsMap[status]}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
toggleSelect={toggleSelect}
currentAsset={null}
currentUserId={this.props.id}
minimal={true} />;
viewUserDetail={viewUserDetail}
/>;
})
}
</div>
@@ -169,4 +171,11 @@ export default class UserDetail extends React.Component {
</ClickOutside>
);
}
render () {
if (this.props.loading) {
return this.renderLoading();
}
return this.renderLoaded();
}
}
@@ -0,0 +1,128 @@
.root {
display: block;
margin: 0;
border-bottom: 1px solid #e0e0e0;
width: 100%;
transition: all 200ms;
padding: 10px 0px;
min-height: 0;
}
.rootSelected {
background-color: #ecf4ff;
}
.container {
padding: 0px 14px;
}
.story {
font-size: 14px;
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
max-width: 500px;
}
.story > a {
display: inline-block;
color: #063b9a;
text-decoration: none;
font-weight: 500;
letter-spacing: .5px;
margin-left: 10px;
font-size: 13px;
margin-left: 5px;
padding-bottom: 0px;
border-bottom: solid 1px;
line-height: 16px;
}
.bodyContainer {
display: flex;
justify-content: space-between;
font-size: 14px;
line-height: 1.5;
font-weight: 300;
margin-bottom: 8px;
}
.header {
position: relative;
}
.commentType {
position: absolute;
right: 0px;
}
.created {
padding: 5px;
color: #262626;
font-size: 14px;
line-height: 1px;
font-weight: 300;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
font-size: 16px;
max-width: 360px;
}
.sideActions {
}
.editedMarker {
font-style: italic;
color: #666;
font-size: 12px;
line-height: 1px;
font-weight: 300;
}
.actions {
display: flex;
justify-content: flex-end;
}
.bulkSelectInput {
cursor: pointer;
}
.external {
font-size: .7em;
text-decoration: none;
color: #063b9a;
cursor: pointer;
font-weight: normal;
margin-left: 10px;
white-space: nowrap;
&:hover {
text-decoration: underline;
opacity: .9;
}
> i {
font-size: 12px;
top: 2px;
position: relative;
}
}
.hasLinks {
color: #f00;
text-align: right;
display: flex;
align-items: center;
> i {
margin-right: 5px;
}
}
@@ -0,0 +1,151 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
import styles from './UserDetailComment.css';
import CommentType from './CommentType';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import {getCommentType} from 'coral-admin/src/utils/comment';
import CommentAnimatedEdit from './CommentAnimatedEdit';
import t, {timeago} from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
render() {
const {
actions = [],
comment,
viewUserDetail,
suspectWords,
bannedWords,
selected,
toggleSelect,
className,
user,
...props
} = this.props;
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const commentType = getCommentType(comment);
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
>
<div className={styles.container}>
<div className={styles.header}>
<input
className={styles.bulkSelectInput}
type='checkbox'
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
<span className={styles.created}>
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
<CommentType type={commentType} className={styles.commentType}/>
</div>
<div className={styles.story}>
Story: {comment.asset.title}
{<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
<p className={styles.body}>
<CommentBodyHighlighter
suspectWords={suspectWords}
bannedWords={bannedWords}
body={comment.body}
/>
{' '}
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
target="_blank"
>
<Icon name="open_in_new" /> {t('comment.view_context')}
</a>
</p>
<div className={styles.sideActions}>
<IfHasLink text={comment.body}>
<span className={styles.hasLinks}>
<Icon name="error_outline" /> Contains Link
</span>
</IfHasLink>
<div className={styles.actions}>
{actions.map((action, i) => {
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
minimal={true}
key={i}
type={action}
user={user}
status={comment.status}
active={active}
acceptComment={() =>
(comment.status === 'ACCEPTED'
? null
: props.acceptComment({commentId: comment.id}))}
rejectComment={() =>
(comment.status === 'REJECTED'
? null
: props.rejectComment({commentId: comment.id}))}
/>
);
})}
</div>
</div>
</div>
</CommentAnimatedEdit>
</div>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
);
}
}
UserDetailComment.propTypes = {
user: PropTypes.object.isRequired,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
className: PropTypes.string,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
toggleSelect: PropTypes.func,
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
actions: PropTypes.array,
created_at: PropTypes.string.isRequired,
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
})
})
};
export default UserDetailComment;
@@ -1,13 +1,7 @@
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
export const SINGLE_VIEW = 'SINGLE_VIEW';
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
@@ -0,0 +1,7 @@
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
@@ -10,6 +10,7 @@ import SuspendUserDialog from './SuspendUserDialog';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
class LayoutContainer extends Component {
componentWillMount() {
@@ -57,6 +58,7 @@ class LayoutContainer extends Component {
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{this.props.children}
</Layout>
);
@@ -1,44 +1,41 @@
import React, {PropTypes} from 'react';
import React from 'react';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import {getDefinitionName} from 'coral-framework/utils';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
viewUserDetail,
hideUserDetail,
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail
} from 'coral-admin/src/actions/moderation';
toggleSelectCommentInUserDetail,
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import Comment from './Comment';
import UserDetailComment from './UserDetailComment';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
...${getDefinitionName(UserDetailComment.fragments.comment)}
}
hasNextPage
startCursor
endCursor
}
${Comment.fragments.comment}
${UserDetailComment.fragments.comment}
`;
const pluginFragments = getSlotsFragments([
const slots = [
'userProfile',
]);
];
class UserDetailContainer extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired
}
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = (status) => {
const changes = this.props.moderation.userDetailSelectedIds.map((commentId) => {
const changes = this.props.selectedCommentIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
});
@@ -56,16 +53,29 @@ class UserDetailContainer extends React.Component {
this.bulkSetCommentStatus('ACCEPTED');
}
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
render () {
if (!('user' in this.props.root)) {
if (!this.props.userId) {
return null;
}
const loading = !('user' in this.props.root) || this.props.root.user.id !== this.props.userId;
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeStatus={this.props.changeUserDetailStatuses}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
{...this.props} />;
}
}
@@ -80,7 +90,7 @@ export const withUserDetailQuery = withQuery(gql`
id
provider
}
${pluginFragments.spreads('user')}
${getSlotFragmentSpreads(slots, 'user')}
}
totalComments: commentCount(query: {author_id: $author_id})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
@@ -90,29 +100,34 @@ export const withUserDetailQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
${pluginFragments.spreads('root')}
${getSlotFragmentSpreads(slots, 'root')}
}
${Comment.fragments.comment}
${pluginFragments.definitions('user')}
${pluginFragments.definitions('root')}
${commentConnectionFragment}
`, {
options: ({id, moderation: {userDetailStatuses: statuses}}) => {
options: ({userId, statuses}) => {
return {
variables: {author_id: id, statuses}
variables: {author_id: userId, statuses}
};
}
},
skip: (ownProps) => !ownProps.userId,
});
const mapStateToProps = (state) => ({
moderation: state.moderation.toJS()
userId: state.userDetail.userId,
selectedCommentIds: state.userDetail.selectedCommentIds,
statuses: state.userDetail.statuses,
activeTab: state.userDetail.activeTab,
bannedWords: state.settings.toJS().wordlist.banned,
suspectWords: state.settings.toJS().wordlist.suspect,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
}, dispatch)
});
@@ -0,0 +1,39 @@
import {gql} from 'react-apollo';
import UserDetailComment from '../components/UserDetailComment';
import withFragments from 'coral-framework/hocs/withFragments';
export default withFragments({
comment: gql`
fragment CoralAdmin_UserDetail_comment on Comment {
id
body
created_at
status
asset {
id
title
url
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
}
}
editing {
edited
}
}
`
})(UserDetailComment);
+26 -13
View File
@@ -1,7 +1,7 @@
import update from 'immutability-helper';
import * as notification from 'coral-admin/src/services/notification';
const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
const queues = ['all', 'premod', 'reported', 'approved', 'rejected', 'new'];
const limit = 10;
const ascending = (a, b) => {
@@ -64,23 +64,35 @@ function addCommentToQueue(root, queue, comment, sort) {
return update(root, changes);
}
/**
* getCommentQueues determines in which queues a comment should be placed.
*/
function getCommentQueues(comment) {
const queues = ['all'];
if (comment.status === 'ACCEPTED') {
queues.push('accepted');
}
else if (comment.status === 'REJECTED') {
const isFlagged = comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction');
switch(comment.status) {
case 'ACCEPTED':
queues.push('approved');
break;
case 'REJECTED':
queues.push('rejected');
}
else if (comment.status === 'PREMOD') {
break;
case 'PREMOD':
queues.push('premod');
queues.push('new');
if (isFlagged) {
queues.push('reported');
}
break;
case 'NONE':
queues.push('new');
if (isFlagged) {
queues.push('reported');
}
break;
}
if (
['NONE', 'PREMOD'].indexOf(comment.status) >= 0
&& comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction')
) {
queues.push('flagged');
}
return queues;
}
@@ -98,6 +110,7 @@ function getCommentQueues(comment) {
*/
export function handleCommentChange(root, comment, sort, notify) {
let next = root;
const nextQueues = getCommentQueues(comment);
let notificationShown = false;
+14 -4
View File
@@ -1,8 +1,11 @@
import React from 'react';
import {render} from 'react-dom';
import {ApolloProvider} from 'react-apollo';
import smoothscroll from 'smoothscroll-polyfill';
import EventEmitter from 'eventemitter2';
import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
import {getClient} from './services/client';
import {getClient} from 'coral-framework/services/client';
import store from './services/store';
import App from './components/App';
@@ -11,12 +14,19 @@ import 'react-mdl/extra/material.js';
import './graphql';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
const eventEmitter = new EventEmitter();
// TODO: pass redux actions through the emitter.
loadPluginsTranslations();
injectPluginsReducers();
smoothscroll.polyfill();
render(
<ApolloProvider client={getClient()} store={store}>
<App />
</ApolloProvider>
<EventEmitterProvider eventEmitter={eventEmitter}>
<ApolloProvider client={getClient()} store={store}>
<App />
</ApolloProvider>
</EventEmitterProvider>
, document.querySelector('#root')
);
+2
View File
@@ -7,11 +7,13 @@ import install from './install';
import config from './config';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
import userDetail from './userDetail';
export default {
auth,
banUserDialog,
suspendUserDialog,
userDetail,
assets,
settings,
community,
+1 -24
View File
@@ -1,13 +1,9 @@
import {fromJS, Set} from 'immutable';
import {fromJS} from 'immutable';
import * as actions from '../constants/moderation';
const initialState = fromJS({
singleView: false,
modalOpen: false,
userDetailId: null,
userDetailActiveTab: 'all',
userDetailStatuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
userDetailSelectedIds: new Set(),
storySearchVisible: false,
storySearchString: '',
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
@@ -18,9 +14,6 @@ export default function moderation (state = initialState, action) {
switch (action.type) {
case actions.MODERATION_CLEAR_STATE:
return initialState;
case actions.SET_ACTIVE_TAB:
return state
.set('activeTab', action.activeTab);
case actions.TOGGLE_MODAL:
return state
.set('modalOpen', action.open);
@@ -30,22 +23,6 @@ export default function moderation (state = initialState, action) {
case actions.HIDE_SHORTCUTS_NOTE:
return state
.set('shortcutsNoteVisible', 'hide');
case actions.VIEW_USER_DETAIL:
return state.set('userDetailId', action.userId);
case actions.HIDE_USER_DETAIL:
return state
.set('userDetailId', null)
.update('userDetailSelectedIds', (set) => set.clear());
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return state.update('userDetailSelectedIds', (set) => set.clear());
case actions.CHANGE_USER_DETAIL_STATUSES:
return state
.set('userDetailActiveTab', action.tab)
.set('userDetailStatuses', action.statuses);
case actions.SELECT_USER_DETAIL_COMMENT:
return state.update('userDetailSelectedIds', (set) => set.add(action.id));
case actions.UNSELECT_USER_DETAIL_COMMENT:
return state.update('userDetailSelectedIds', (set) => set.delete(action.id));
case actions.SHOW_STORY_SEARCH:
return state.set('storySearchVisible', true);
case actions.HIDE_STORY_SEARCH:
@@ -0,0 +1,47 @@
import * as actions from '../constants/userDetail';
const initialState = {
userId: null,
activeTab: 'all',
statuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
selectedCommentIds: [],
};
export default function banUserDialog(state = initialState, action) {
switch (action.type) {
case actions.VIEW_USER_DETAIL:
return {
...state,
userId: action.userId,
};
case actions.HIDE_USER_DETAIL:
return {
...state,
userId: null,
selectedCommentIds: [],
};
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return {
...state,
selectedCommentIds: [],
};
case actions.CHANGE_USER_DETAIL_STATUSES:
return {
...state,
activeTab: action.tab,
statuses: action.statuses,
};
case actions.SELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: [...state.selectedCommentIds, action.id],
};
case actions.UNSELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: state.selectedCommentIds.filter((id) => id !== action.id),
};
default:
return state;
}
}
@@ -1,7 +1,7 @@
import React from 'react';
import styles from './Community.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../Moderation/helpers/moderationQueueActionsMap';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
@@ -52,57 +52,6 @@
}
}
.email {
display: block;
}
.dataTable {
width: 100%;
border-left: none;
border-right: none;
th {
font-size: 1.1em;
}
th:nth-child(2), th:nth-child(3) {
width: 100px;
}
}
.selectField {
position: relative;
width: 150px;
height: 36px;
background: #2c2c2c;
padding: 10px 15px;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
}
.list {
padding: 8px 0;
list-style: none;
@@ -340,3 +289,11 @@ p.flaggedByReason {
margin: 0px;
line-height: 1.4;
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
vertical-align: text-bottom;
&:hover {
background-color: #E0E0E0;
}
}
@@ -54,7 +54,7 @@ export default class Community extends Component {
}
getTabContent(searchValue, props) {
const {community, root: {users}} = props;
const {community, root: {users}, viewUserDetail} = props;
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
if (activeTab === 'people') {
@@ -84,6 +84,7 @@ export default class Community extends Component {
approveUser={props.approveUser}
rejectUsername={props.rejectUsername}
currentUser={this.props.currentUser}
viewUserDetail={viewUserDetail}
/>
<RejectUsernameDialog
open={community.rejectUsernameDialog}
@@ -5,7 +5,7 @@ import styles from './Community.css';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import User from './User';
const FlaggedAccounts = ({...props}) => {
const FlaggedAccounts = (props) => {
const {commenters} = props;
const hasResults = commenters && !!commenters.length;
@@ -25,6 +25,7 @@ const FlaggedAccounts = ({...props}) => {
showRejectUsernameDialog={props.showRejectUsernameDialog}
approveUser={props.approveUser}
currentUser={props.currentUser}
viewUserDetail={props.viewUserDetail}
/>;
})
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
@@ -0,0 +1,58 @@
.dataTable {
width: 100%;
border-left: none;
border-right: none;
th {
font-size: 1.1em;
}
th:nth-child(2), th:nth-child(3) {
width: 100px;
}
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
&:hover {
background-color: #D0D0D0;
}
}
.email {
display: block;
}
.selectField {
position: relative;
width: 150px;
height: 36px;
background: #2c2c2c;
padding: 10px 15px;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
}
@@ -1,9 +1,9 @@
import React from 'react';
import {SelectField, Option} from 'react-mdl-selectfield';
import styles from '../components/Community.css';
import styles from '../components/Table.css';
import t from 'coral-framework/services/i18n';
export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange}) => (
export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => (
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
@@ -21,7 +21,7 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
{commenters.map((row, i)=> (
<tr key={i}>
<td className="mdl-data-table__cell--non-numeric">
{row.username}
<button onClick={() => {viewUserDetail(row.id);}} className={styles.button}>{row.username}</button>
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
@@ -2,17 +2,18 @@ import React from 'react';
import styles from './Community.css';
import ActionButton from './ActionButton';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import t from 'coral-framework/services/i18n';
const shortReasons = {
'This comment is offensive': t('community.offensive'),
'This looks like an ad/marketing': t('community.spam_ads'),
'This user is impersonating': t('community.impersonating'),
'I don\'t like this username': t('community.dont_like_username'),
'Other': t('community.other')
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
[username.offensive]: t('community.offensive'),
[username.nolike]: t('community.dont_like_username'),
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
@@ -37,9 +38,7 @@ const User = (props) => {
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>
{user.username}
</span>
<button onClick={() => {props.viewUserDetail(user.id);}} className={styles.button}>{user.username}</button>
{props.currentUser.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
@@ -81,7 +80,12 @@ const User = (props) => {
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user && action.user.username}: {action.message ? action.message : 'n/a'}
{action.user &&
<button onClick={() => {props.viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
}
return null;
@@ -17,6 +17,7 @@ import {
showRejectUsernameDialog,
hideRejectUsernameDialog
} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import Community from '../components/Community';
@@ -99,6 +100,7 @@ const mapDispatchToProps = (dispatch) =>
hideRejectUsernameDialog,
updateSorting,
newPage,
viewUserDetail,
}, dispatch);
export default compose(
@@ -4,6 +4,7 @@ import {bindActionCreators} from 'redux';
import {compose} from 'react-apollo';
import {setRole, setCommenterStatus} from '../../../actions/community';
import Table from '../components/Table';
import {viewUserDetail} from '../../../actions/userDetail';
class TableContainer extends Component {
@@ -29,6 +30,7 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setCommenterStatus,
setRole,
viewUserDetail,
}, dispatch);
export default compose(
@@ -136,13 +136,6 @@
resize: none;
}
#bannedWordlist, #suspectWordlist {
display: block;
input {
width: 100%;
}
}
.customCSSInput {
width: 100%;
font-size: 14px;
@@ -1,7 +1,7 @@
import React from 'react';
import {Card} from 'coral-ui';
import styles from './Configure.css';
import TagsInput from 'react-tagsinput';
import TagsInput from 'coral-admin/src/components/TagsInput';
import t from 'coral-framework/services/i18n';
const Domainlist = ({domains, onChangeDomainlist}) => {
@@ -14,8 +14,6 @@ const Domainlist = ({domains, onChangeDomainlist}) => {
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => onChangeDomainlist('whitelist', tags)}
/>
</div>
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
import t from 'coral-framework/services/i18n';
import styles from './Configure.css';
import {Button, Card} from 'coral-ui';
import {BASE_URL} from 'coral-framework/constants/url';
class EmbedLink extends Component {
@@ -24,21 +25,14 @@ class EmbedLink extends Component {
}
render () {
const location = window.location;
const talkBaseUrl = [
location.protocol,
'//',
location.hostname,
location.port ? (`:${window.location.port}`) : ''
].join('');
const coralJsUrl = [talkBaseUrl, '/embed.js'].join('');
const coralJsUrl = [BASE_URL, '/embed.js'].join('');
const nonce = String(Math.random()).slice(2);
const streamElementId = `coral_talk_${nonce}`;
const embedText = `
<div id="${streamElementId}"></div>
<script src="${coralJsUrl}" async onload="
Coral.Talk.render(document.getElementById('${streamElementId}'), {
talk: '${talkBaseUrl}'
talk: '${BASE_URL}'
});
"></script>
`.trim();
@@ -1,6 +1,6 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import TagsInput from 'react-tagsinput';
import TagsInput from 'coral-admin/src/components/TagsInput';
import styles from './Configure.css';
import {Card} from 'coral-ui';
@@ -13,8 +13,6 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
<TagsInput
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => onChangeWordlist('banned', tags)}
/>
</div>
@@ -26,8 +24,6 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
<TagsInput
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => onChangeWordlist('suspect', tags)} />
</div>
</Card>
@@ -48,7 +48,7 @@ const InitialStep = (props) => {
label={t('install.create.confirm_password')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.confirm_password}
errorMsg={install.errors.confirmPassword}
/>
{
@@ -1,7 +1,7 @@
import React from 'react';
import styles from './style.css';
import {Button, Card} from 'coral-ui';
import TagsInput from 'react-tagsinput';
import TagsInput from 'coral-admin/src/components/TagsInput';
import t from 'coral-framework/services/i18n';
@@ -16,9 +16,6 @@ const PermittedDomainsStep = (props) => {
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnBlur={true}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => handleDomainsChange(tags)}
/>
</Card>
@@ -1,22 +1,20 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import Linkify from 'react-linkify';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
import FlagBox from 'coral-admin/src/components/FlagBox';
import styles from './styles.css';
import CommentType from './CommentType';
import Highlighter from 'react-highlight-words';
import CommentType from 'coral-admin/src/components/CommentType';
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
const linkify = new Linkify();
import {getCommentType} from 'coral-admin/src/utils/comment';
import t, {timeago} from 'coral-framework/services/i18n';
@@ -29,41 +27,16 @@ class Comment extends React.Component {
viewUserDetail,
suspectWords,
bannedWords,
minimal,
selected,
toggleSelect,
className,
...props
} = this.props;
const links = linkify.getMatches(comment.body);
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';
}
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const commentType = getCommentType(comment);
// since words are checked against word boundaries on the backend,
// should be the behavior on the front end as well.
// currently the highlighter plugin does not support out of the box.
const searchWords = [...suspectWords, ...bannedWords]
.filter((w) => {
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
})
.concat(linkText);
let selectionStateCSS;
if (minimal) {
selectionStateCSS = selected ? styles.minimalSelection : '';
} else {
selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
}
let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const showSuspenUserDialog = () => props.showSuspendUserDialog({
userId: comment.user.id,
@@ -81,31 +54,21 @@ class Comment extends React.Component {
return (
<li
tabIndex={props.index}
className={cn(className, 'mdl-card', selectionStateCSS, styles.Comment, styles.listItem, {[styles.minimal]: minimal, [styles.selected]: selected})}
tabIndex={0}
className={cn(className, 'mdl-card', selectionStateCSS, styles.Comment, styles.listItem, {[styles.selected]: selected})}
>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
{
!minimal && (
(
<span className={styles.username} onClick={() => viewUserDetail(comment.user.id)}>
{comment.user.username}
</span>
)
}
{
minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && (
<input
className={styles.bulkSelectInput}
type='checkbox'
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
)
}
<span className={styles.created}>
{timeago(comment.created_at || Date.now() - props.index * 60 * 1000)}
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
@@ -125,50 +88,36 @@ class Comment extends React.Component {
</ActionsMenuItem>
</ActionsMenu>
}
<CommentType type={commentType} />
<div className={styles.adminCommentInfoBar}>
<CommentType type={commentType} className={styles.commentType}/>
<Slot
data={props.data}
root={props.root}
comment={comment}
asset={comment.asset}
fill="adminCommentInfoBar"
/>
</div>
</div>
{comment.user.status === 'banned'
? <span className={styles.banned}>
<Icon name="error_outline" />
{t('comment.banned_user')}
</span>
: null}
<Slot
data={props.data}
root={props.root}
fill="adminCommentInfoBar"
comment={comment}
/>
</div>
<div className={styles.moderateArticle}>
Story: {comment.asset.title}
{!props.currentAsset &&
<Link to={`/admin/moderate/all/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
</div>
<CSSTransitionGroup
component={'div'}
style={{position: 'relative'}}
transitionName={{
enter: styles.bodyEnter,
enterActive: styles.bodyEnterActive,
leave: styles.bodyLeave,
leaveActive: styles.bodyLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={3600}
transitionLeaveTimeout={2800}
>
<div className={styles.itemBody} key={murmur3(comment.body)}>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.itemBody}>
<p className={styles.body}>
<Highlighter
searchWords={searchWords}
textToHighlight={comment.body}
<CommentBodyHighlighter
suspectWords={suspectWords}
bannedWords={bannedWords}
body={comment.body}
/>
{' '}
<a
className={styles.external}
href={`${comment.asset.url}#${comment.id}`}
href={`${comment.asset.url}?commentId=${comment.id}`}
target="_blank"
>
<Icon name="open_in_new" /> {t('comment.view_context')}
@@ -181,11 +130,11 @@ class Comment extends React.Component {
comment={comment}
/>
<div className={styles.sideActions}>
{links
? <span className={styles.hasLinks}>
<Icon name="error_outline" /> Contains Link
</span>
: null}
<IfHasLink text={comment.body}>
<span className={styles.hasLinks}>
<Icon name="error_outline" /> Contains Link
</span>
</IfHasLink>
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) => {
const active =
@@ -193,7 +142,6 @@ class Comment extends React.Component {
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
minimal={minimal}
key={i}
type={action}
user={comment.user}
@@ -219,7 +167,7 @@ class Comment extends React.Component {
/>
</div>
</div>
</CSSTransitionGroup>
</CommentAnimatedEdit>
</div>
<Slot
data={props.data}
@@ -231,7 +179,7 @@ class Comment extends React.Component {
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={() => viewUserDetail(comment.user.id)}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
@@ -240,7 +188,6 @@ class Comment extends React.Component {
}
Comment.propTypes = {
minimal: PropTypes.bool,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
@@ -251,7 +198,6 @@ Comment.propTypes = {
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
toggleSelect: PropTypes.func,
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
@@ -6,8 +6,8 @@ import ModerationQueue from './ModerationQueue';
import ModerationMenu from './ModerationMenu';
import ModerationHeader from './ModerationHeader';
import ModerationKeysModal from '../../../components/ModerationKeysModal';
import UserDetail from '../containers/UserDetail';
import StorySearch from '../containers/StorySearch';
import Slot from 'coral-framework/components/Slot';
export default class Moderation extends Component {
constructor() {
@@ -59,8 +59,7 @@ export default class Moderation extends Component {
}
getComments = () => {
const {root, route} = this.props;
const activeTab = route.path === ':id' ? 'premod' : route.path;
const {root, activeTab} = this.props;
return root[activeTab].nodes;
}
@@ -102,25 +101,28 @@ export default class Moderation extends Component {
}
render () {
const {root, moderation, settings, viewUserDetail, hideUserDetail, activeTab, ...props} = this.props;
const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, premodEnabled, ...props} = this.props;
const assetId = this.props.params.id;
const {asset} = root;
const comments = root[activeTab];
let activeTabCount;
switch(activeTab) {
case 'all':
activeTabCount = root.allCount;
break;
case 'accepted':
activeTabCount = root.acceptedCount;
case 'new':
activeTabCount = root.newCount;
break;
case 'approved':
activeTabCount = root.approvedCount;
break;
case 'premod':
activeTabCount = root.premodCount;
break;
case 'flagged':
activeTabCount = root.flaggedCount;
case 'reported':
activeTabCount = root.reportedCount;
break;
case 'rejected':
activeTabCount = root.rejectedCount;
@@ -138,12 +140,16 @@ export default class Moderation extends Component {
<ModerationMenu
asset={asset}
allCount={root.allCount}
acceptedCount={root.acceptedCount}
newCount={root.newCount}
getModPath={getModPath}
approvedCount={root.approvedCount}
premodCount={root.premodCount}
rejectedCount={root.rejectedCount}
flaggedCount={root.flaggedCount}
reportedCount={root.reportedCount}
selectSort={this.props.setSortOrder}
sort={this.props.moderation.sortOrder}
premodEnabled={premodEnabled}
activeTab={activeTab}
/>
<ModerationQueue
data={this.props.data}
@@ -173,24 +179,20 @@ export default class Moderation extends Component {
open={moderation.modalOpen}
onClose={this.onClose}/>
{moderation.userDetailId && (
<UserDetail
id={moderation.userDetailId}
hideUserDetail={hideUserDetail}
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment} />
)}
<StorySearch
assetId={assetId}
moderation={this.props.moderation}
closeSearch={this.closeSearch}
storySearchChange={this.props.storySearchChange}
/>
<Slot
data={data}
root={root}
assset={asset}
activeTab={activeTab}
fill='adminModeration'
/>
</div>
);
}
@@ -4,52 +4,72 @@ import styles from './styles.css';
import {SelectField, Option} from 'react-mdl-selectfield';
import {Icon} from 'coral-ui';
import {Link} from 'react-router';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
const ModerationMenu = (
{asset, allCount, acceptedCount, premodCount, rejectedCount, flaggedCount, selectSort, sort}
) => {
function getPath (type) {
return asset ? `/admin/moderate/${type}/${asset.id}` : `/admin/moderate/${type}`;
}
const ModerationMenu = ({
asset = {},
allCount,
approvedCount,
premodCount,
newCount,
rejectedCount,
reportedCount,
selectSort,
sort,
premodEnabled,
getModPath,
activeTab
}) => {
return (
<div className="mdl-tabs">
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div className={styles.tabBarPadding} />
<div>
{
premodEnabled ? (
<Link
to={getModPath('premod', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'premod'})}
activeClassName={styles.active}>
<Icon name='access_time' className={styles.tabIcon} /> {t('modqueue.premod')} <CommentCount count={premodCount} />
</Link>
) : (
<Link
to={getModPath('new', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'new'})}
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.new')} <CommentCount count={newCount} />
</Link>
)
}
<Link
to={getPath('all')}
className={`mdl-tabs__tab ${styles.tab}`}
to={getModPath('reported', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'reported'})}
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.all')} <CommentCount count={allCount} />
<Icon name='flag' className={styles.tabIcon} /> {t('modqueue.reported')} <CommentCount count={reportedCount} />
</Link>
<Link
to={getPath('premod')}
className={`mdl-tabs__tab ${styles.tab}`}
to={getModPath('approved', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'approved'})}
activeClassName={styles.active}>
<Icon name='access_time' className={styles.tabIcon} /> {t('modqueue.premod')} <CommentCount count={premodCount} />
<Icon name='check' className={styles.tabIcon} /> {t('modqueue.approved')} <CommentCount count={approvedCount} />
</Link>
<Link
to={getPath('flagged')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='flag' className={styles.tabIcon} /> {t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
<Link
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={getPath('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}
to={getModPath('rejected', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'rejected'})}
activeClassName={styles.active}>
<Icon name='close' className={styles.tabIcon} /> {t('modqueue.rejected')} <CommentCount count={rejectedCount} />
</Link>
<Link
to={getModPath('all', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'all'})}
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.all')} <CommentCount count={allCount} />
</Link>
</div>
<SelectField
className={styles.selectField}
@@ -68,7 +88,7 @@ ModerationMenu.propTypes = {
allCount: PropTypes.number.isRequired,
premodCount: PropTypes.number.isRequired,
rejectedCount: PropTypes.number.isRequired,
flaggedCount: PropTypes.number.isRequired,
reportedCount: PropTypes.number.isRequired,
asset: PropTypes.shape({
id: PropTypes.string
})
@@ -3,7 +3,7 @@ import React, {PropTypes} from 'react';
import Comment from '../containers/Comment';
import styles from './styles.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../helpers/moderationQueueActionsMap';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
import LoadMore from './LoadMore';
import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
@@ -85,7 +85,6 @@ class ModerationQueue extends React.Component {
data={this.props.data}
root={this.props.root}
key={comment.id}
index={i}
comment={comment}
selected={i === selectedIndex}
suspectWords={props.suspectWords}
@@ -74,7 +74,7 @@
cursor: pointer;
display: block;
text-decoration: none;
height: 50px;
min-height: 50px;
box-sizing: border-box;
transition: background-color 400ms;
@@ -64,6 +64,7 @@ const StorySearch = (props) => {
? <Spinner />
: assets.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
return <Story
key={i}
id={story.id}
@@ -213,11 +213,12 @@ span {
.author {
font-weight: 300;
min-width: 230px;
width: 100%;
display: flex;
align-items: center;
color: #262626;
font-size: 16px;
position: relative;
}
}
@@ -457,18 +458,6 @@ span {
}
}
.minimal {
margin: 0;
}
.minimalSelection {
background-color: #ecf4ff;
}
.bulkSelectInput {
cursor: pointer;
}
.emptyCardContainer {
margin-top: 16px;
}
@@ -491,31 +480,6 @@ span {
opacity: 1.0;
}
.bodyLeave {
position: absolute;
width: 100%;
top: 0;
background-color: white;
opacity: 1.0;
transition: background 400ms, opacity 800ms 1600ms;
pointer-events: none;
}
.bodyLeaveActive {
opacity: 0;
background-color: rgba(255,255,0, 0.2);
}
.bodyEnter {
opacity: 0;
pointer-events: none;
}
.bodyEnterActive {
opacity: 1.0;
transition: opacity 800ms 2400ms;
}
.editedMarker {
font-style: italic;
color: #666;
@@ -528,3 +492,11 @@ span {
position: relative;
top: .3em;
}
.adminCommentInfoBar {
min-width: 100px;
position: absolute;
right: 0px;
top: 0px;
text-align: right;
}
@@ -1,22 +1,21 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
const pluginFragments = getSlotsFragments([
const slots = [
'adminCommentInfoBar',
'adminCommentContent',
'adminSideActions',
'adminCommentDetailArea',
]);
];
export default withFragments({
root: gql`
fragment CoralAdmin_ModerationComment_root on RootQuery {
__typename
${pluginFragments.spreads('root')}
${getSlotFragmentSpreads(slots, 'root')}
}
${pluginFragments.definitions('root')}
`,
comment: gql`
fragment CoralAdmin_ModerationComment_comment on Comment {
@@ -42,9 +41,11 @@ export default withFragments({
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
}
@@ -52,8 +53,7 @@ export default withFragments({
editing {
edited
}
${pluginFragments.spreads('comment')}
${getSlotFragmentSpreads(slots, 'comment')}
}
${pluginFragments.definitions('comment')}
`
})(Comment);
@@ -8,6 +8,7 @@ import t from 'coral-framework/services/i18n';
import update from 'immutability-helper';
import truncate from 'lodash/truncate';
import NotFoundAsset from '../components/NotFoundAsset';
import {isPremod, getModPath} from '../../../utils';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {handleCommentChange} from '../../../graphql/utils';
@@ -15,13 +16,12 @@ import {handleCommentChange} from '../../../graphql/utils';
import {fetchSettings} from 'actions/settings';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
import {
toggleModal,
singleView,
hideShortcutsNote,
toggleStorySearch,
viewUserDetail,
hideUserDetail,
setSortOrder,
storySearchChange,
clearState
@@ -38,7 +38,18 @@ function prepareNotificationText(text) {
class ModerationContainer extends Component {
subscriptions = [];
get activeTab() { return this.props.route.path; }
get activeTab() {
const {root: {asset, settings}, router, route} = this.props;
// Grab premod from asset or from settings
const premod = !router.params.id ? settings.moderation : asset.settings.moderation;
const queue = isPremod(premod) ? 'premod' : 'new';
const activeTab = route.path && route.path !== ':id' ? route.path : queue;
return activeTab;
}
subscribeToUpdates(variables = this.props.data.variables) {
const sub1 = this.props.data.subscribeToMore({
@@ -154,13 +165,16 @@ class ModerationContainer extends Component {
case 'all':
variables.statuses = null;
break;
case 'accepted':
case 'new':
variables.statuses = ['NONE', 'PREMOD'];
break;
case 'approved':
variables.statuses = ['ACCEPTED'];
break;
case 'premod':
variables.statuses = ['PREMOD'];
break;
case 'flagged':
case 'reported':
variables.statuses = ['NONE', 'PREMOD'];
variables.action_type = 'FLAG';
break;
@@ -185,7 +199,7 @@ class ModerationContainer extends Component {
};
render () {
const {root, root: {asset}, data, params: {id: assetId}} = this.props;
const {root, root: {asset, settings}, data, params: {id: assetId}} = this.props;
if (data.error) {
return <div>Error</div>;
@@ -210,10 +224,12 @@ class ModerationContainer extends Component {
return <Moderation
{...this.props}
getModPath={getModPath}
loadMore={this.loadMore}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
activeTab={this.activeTab}
premodEnabled={assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation)}
/>;
}
}
@@ -305,7 +321,14 @@ const withModQueueQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
accepted: comments(query: {
new: comments(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id,
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
approved: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id,
sort: $sort
@@ -319,7 +342,7 @@ const withModQueueQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
flagged: comments(query: {
reported: comments(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD],
@@ -338,11 +361,18 @@ const withModQueueQuery = withQuery(gql`
id
title
url
settings {
moderation
}
}
allCount: commentCount(query: {
asset_id: $asset_id
})
acceptedCount: commentCount(query: {
newCount: commentCount(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id
})
approvedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
@@ -354,13 +384,14 @@ const withModQueueQuery = withQuery(gql`
statuses: [REJECTED],
asset_id: $asset_id
})
flaggedCount: commentCount(query: {
reportedCount: commentCount(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD]
})
settings {
organizationName
moderation
}
}
${commentConnectionFragment}
@@ -381,7 +412,11 @@ const withQueueCountPolling = withQuery(gql`
allCount: commentCount(query: {
asset_id: $asset_id
})
acceptedCount: commentCount(query: {
newCount: commentCount(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id
})
approvedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
@@ -393,7 +428,7 @@ const withQueueCountPolling = withQuery(gql`
statuses: [REJECTED],
asset_id: $asset_id
})
flaggedCount: commentCount(query: {
reportedCount: commentCount(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD]
@@ -426,7 +461,6 @@ const mapDispatchToProps = (dispatch) => ({
toggleStorySearch,
showSuspendUserDialog,
viewUserDetail,
hideUserDetail,
setSortOrder,
storySearchChange,
clearState
@@ -59,13 +59,13 @@ class StorySearchContainer extends React.Component {
goToStory = (id) => {
const {router} = this.props;
router.push(`/admin/moderate/all/${id}`);
router.push(`/admin/moderate/${id}`);
this.clearAndCloseSearch();
}
goToModerateAll = () => {
const {router} = this.props;
router.push('/admin/moderate/all');
router.push('/admin/moderate');
this.clearAndCloseSearch();
}
@@ -1,6 +0,0 @@
import {getClient as getFrameworkClient} from 'coral-framework/services/client';
import fragmentMatcher from './fragmentMatcher';
export function getClient() {
return getFrameworkClient({fragmentMatcher});
}
@@ -1,65 +0,0 @@
import {IntrospectionFragmentMatcher} from 'apollo-client';
// TODO this is a short-term fix
// we need to set up something to query the server for the schema before ApolloClient initialization
// https://github.com/apollographql/apollo-client/issues/1555#issuecomment-295834774
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: 'ModifyTagResponse'},
{name: 'IgnoreUserResponse'},
{name: 'StopIgnoringUserResponse'}
]
},
{
kind: 'INTERFACE',
name: 'Action',
possibleTypes: [
{name: 'DefaultAction'},
{name: 'FlagAction'},
{name: 'DontAgreeAction'}
]
},
{
kind: 'INTERFACE',
name: 'ActionSummary',
possibleTypes: [
{name: 'DefaultActionSummary'},
{name: 'FlagActionSummary'},
{name: 'DontAgreeActionSummary'}
]
},
{
kind: 'INTERFACE',
name: 'AssetActionSummary',
possibleTypes: [
{name: 'DefaultAssetActionSummary'},
{name: 'FlagAssetActionSummary'}
]
}
]
}
}
});
export default fm;
+1 -1
View File
@@ -1,7 +1,7 @@
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from '../reducers';
import {getClient} from './client';
import {getClient} from 'coral-framework/services/client';
const middlewares = [
applyMiddleware(getClient().middleware()),
+9
View File
@@ -0,0 +1,9 @@
export function getCommentType(comment) {
let commentType = '';
if (comment.status === 'PREMOD') {
commentType = 'premod';
} else if (comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction')) {
commentType = 'flagged';
}
return commentType;
}
+4
View File
@@ -0,0 +1,4 @@
export const isPremod = (mod) => mod === 'PRE';
export const getModPath = (type = 'all', assetId) =>
assetId ? `/admin/moderate/${type}/${assetId}` : `/admin/moderate/${type}`;
+3 -2
View File
@@ -1,5 +1,6 @@
import React from 'react';
import {Router, Route, browserHistory} from 'react-router';
import {Router, Route} from 'react-router';
import {history} from 'coral-framework/helpers/router';
import Embed from './containers/Embed';
import {LoginContainer} from 'coral-sign-in/containers/LoginContainer';
@@ -11,6 +12,6 @@ const routes = (
</div>
);
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
const AppRouter = () => <Router history={history} routes={routes} />;
export default AppRouter;
+36 -21
View File
@@ -1,35 +1,24 @@
import {pym} from 'coral-framework';
import pym from 'coral-framework/services/pym';
import * as actions from '../constants/stream';
import {buildUrl} from 'coral-framework/utils';
import queryString from 'query-string';
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
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 = () => {
const search = queryString.stringify({
...queryString.parse(location.search),
comment_id: undefined,
});
// remove the comment_id url param
const modifiedUrl = removeParam('comment_id', location.href);
const url = buildUrl({...location, search});
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, modifiedUrl);
window.history.replaceState({}, document.title, url);
// also change the parent url
pym.sendMessage('coral-view-all-comments');
@@ -38,6 +27,28 @@ export const viewAllComments = () => {
return {type: actions.VIEW_ALL_COMMENTS};
};
export const viewComment = (id) => {
const search = queryString.stringify({
...queryString.parse(location.search),
comment_id: id,
});
// remove the comment_id url param
const url = buildUrl({...location, search});
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, url);
// also change the parent url
pym.sendMessage('coral-view-comment', id);
} catch (e) { /* not sure if we're worried about old browsers */ }
return {type: actions.VIEW_COMMENT, id};
};
export const addCommentClassName = (className) => ({
type: actions.ADD_COMMENT_CLASSNAME,
className
@@ -47,3 +58,7 @@ export const removeCommentClassName = (idx) => ({
type: actions.REMOVE_COMMENT_CLASSNAME,
idx
});
export const setActiveTab = (tab) => (dispatch) => {
dispatch({type: actions.SET_ACTIVE_TAB, tab});
};
@@ -0,0 +1,198 @@
import React from 'react';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import {TransitionGroup} from 'react-transition-group';
import {forEachError} from 'coral-framework/utils';
import Comment from '../components/Comment';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
const comments = props.root.asset.comments;
if (comments && comments.nodes.length) {
const idCursors = [comments.nodes[0].id];
if (comments.nodes[1]) {
idCursors.push(comments.nodes[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const comments = props.root.asset.comments;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
const nextInLine = comments.nodes[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
class AllCommentsPane extends React.Component {
constructor(props) {
super(props);
this.state = {
...resetCursors(this.state, props),
loadingState: '',
};
}
componentWillReceiveProps(next) {
const {comments: prevComments} = this.props;
const {comments: nextComments} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
}
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
}
}
}
loadMore = () => {
this.setState({loadingState: 'loading'});
this.props.loadMore()
.then(() => {
this.setState({loadingState: 'success'});
})
.catch((error) => {
this.setState({loadingState: 'error'});
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
});
}
viewNewComments = () => {
this.setState(resetCursors);
};
// getVisibileComments returns a list containing comments
// which were authored by current user or comes after the `idCursor`.
getVisibleComments() {
const {comments, currentUser: user} = this.props;
const idCursor = this.state.idCursors[0];
const userId = user ? user.id : null;
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.nodes.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor || comment.user.id === userId) {
view.push(comment);
}
});
return view;
}
render() {
const {
data,
root,
comments,
commentClassNames,
ignoreUser,
setActiveReplyBox,
activeReplyBox,
addNotification,
disableReply,
postComment,
asset,
currentUser,
postFlag,
postDontAgree,
loadNewReplies,
deleteAction,
showSignInDialog,
commentIsIgnored,
charCountEnable,
maxCharCount,
editComment,
} = this.props;
const {loadingState} = this.state;
const view = this.getVisibleComments();
return (
<div className="talk-stream-comments-container">
<NewCount
count={comments.nodes.length - view.length}
loadMore={this.viewNewComments}
/>
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
commentClassNames={commentClassNames}
data={data}
root={root}
disableReply={disableReply}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
depth={0}
postComment={postComment}
asset={asset}
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
comment={comment}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
editComment={editComment}
/>;
})}
</TransitionGroup>
<LoadMore
topLevel={true}
moreComments={asset.comments.hasNextPage}
loadMore={this.loadMore}
loadingState={loadingState}
/>
</div>
);
}
}
export default AllCommentsPane;
@@ -1,11 +1,48 @@
.Reply {
.root {
margin-left: 20px;
margin-bottom: 16px;
position: relative;
margin-bottom: 15px;
padding-top: 12px;
}
.Comment {
margin-bottom: 15px;
.rootLevel0:first-child {
padding-top: 0;
border: 0;
}
.rootLevel0 {
margin-left: 0px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.comment {
position: relative;
padding-left: 15px;
}
.commentLevel0 {
padding-left: 0px;
}
.commentLevel1 {
border-left: 3px #212121 solid;
}
.commentLevel2 {
border-left: 2px #6a6a6a solid;
}
.commentLevel3 {
border-left: 2px #9e9e9e solid;
}
.commentLevel4 {
border-left: 2px #c1c1c1 solid;
}
.highlightedComment {
padding-left: 15px;
border-left: 3px solid rgb(35,118,216);
}
.pendingComment {
@@ -22,15 +59,11 @@
font-style: italic;
}
/* element in the top right of the Comment */
.topRight {
float: right;
margin-top: 10px;
text-align: right;
}
.topRight > * {
text-align: initial;
.hr {
border: 0;
height: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.topRight .popover {
@@ -44,11 +77,6 @@
border-bottom: 2px solid currentColor;
}
.topRightMenu {
cursor: pointer;
margin-top: 5px;
}
.editCommentForm {
margin-bottom: 10px;
}
@@ -99,7 +127,7 @@
}
.commentInfoBar {
float: right;
margin-left: auto;
}
@keyframes enter {
@@ -111,3 +139,35 @@
.enter {
animation: enter 1000ms;
}
.commentContainer {
}
.commentAvatar {
max-width: 60px;
}
.commentAvatar:empty {
display: none;
}
.timerIcon {
vertical-align: middle;
font-size: 14px;
}
.commentFooter {
padding-top: 8px;
}
.header {
display: flex;
align-items: center;
}
.content {
}
.footer {
min-height: 10px;
}
@@ -1,31 +1,28 @@
import React, {PropTypes} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import TagLabel from 'coral-plugin-tag-label/TagLabel';
import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment';
import AuthorName from 'talk-plugin-author-name/AuthorName';
import TagLabel from 'talk-plugin-tag-label/TagLabel';
import PubDate from 'talk-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'talk-plugin-replies';
import {FlagComment} from 'talk-plugin-flags';
import {can} from 'coral-framework/services/perms';
import {TransitionGroup} from 'react-transition-group';
import cn from 'classnames';
import styles from './Comment.css';
import {THREADING_LEVEL} from '../constants/stream';
import merge from 'lodash/merge';
import mapValues from 'lodash/mapValues';
import {
BestButton,
IfUserCanModifyBest,
BEST_TAG,
commentIsBest,
BestIndicator
} from 'coral-plugin-best/BestButton';
import LoadMore from './LoadMore';
import {getEditableUntilDate} from './util';
import {TopRightMenu} from './TopRightMenu';
import CommentContent from './CommentContent';
import Slot from 'coral-framework/components/Slot';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import InactiveCommentLabel from './InactiveCommentLabel';
import {EditableCommentContent} from './EditableCommentContent';
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive} from 'coral-framework/utils';
import t from 'coral-framework/services/i18n';
const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF');
@@ -76,7 +73,6 @@ const ActionButton = ({children}) => {
};
export default class Comment extends React.Component {
isLoadingReplies = false;
constructor(props) {
super(props);
@@ -91,6 +87,7 @@ export default class Comment extends React.Component {
isEditing: false,
replyBoxVisible: false,
animateEnter: false,
loadingState: '',
...resetCursors({}, props),
};
}
@@ -132,7 +129,6 @@ export default class Comment extends React.Component {
}
static propTypes = {
reactKey: PropTypes.string.isRequired,
// id of currently opened ReplyBox. tracked in Stream.js
activeReplyBox: PropTypes.string.isRequired,
@@ -146,12 +142,8 @@ export default class Comment extends React.Component {
addNotification: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
liveUpdates: PropTypes.bool.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
url: PropTypes.string
}).isRequired,
liveUpdates: PropTypes.bool,
asset: PropTypes.object.isRequired,
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
@@ -170,7 +162,7 @@ export default class Comment extends React.Component {
replies: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
username: PropTypes.string.isRequired
}).isRequired,
editing: PropTypes.shape({
edited: PropTypes.bool,
@@ -181,23 +173,25 @@ export default class Comment extends React.Component {
}).isRequired,
// given a comment, return whether it should be rendered as ignored
commentIsIgnored: React.PropTypes.func,
// dispatch action to add a tag to a comment
addTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeTag: React.PropTypes.func,
commentIsIgnored: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
ignoreUser: PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: React.PropTypes.func,
editComment: PropTypes.func,
}
editComment = (...args) => {
return this.props.editComment(this.props.comment.id, this.props.asset.id, ...args);
}
onClickEdit (e) {
e.preventDefault();
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
this.setState({isEditing: true});
}
@@ -213,24 +207,23 @@ export default class Comment extends React.Component {
}
loadNewReplies = () => {
if (!this.isLoadingReplies) {
this.isLoadingReplies = true;
const {replies, replyCount, id} = this.props.comment;
if (replyCount > replies.nodes.length) {
this.props.loadMore(id)
.then(() => {
this.setState(resetCursors(this.state, this.props));
this.isLoadingReplies = false;
})
.catch((e) => {
this.isLoadingReplies = false;
throw e;
const {replies, replyCount, id} = this.props.comment;
if (replyCount > replies.nodes.length) {
this.setState({loadingState: 'loading'});
this.props.loadMore(id)
.then(() => {
this.setState({
...resetCursors(this.state, this.props),
loadingState: 'success',
});
return;
}
this.setState(resetCursors);
this.isLoadingReplies = false;
})
.catch((error) => {
this.setState({loadingState: 'error'});
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
});
return;
}
this.setState(resetCursors);
};
showReplyBox = () => {
@@ -240,7 +233,8 @@ export default class Comment extends React.Component {
}
if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.setActiveReplyBox(this.props.comment.id);
return;
} else {
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
}
return;
}
@@ -298,6 +292,8 @@ export default class Comment extends React.Component {
render () {
const {
asset,
data,
root,
depth,
comment,
postFlag,
@@ -309,14 +305,13 @@ export default class Comment extends React.Component {
postDontAgree,
setActiveReplyBox,
activeReplyBox,
loadMore,
deleteAction,
disableReply,
maxCharCount,
addNotification,
charCountEnable,
showSignInDialog,
addTag,
removeTag,
liveUpdates,
commentIsIgnored,
commentClassNames = []
@@ -324,8 +319,15 @@ export default class Comment extends React.Component {
const view = this.getVisibileReplies();
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
const isActive = isCommentActive(comment.status);
const {loadingState} = this.state;
const isPending = comment.id.indexOf('pending') >= 0;
const isHighlighted = highlighted === comment.id;
const hasMoreComments = comment.replies && (comment.replies.hasNextPage || comment.replies.nodes.length > view.length);
const replyCount = this.hasIgnoredReplies() ? '' : comment.replyCount;
const moreRepliesCount = this.hasIgnoredReplies() ? -1 : comment.replyCount - view.length;
const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary(
'DontAgreeActionSummary',
@@ -338,47 +340,8 @@ export default class Comment extends React.Component {
myFlag = dontAgreeSummary.find((s) => s.current_user);
}
let commentClass = parentId
? `reply ${styles.Reply}`
: `comment ${styles.Comment}`;
commentClass += comment.id.indexOf('pending') >= 0 ? ` ${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 addBestTag = notifyOnError(
() =>
addTag({
id: comment.id,
name: BEST_TAG,
assetId: asset.id
}),
() => 'Failed to tag comment as best'
);
const removeBestTag = notifyOnError(
() =>
removeTag({
id: comment.id,
name: BEST_TAG,
assetId: asset.id
}),
() => 'Failed to remove best comment tag'
);
/**
* classNamesToAdd
* conditionClassNames
* adds classNames based on condition
* classnames is an array of objects with key as classnames and value as conditions
* i.e:
@@ -388,155 +351,176 @@ export default class Comment extends React.Component {
*
* This will add myClassName to comments tagged with STAFF TAG.
* **/
const classNamesToAdd = commentClassNames.reduce((acc, className) => {
let res = [];
// Adding classNames based on tags
Object.keys(className).forEach((cn) => {
const condition = className[cn];
condition.tags.forEach((tag) => {
if (hasTag(comment.tags, tag)) {
res = [...res, cn];
}
});
const conditionalClassNames =
mapValues(merge({}, ...commentClassNames), (condition) => {
if (condition.tags) {
return condition.tags.some((tag) => hasTag(comment.tags, tag));
}
return false;
});
// TODO: Compare rest of the comment obj to the condition if needed
const rootClassName = cn(
'talk-stream-comment-wrapper',
`talk-stream-comment-wrapper-level-${depth}`,
styles.root,
styles[`rootLevel${depth}`],
{
...conditionalClassNames,
[styles.enter]: this.state.animateEnter,
},
);
return [...acc, ...res];
}, []);
const commentClassName = cn(
'talk-stream-comment',
`talk-stream-comment-level-${depth}`,
styles.comment,
styles[`commentLevel${depth}`],
{
[styles.pendingComment]: isPending,
[styles.highlightedComment]: isHighlighted,
'talk-stream-pending-comment': isPending,
'talk-stream-highlighted-comment': isHighlighted,
}
);
// props that are passed down the slots.
const slotProps = {
data,
root,
asset,
comment,
depth,
};
return (
<div
className={cn(commentClass, 'talk-stream-comment-wrapper', classNamesToAdd, {[styles.enter]: this.state.animateEnter})}
className={rootClassName}
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}
>
<hr aria-hidden={true} />
<div
className={highlighted === comment.id ? 'highlighted-comment' : ''}
>
<AuthorName author={comment.user} className={'talk-stream-comment-user-name'} />
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
{commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel>
: null }
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
</span>
<div className={commentClassName}>
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
depth={depth}
comment={comment}
commentId={comment.id}
data={this.props.data}
root={this.props.root}
className={styles.commentAvatar}
fill="commentAvatar"
{...slotProps}
inline
/>
{ (currentUser && (comment.user.id === currentUser.id)) &&
<div className={styles.commentContainer}>
/* User can edit/delete their own comment for a short window after posting */
<span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
}
{ (currentUser && (comment.user.id !== currentUser.id)) &&
<div className={styles.header}>
<AuthorName author={comment.user} className={'talk-stream-comment-user-name'} />
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
/* TopRightMenu allows currentUser to ignore other users' comments */
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
</span>
}
{
this.state.isEditing
? <EditableCommentContent
editComment={this.props.editComment.bind(null, comment.id, asset.id)}
addNotification={addNotification}
asset={asset}
comment={comment}
currentUser={currentUser}
maxCharCount={maxCharCount}
parentId={parentId}
stopEditing={this.stopEditing}
/>
: <div>
<Slot fill="commentContent" comment={comment} defaultComponent={CommentContent} />
</div>
}
<div className="commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
data={this.props.data}
root={this.props.root}
comment={comment}
commentId={comment.id}
inline
/>
<ActionButton>
<IfUserCanModifyBest user={currentUser}>
<BestButton
isBest={commentIsBest(comment)}
addBest={addBestTag}
removeBest={removeBestTag}
/>
</IfUserCanModifyBest>
</ActionButton>
{!disableReply &&
<ActionButton>
<ReplyButton
onClick={this.showReplyBox}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
/>
</ActionButton>}
<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>
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
</ActionButton>
<ActionButton>
<FlagComment
flaggedByCurrentUser={!!myFlag}
flag={myFlag}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser}
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
{...slotProps}
/>
</ActionButton>
{ isActive && (currentUser && (comment.user.id === currentUser.id)) &&
/* User can edit/delete their own comment for a short window after posting */
<span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
}
{ isActive && (currentUser && (comment.user.id !== currentUser.id)) &&
/* TopRightMenu allows currentUser to ignore other users' comments */
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
</span>
}
{ !isActive &&
<InactiveCommentLabel status={comment.status}/>
}
</div>
<div className={styles.content}>
{
this.state.isEditing
? <EditableCommentContent
editComment={this.editComment}
addNotification={addNotification}
comment={comment}
currentUser={currentUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
parentId={parentId}
stopEditing={this.stopEditing}
/>
: <div>
<Slot
fill="commentContent"
defaultComponent={CommentContent}
{...slotProps}
/>
</div>
}
</div>
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
{isActive &&
<div className={'talk-stream-comment-actions-container'}>
<div className="commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
{...slotProps}
inline
/>
{!disableReply &&
<ActionButton>
<ReplyButton
onClick={this.showReplyBox}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
/>
</ActionButton>}
</div>
<div className="commentActionsRight comment__action-container">
<Slot
fill="commentActions"
wrapperComponent={ActionButton}
{...slotProps}
inline
/>
<ActionButton>
<FlagComment
flaggedByCurrentUser={!!myFlag}
flag={myFlag}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
addNotification={addNotification}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser}
/>
</ActionButton>
</div>
</div>
}
</div>
</div>
</div>
{activeReplyBox === comment.id
? <ReplyBox
commentPostedHandler={() => {
@@ -545,10 +529,10 @@ export default class Comment extends React.Component {
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
setActiveReplyBox={setActiveReplyBox}
parentId={parentId || comment.id}
parentId={(depth < THREADING_LEVEL) ? comment.id : parentId}
addNotification={addNotification}
authorId={currentUser.id}
postComment={postComment}
currentUser={currentUser}
assetId={asset.id}
/>
: null}
@@ -573,8 +557,7 @@ export default class Comment extends React.Component {
currentUser={currentUser}
postFlag={postFlag}
deleteAction={deleteAction}
addTag={addTag}
removeTag={removeTag}
loadMore={loadMore}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
@@ -587,12 +570,13 @@ export default class Comment extends React.Component {
/>;
})}
</TransitionGroup>
<div className="coral-load-more-replies">
<div className="talk-load-more-replies">
<LoadMore
topLevel={false}
replyCount={replyCount}
replyCount={moreRepliesCount}
moreComments={hasMoreComments}
loadMore={this.loadNewReplies}
loadingState={loadingState}
/>
</div>
</div>
@@ -1,9 +1,11 @@
import React, {PropTypes} from 'react';
import {notifyForNewCommentStatus} from 'coral-plugin-commentbox/CommentBox';
import {CommentForm} from 'coral-plugin-commentbox/CommentForm';
import {notifyForNewCommentStatus} from 'talk-plugin-commentbox/CommentBox';
import {CommentForm} from 'talk-plugin-commentbox/CommentForm';
import styles from './Comment.css';
import {CountdownSeconds} from './CountdownSeconds';
import {getEditableUntilDate} from './util';
import {can} from 'coral-framework/services/perms';
import {forEachError} from 'coral-framework/utils';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
@@ -16,11 +18,6 @@ export class EditableCommentContent extends React.Component {
// show notification to the user (e.g. for errors)
addNotification: PropTypes.func.isRequired,
asset: PropTypes.shape({
settings: PropTypes.shape({
charCountEnable: PropTypes.bool,
}),
}).isRequired,
// comment that is being edited
comment: PropTypes.shape({
@@ -37,6 +34,7 @@ export class EditableCommentContent extends React.Component {
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
// edit a comment, passed {{ body }}
@@ -45,10 +43,14 @@ export class EditableCommentContent extends React.Component {
// called when editing should be stopped
stopEditing: React.PropTypes.func,
}
constructor(props) {
super(props);
this.editComment = this.editComment.bind(this);
this.editWindowExpiryTimeout = null;
this.state = {
body: props.comment.body,
loadingState: '',
};
}
componentDidMount() {
const editableUntil = getEditableUntilDate(this.props.comment);
@@ -65,69 +67,75 @@ export class EditableCommentContent extends React.Component {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
}
async editComment(edit) {
handleBodyChange = (body) => {
this.setState({body});
}
handleSubmit = async () => {
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
this.setState({loadingState: 'loading'});
const {editComment, addNotification, stopEditing} = this.props;
if (typeof editComment !== 'function') {return;}
let response;
let successfullyEdited = false;
try {
response = await editComment(edit);
const errors = (response && response.data && response.data.editComment)
? response.data.editComment.errors
: null;
if (errors && (errors.length === 1)) {
throw errors[0];
}
successfullyEdited = true;
} catch (error) {
const errors = error.errors || [error];
errors.forEach((e) => {
if (e.translation_key) {
addNotification('error', t(`error.${e.translation_key}`));
} else if (error.networkError) {
addNotification('error', t('error.network_error'));
} else {
addNotification('error', t('edit_comment.unexpected_error'));
console.error(e);
}
});
}
if (successfullyEdited) {
response = await editComment({body: this.state.body});
this.setState({loadingState: 'success'});
const status = response.data.editComment.comment.status;
notifyForNewCommentStatus(this.props.addNotification, status);
}
if (successfullyEdited && typeof stopEditing === 'function') {
stopEditing();
if (typeof stopEditing === 'function') {
stopEditing();
}
} catch (error) {
this.setState({loadingState: 'error'});
forEachError(error, ({msg}) => addNotification('error', msg));
}
}
getEditableUntil = (props = this.props) => {
return getEditableUntilDate(props.comment);
}
isEditWindowExpired = (props = this.props) => {
return (this.getEditableUntil(props) - new Date()) < 0;
}
isSubmitEnabled = (comment) => {
// should be disabled if user hasn't actually changed their
// original comment
return (comment.body !== this.props.comment.body) && !this.isEditWindowExpired();
}
render() {
const originalBody = this.props.comment.body;
const editableUntil = getEditableUntilDate(this.props.comment);
const editWindowExpired = (editableUntil - new Date()) < 0;
return (
<div className={styles.editCommentForm}>
<CommentForm
defaultValue={this.props.comment.body}
charCountEnable={this.props.asset.settings.charCountEnable}
charCountEnable={this.props.charCountEnable}
maxCharCount={this.props.maxCharCount}
saveCommentEnabled={(comment) => {
// should be disabled if user hasn't actually changed their
// original comment
return (comment.body !== originalBody) && !editWindowExpired;
}}
saveComment={this.editComment}
submitEnabled={this.isSubmitEnabled}
body={this.state.body}
onBodyChange={this.handleBodyChange}
onSubmit={this.handleSubmit}
bodyLabel={t('edit_comment.body_input_label')}
bodyPlaceholder=""
submitText={<span>{t('edit_comment.save_button')}</span>}
saveButtonCStyle="green"
cancelButtonClicked={this.props.stopEditing}
buttonClass={styles.button}
submitButtonCStyle="green"
onCancel={this.props.stopEditing}
submitButtonClassName={styles.button}
cancelButtonClassName={styles.button}
loadingState={this.state.loadingState}
buttonContainerStart={
<div className={styles.buttonContainerLeft}>
<span className={styles.editWindowRemaining}>
{
editWindowExpired
this.isEditWindowExpired()
? <span>
{t('edit_comment.edit_window_expired')}
{
@@ -137,9 +145,9 @@ export class EditableCommentContent extends React.Component {
}
</span>
: <span>
<Icon name="timer"/> {t('edit_comment.edit_window_timer_prefix')}
<Icon name="timer" className={styles.timerIcon}/> {t('edit_comment.edit_window_timer_prefix')}
<CountdownSeconds
until={editableUntil}
until={this.getEditableUntil()}
classNameForMsRemaining={(remainingMs) => (remainingMs <= 10 * 1000) ? styles.editWindowAlmostOver : '' }
/>
</span>
@@ -4,67 +4,82 @@ import Slot from 'coral-framework/components/Slot';
import {can} from 'coral-framework/services/perms';
import t from 'coral-framework/services/i18n';
import {TabBar, Tab, TabContent, Button} from 'coral-ui';
import Count from 'coral-plugin-comment-count/CommentCount';
import {TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import Popup from 'coral-framework/components/Popup';
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
import ConfigureStreamContainer
from 'coral-configure/containers/ConfigureStreamContainer';
import cn from 'classnames';
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.
// TODO: move data fetching to appropiate containers.
switch (tab) {
case 'profile':
this.props.data.refetch();
break;
case 2:
this.props.setActiveTab('config');
// TODO: move data fetching to config container.
case 'config':
this.props.data.refetch();
break;
}
this.props.setActiveTab(tab);
};
handleShowProfile = () => this.props.setActiveTab('profile');
render() {
const {activeTab, viewAllComments, commentId} = this.props;
const {asset: {totalCommentCount}} = this.props.root;
const {activeTab, commentId, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props;
const {user} = this.props.auth;
const hasHighlightedComment = !!commentId;
return (
<div>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab} className='talk-stream-tabbar'>
<Tab className={'talk-stream-comment-count-tab'} id='stream'><Count count={totalCommentCount}/></Tab>
<Tab className={'talk-stream-profile-tab'} id='profile'>{t('framework.my_profile')}</Tab>
<Tab className={'talk-stream-configuration-tab'} id='config' restricted={!can(user, 'UPDATE_CONFIG')}>{t('framework.configure_stream')}</Tab>
</TabBar>
{commentId &&
<Button
cStyle="darkGrey"
style={{float: 'right'}}
onClick={viewAllComments}
>
{t('framework.show_all_comments')}
</Button>}
<Slot fill="embed" />
<TabContent show={activeTab === 'stream'}>
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
<IfSlotIsNotEmpty slot="login">
<Popup
href='embed/stream/login'
title='Login'
features='menubar=0,resizable=0,width=500,height=550,top=200,left=500'
open={showSignInDialog}
focus={signInDialogFocus}
onFocus={focusSignInDialog}
onBlur={blurSignInDialog}
onClose={hideSignInDialog}
/>
</IfSlotIsNotEmpty>
<TabBar
onTabClick={this.changeTab}
activeTab={activeTab}
className='talk-embed-stream-tab-bar'
aria-controls='talk-embed-stream-tab-content'
>
<Tab tabId={'stream'} className={'talk-embed-stream-comments-tab'}>
{t('embed_comments_tab')}
</Tab>
<Tab tabId={'profile'} className={'talk-embed-stream-profile-tab'}>
{t('framework.my_profile')}
</Tab>
{can(user, 'UPDATE_CONFIG') &&
<Tab tabId={'config'} className={'talk-embed-stream-configuration-tab'}>
{t('framework.configure_stream')}
</Tab>
}
</TabBar>
<Slot fill="embed" />
<TabContent
activeTab={activeTab}
id='talk-embed-stream-tab-content'
>
<TabPane tabId={'stream'}>
<Stream data={this.props.data} root={this.props.root} />
</TabContent>
<TabContent show={activeTab === 'profile'}>
</TabPane>
<TabPane tabId={'profile'}>
<ProfileContainer />
</TabContent>
<TabContent show={activeTab === 'config'}>
</TabPane>
<TabPane tabId={'config'}>
<ConfigureStreamContainer />
</TabContent>
</div>
</TabPane>
</TabContent>
</div>
);
}
@@ -9,7 +9,7 @@ export class IgnoreUserWizard extends React.Component {
// comment on which this menu appears
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
username: PropTypes.string.isRequired
}).isRequired,
cancel: PropTypes.func.isRequired,
@@ -47,7 +47,7 @@ export class IgnoreUserWizard extends React.Component {
const step2Confirmation = (
<div>
<header>Ignore User</header>
<p>Are you sure you want to ignore { user.name }?</p>
<p>Are you sure you want to ignore { user.username }?</p>
<div className={styles.textAlignRight}>
<Button cStyle='cancel' onClick={this.onClickCancel}>Cancel</Button>
<Button onClick={onClickIgnoreUser}>Ignore user</Button>
@@ -0,0 +1,32 @@
.root {
display: inline-block;
color: white;
background: grey;
height: 22px;
box-sizing: border-box;
line-height: 19px;
padding: 2px 6px 2px 4px;
border-radius: 2px;
font-size: 12px;
text-transform: capitalize;
}
.icon {
font-size: 14px;
vertical-align: text-top;
margin: 0;
margin-right: 4px;
}
.label {
display: inline-block;
}
.premod {
background: #063B9A;
}
.rejected {
background: #d03235;
}
@@ -0,0 +1,36 @@
import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
import styles from './InactiveCommentLabel.css';
import {Icon} from 'coral-ui';
import cn from 'classnames';
const InactiveCommentLabel = ({status, className, ...rest}) => {
let label;
let icon;
switch (status) {
case 'PREMOD':
label = t('modqueue.premod');
icon = 'query_builder';
break;
case 'REJECTED':
label = t('modqueue.rejected');
icon = 'close';
break;
default:
throw new Error(`Unknown inactive status ${status}`);
}
return (
<span {...rest} className={cn(className, styles.root, styles[status.toLowerCase()])}>
<Icon name={icon} className={styles.icon}/>
<span className={styles.label}>{label}</span>
</span>
);
};
InactiveCommentLabel.propTypes = {
status: PropTypes.string.isRequired,
};
export default InactiveCommentLabel;
@@ -1,39 +1,31 @@
import React, {PropTypes} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
class LoadMore extends React.Component {
componentDidMount () {
this.initialState = true;
}
replyCountFormat = (count) => {
if (!count) {
return t('framework.view_all_replies_unknown_number');
return t('framework.show_all_replies');
}
if (count === 1) {
return t('framework.view_reply');
return t('framework.show_1_more_reply');
}
if (this.initialState) {
return t('framework.view_all_replies_initial', count);
} else {
return t('framework.view_all_replies', count);
}
}
loadMore = () => {
this.initialState = false;
this.props.loadMore();
return t('framework.show_x_more_replies', count);
}
render () {
const {topLevel, moreComments, replyCount} = this.props;
const {topLevel, moreComments, replyCount, loadingState, loadMore} = this.props;
const disabled = loadingState === 'loading';
return moreComments
? <div className='coral-load-more'>
? <div className='talk-load-more'>
<Button
onClick={this.loadMore}>
onClick={loadMore}
className={cn('talk-load-more-button', {[`talk-load-more-button-${loadingState}`]: loadingState})}
disabled={disabled}
>
{topLevel ? t('framework.view_more_comments') : this.replyCountFormat(replyCount)}
</Button>
</div>
@@ -44,7 +36,9 @@ class LoadMore extends React.Component {
LoadMore.propTypes = {
replyCount: PropTypes.number,
topLevel: PropTypes.bool.isRequired,
loadMore: PropTypes.func.isRequired
loadMore: PropTypes.func.isRequired,
moreComments: PropTypes.bool,
loadingState: PropTypes.oneOf(['', 'loading', 'success', 'error']),
};
export default LoadMore;
@@ -1,16 +1,17 @@
import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const NewCount = ({count, loadMore}) => {
return <div className='coral-new-comments coral-load-more'>
return <div className='talk-new-comments talk-load-more'>
{
count ?
<button onClick={loadMore}>
<Button onClick={loadMore}>
{count === 1
? t('framework.new_count', count, t('framework.comment'))
: t('framework.new_count', count, t('framework.comments'))}
</button>
</Button>
: null
}
</div>;
@@ -0,0 +1,29 @@
.root {
margin-top: 6px;
}
.viewAllButton {
position: absolute;
right: 0px;
}
.tabPanel {
margin-top: 8px;
}
.filterWrapper {
position: absolute;
right: 0;
margin-top: 4px;
}
.highlightedContainer {
margin-top: 28px;
}
.tabContainer {
position: relative;
margin-top: 28px;
padding-bottom: 50px;
min-height: 600px;
}
+186 -203
View File
@@ -1,162 +1,121 @@
import React, {PropTypes} from 'react';
import LoadMore from './LoadMore';
import React from 'react';
import PropTypes from 'prop-types';
import {StreamError} from './StreamError';
import Comment from '../components/Comment';
import SuspendedAccount from './SuspendedAccount';
import Slot from 'coral-framework/components/Slot';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import InfoBox from 'talk-plugin-infobox/InfoBox';
import {can} from 'coral-framework/services/perms';
import {ModerationLink} from 'coral-plugin-moderation';
import {ModerationLink} from 'talk-plugin-moderation';
import RestrictedMessageBox
from 'coral-framework/components/RestrictedMessageBox';
import t, {timeago} from 'coral-framework/services/i18n';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import {TransitionGroup} from 'react-transition-group';
import {getSlotComponents} from 'coral-framework/helpers/plugins';
import CommentBox from 'talk-plugin-commentbox/CommentBox';
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import {isCommentActive} from 'coral-framework/utils';
import {Button, TabBar, Tab, TabCount, TabContent, TabPane} from 'coral-ui';
import cn from 'classnames';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
import AllCommentsPane from './AllCommentsPane';
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
const comments = props.root.asset.comments;
if (comments && comments.nodes.length) {
const idCursors = [comments.nodes[0].id];
if (comments.nodes[1]) {
idCursors.push(comments.nodes[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const comments = props.root.asset.comments;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
const nextInLine = comments.nodes[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
import styles from './Stream.css';
class Stream extends React.Component {
isLoadingMore = false;
constructor(props) {
super(props);
this.state = resetCursors(this.state, props);
this.state = {
keepCommentBox: false,
};
}
componentWillReceiveProps(next) {
const {root: {asset: {comments: prevComments}}} = this.props;
const {root: {asset: {comments: nextComments}}} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
// Keep comment box when user was live suspended, banned, ...
if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) {
this.setState({keepCommentBox: true});
}
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
this.fallbackAllTab(next);
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
componentDidMount() {
this.fallbackAllTab();
}
fallbackAllTab(props = this.props) {
if (props.activeStreamTab !== 'all') {
const slotPlugins = this.getSlotComponents('streamTabs', props).map((c) => c.talkPluginName);
if (slotPlugins.indexOf(props.activeStreamTab) === -1) {
props.setActiveStreamTab('all');
}
}
}
viewNewComments = () => {
this.setState(resetCursors);
};
getSlotProps({data, root, root: {asset}} = this.props) {
return {data, root, asset};
}
setActiveReplyBox = (reactKey) => {
getSlotComponents(slot, props = this.props) {
return getSlotComponents(slot, props.reduxState, this.getSlotProps(props));
}
setActiveReplyBox = (id) => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
} else {
this.props.setActiveReplyBox(reactKey);
this.props.setActiveReplyBox(id);
}
};
loadMoreComments = () => {
if (!this.isLoadingMore) {
this.isLoadingMore = true;
this.props.loadMoreComments()
.then(() => this.isLoadingMore = false)
.catch((e) => {
this.isLoadingMore = false;
throw e;
});
}
}
// getVisibileComments returns a list containing comments
// which were authored by current user or comes after the `idCursor`.
getVisibleComments() {
const {root: {asset: {comments}}, auth: {user}} = this.props;
const idCursor = this.state.idCursors[0];
const userId = user ? user.id : null;
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.nodes.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor || comment.user.id === userId) {
view.push(comment);
}
});
return view;
userIsDegraged({auth: {user}} = this.props) {
return !can(user, 'INTERACT_WITH_COMMUNITY');
}
render() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
appendItemArray,
commentClassNames,
root: {asset, asset: {comments}, comment, me},
root: {asset, asset: {comment, comments, totalCommentCount}, me},
postComment,
addNotification,
editComment,
postFlag,
postDontAgree,
deleteAction,
showSignInDialog,
addTag,
updateItem,
ignoreUser,
activeStreamTab,
setActiveStreamTab,
loadNewReplies,
loadMoreComments,
viewAllComments,
auth: {loggedIn, user},
removeTag,
pluginProps,
editName
} = this.props;
const view = this.getVisibleComments();
const {keepCommentBox} = this.state;
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;
let highlightedComment = comment && getTopLevelParent(comment);
if (highlightedComment) {
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
const isInactive = !isCommentActive(comment.status);
if (comment.parent && isInactive) {
// the highlighted comment is not active and as such not in the replies, so we
// attach it to the right parent.
highlightedComment = attachCommentToParent(highlightedComment, comment);
}
}
const banned = user && user.status === 'BANNED';
const temporarilySuspended =
@@ -171,8 +130,25 @@ class Stream extends React.Component {
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
};
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const slotProps = this.getSlotProps();
if (!comment && !comments) {
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
return <StreamError />;
}
return (
<div id="stream">
<div id="stream" className={styles.root}>
{comment &&
<Button
cStyle="darkGrey"
className={cn('talk-stream-show-all-comments-button', styles.viewAllButton)}
onClick={viewAllComments}
>
{t('framework.show_all_comments')}
</Button>}
{open
? <div id="commentBox">
@@ -189,7 +165,7 @@ class Stream extends React.Component {
<RestrictedMessageBox>
{t(
'stream.temporarily_suspended',
this.props.root.settings.organizationName,
root.settings.organizationName,
timeago(user.suspension.until)
)}
</RestrictedMessageBox>}
@@ -199,26 +175,26 @@ class Stream extends React.Component {
editName={editName}
currentUsername={user.username}
/>}
{loggedIn &&
!banned &&
!temporarilySuspended &&
!highlightedComment &&
{showCommentBox &&
<CommentBox
addNotification={this.props.addNotification}
postComment={this.props.postComment}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
addNotification={addNotification}
postComment={postComment}
appendItemArray={appendItemArray}
updateItem={updateItem}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
authorId={user.id}
currentUser={user}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>}
</div>
: <p>{asset.settings.closedMessage}</p>}
<Slot fill="stream" />
<Slot
fill="stream"
{...slotProps}
/>
{loggedIn && (
<ModerationLink
@@ -227,85 +203,98 @@ class Stream extends React.Component {
/>
)}
<div className="talk-stream-wrapper-box">
<Slot fill="streamBox" />
</div>
{/* 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}
disableReply={!open}
postComment={this.props.postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadNewReplies}
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}
editComment={this.props.editComment}
liveUpdates={true}
/>
: <div className="talk-stream-comments-container">
<NewCount
count={comments.nodes.length - view.length}
loadMore={this.viewNewComments}
? (
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
<Comment
data={data}
root={root}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
depth={0}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={highlightedComment.id}
commentIsIgnored={commentIsIgnored}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
liveUpdates={true}
/>
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
commentClassNames={commentClassNames}
data={this.props.data}
root={this.props.root}
disableReply={!open}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
addTag={addTag}
removeTag={removeTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={this.props.loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
pluginProps={pluginProps}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
liveUpdates={false}
/>;
})}
</TransitionGroup>
<LoadMore
topLevel={true}
moreComments={asset.comments.hasNextPage}
loadMore={this.loadMoreComments}
/>
</div>}
</div>
)
: <div className={cn('talk-stream-tab-container', styles.tabContainer)}>
<div
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
>
<Slot
fill="streamFilter"
{...slotProps}
/>
</div>
<TabBar activeTab={activeStreamTab} onTabClick={setActiveStreamTab} sub>
{this.getSlotComponents('streamTabs').map((PluginComponent) => (
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...slotProps}
active={activeStreamTab === PluginComponent.talkPluginName}
/>
</Tab>
))}
<Tab tabId={'all'}>
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
</Tab>
</TabBar>
<TabContent activeTab={activeStreamTab} sub>
{this.getSlotComponents('streamTabPanes').map((PluginComponent) => (
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...slotProps}
/>
</TabPane>
))}
<TabPane tabId={'all'}>
<AllCommentsPane
data={data}
root={root}
comments={comments}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadMoreComments}
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
commentIsIgnored={commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
/>
</TabPane>
</TabContent>
</div>
}
</div>
);
}
@@ -315,12 +304,6 @@ Stream.propTypes = {
addNotification: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
// dispatch action to add a tag to a comment
addTag: PropTypes.func,
// dispatch action to remove a tag from a comment
removeTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,

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