Compare commits

...
303 Commits
Author SHA1 Message Date
Kim Gardner 2564aa98bb Merge pull request #916 from coralproject/author-menu
Author menu, member since, and ignored users
2017-09-05 09:03:17 +01:00
Chi Vinh Le 612ed3a16d Merge branch 'author-menu' of ssh://github.com/coralproject/talk into author-menu 2017-09-05 02:24:55 +07:00
Chi Vinh Le a5acf8ba82 Add some global css classes 2017-09-05 02:22:10 +07:00
Kiwi 00b46fe0ea Merge branch 'master' into author-menu 2017-09-04 20:27:19 +02:00
Chi Vinh Le a722456359 reorder 2017-09-05 01:25:16 +07:00
Chi Vinh Le 943c9decba Add to default plugins 2017-09-05 01:18:14 +07:00
Chi Vinh Le 77396ccc31 Fix graphql tree bug 2017-09-05 01:17:05 +07:00
Chi Vinh Le e1a5a64b39 Pluginize stop ignoring user feature in profile 2017-09-05 01:09:43 +07:00
Chi Vinh Le 44e3d0b811 Add title 2017-09-05 00:28:07 +07:00
Chi Vinh Le 07f5f42870 Optimized render 2017-09-05 00:16:27 +07:00
Chi Vinh Le 123935c8bc Also notify on errors 2017-09-05 00:00:27 +07:00
Chi Vinh Le 2d4adb60be Show notification on success 2017-09-04 23:55:44 +07:00
Chi Vinh Le f8297e9652 Hide menu after action 2017-09-04 23:29:36 +07:00
Chi Vinh Le 74e8c28f45 Apply styling 2017-09-04 23:13:00 +07:00
Chi Vinh Le 89258638d4 Correct exclude if logic 2017-09-04 22:35:05 +07:00
Kim Gardner 902670cdac Merge pull request #914 from coralproject/ignored
Top-level comments by ignored user are tombstoned, but replies should be as well
2017-09-04 11:32:10 +01:00
Kim Gardner f11cd3c676 Merge branch 'master' into ignored 2017-09-04 11:19:48 +01:00
Kim Gardner f088b4d459 Merge pull request #915 from coralproject/fix-live-modqueue
Use correct moderation queues during updates
2017-09-04 11:18:29 +01:00
Chi Vinh Le c0252e160b Fix reply fragments 2017-09-04 16:44:53 +07:00
Chi Vinh Le 98cb34f554 Don't ignore myself 2017-09-04 16:25:19 +07:00
Chi Vinh Le b2ef7fa795 Fix tombstone bug 2017-09-04 15:51:04 +07:00
Chi Vinh Le ffef2f640d First implementation of ignore user plugin 2017-09-04 15:40:39 +07:00
Chi Vinh Le 26bae69454 Fix bug when determining moderation queues for live updates 2017-09-03 19:36:02 +07:00
Chi Vinh Le c07eaf0edb Add member since 2017-09-01 23:15:00 +07:00
Chi Vinh Le 9c2ea693b9 Add hover styles 2017-09-01 22:29:30 +07:00
Chi Vinh Le 7c339aa510 Render contentSlot if set 2017-09-01 22:25:26 +07:00
Belén Curcio 2ed6bb4067 Merge branch 'master' into ignored 2017-09-01 12:23:21 -03:00
Chi Vinh Le 692d92f645 Implement talk-plugin-author-menu 2017-09-01 22:20:49 +07:00
Belen Curcio 7bb4b03daa Retrieving me from Comment triggers a change 2017-09-01 12:17:48 -03:00
Kim Gardner e132f1d652 Merge pull request #913 from coralproject/redis-fix
Fixed Redis Connection Error
2017-09-01 15:53:10 +01:00
Wyatt Johnson 644788ea31 removed unused option 2017-09-01 08:47:08 -06:00
Wyatt Johnson e368c50687 ioredis changed config, fixes #901 2017-09-01 08:44:25 -06:00
Kim Gardner 4569c1bd4f Merge pull request #911 from coralproject/premodqueue
Create both a PreMod and a New Queue when stream is switched from one status to another
2017-09-01 10:30:13 +01:00
Kim Gardner d6c46cb6d1 Merge branch 'master' into premodqueue 2017-09-01 10:25:30 +01:00
Kim Gardner faeadeb61d Merge pull request #912 from coralproject/embed-fix
Fixed path joining for embed link
2017-09-01 10:01:34 +01:00
Wyatt Johnson d161733cdc fixed path joining 2017-08-31 13:59:00 -06:00
Wyatt Johnson 2fd6d370c8 Merge branch 'master' into premodqueue 2017-08-31 13:16:41 -06:00
Wyatt Johnson 824abcdfa3 Merge pull request #902 from coralproject/ioredis
Replace node_redis with ioredis
2017-08-31 11:57:38 -06:00
Belen Curcio 36c4c6a59f Ready 2017-08-31 14:22:59 -03:00
Belen Curcio 99235df566 No premod status to the New Queue 2017-08-31 13:17:34 -03:00
Belen Curcio af0e9bc880 New Queue should appear if there are comments with NONE status 2017-08-31 13:02:07 -03:00
Belen Curcio 23c8e2a0e6 Refetching settings :) 2017-08-31 12:53:47 -03:00
Wyatt Johnson c17fcfe5ef Merge branch 'master' into ioredis 2017-08-31 09:24:20 -06:00
Belen Curcio f81d5f8957 Grabbing settings from the store 2017-08-31 11:41:20 -03:00
Belen Curcio db55689f75 premod and new 2017-08-31 11:30:11 -03:00
Belen Curcio f140efe1bd Premod and New Queue Edge Case 2017-08-31 11:10:28 -03:00
Kim Gardner fe638a4c54 Merge pull request #910 from coralproject/edit-should-disappear
Make rendering `edit` pure
2017-08-31 13:13:25 +01:00
Chi Vinh Le b61afbc1d6 Make rendering edit pure 2017-08-31 19:04:54 +07:00
Kim Gardner 82e95dc4a1 Merge pull request #908 from coralproject/hide-empty-categories
Hide empty categories
2017-08-31 11:18:31 +01:00
Chi Vinh Le 3b7d006d1e Hide empty categories take 2 2017-08-31 16:59:04 +07:00
Chi Vinh Le b5daf5882b IfSlotIsEmpty and IfSlotIsNotEmpty now only renders a single child 2017-08-31 16:58:01 +07:00
Kim Gardner 11f5460604 Merge branch 'master' into hide-empty-categories 2017-08-31 10:45:52 +01:00
Kim Gardner 6e90dd6d62 Merge pull request #909 from coralproject/cleanup-plugins-default
Cleanup default plugins
2017-08-31 10:44:17 +01:00
Kiwi 635f6d7720 Merge branch 'master' into hide-empty-categories 2017-08-31 11:39:17 +02:00
Kiwi a4b40f2c46 Merge branch 'master' into cleanup-plugins-default 2017-08-31 11:39:13 +02:00
Kim Gardner 14541b48a1 Merge pull request #907 from coralproject/hide-on-click
Hide menu when clicking on a sort option
2017-08-31 10:38:56 +01:00
Chi Vinh Le c7c5167311 Cleanup default plugins 2017-08-31 16:36:08 +07:00
Chi Vinh Le e39c377e08 Hide empty categories 2017-08-31 16:33:51 +07:00
Chi Vinh Le 2887b20e38 Merge branch 'master' into hide-on-click 2017-08-31 16:32:46 +07:00
Chi Vinh Le 0a631cfcae Hide viewing options when changing sort 2017-08-31 16:12:28 +07:00
Kim Gardner 12c54c39db Merge branch 'master' into ioredis 2017-08-31 08:51:42 +01:00
Kim Gardner 5ad92972b1 Merge pull request #903 from coralproject/paginate-flagged-users
Paginate flagged users and more
2017-08-31 08:42:00 +01:00
Kim Gardner 201f6fd8ae Merge branch 'master' into paginate-flagged-users 2017-08-31 08:32:17 +01:00
Kim Gardner b0a6417848 Merge pull request #906 from gjtorikian/capitalize-the-h
Capitalize the H
2017-08-31 08:32:05 +01:00
Garen J. Torikian f80e4af683 Capitalize the H 2017-08-30 23:22:33 -04:00
Wyatt Johnson 7f9f864aeb Merge branch 'master' into paginate-flagged-users 2017-08-30 16:11:13 -06:00
Wyatt Johnson e9f5fcda2a Merge branch 'master' into ioredis 2017-08-30 16:06:36 -06:00
Wyatt Johnson b1c9d07ca7 Merge pull request #905 from coralproject/fix-query-event-names
Use correct event name in queries
2017-08-30 14:40:08 -06:00
Wyatt Johnson 53c32518b4 Merge branch 'master' into paginate-flagged-users 2017-08-30 14:14:45 -06:00
Kiwi 9d08dc2490 Merge branch 'master' into ioredis 2017-08-30 21:52:30 +02:00
Chi Vinh Le e5c385ae21 Use correct event name in queries 2017-08-31 02:05:00 +07:00
Belén Curcio 5e5e0028c9 Merge pull request #904 from coralproject/exclude-ignored-fc
Exclude Ignored users in Featured Comments Plugin
2017-08-30 16:01:36 -03:00
Belen Curcio cb7e7eae97 Using a variable 2017-08-30 15:45:25 -03:00
Belen Curcio e98432b17e Exclude Ignored 2017-08-30 15:35:52 -03:00
Chi Vinh Le 5434413308 Use fetchPolicy network-only 2017-08-30 23:47:48 +07:00
Chi Vinh Le 8b4f21ff4c Remove extra long transition timeout :-D 2017-08-30 23:27:48 +07:00
Chi Vinh Le 4072c39f05 Merge branch 'master' into paginate-flagged-users
Conflicts:
	client/coral-admin/src/routes/Community/components/FlaggedAccounts.js
	client/coral-admin/src/routes/Community/components/User.js
	client/coral-admin/src/routes/Community/containers/Community.js
	graph/loaders/users.js
2017-08-30 22:56:30 +07:00
Chi Vinh Le 9b9476d44f Add todos 2017-08-30 22:51:16 +07:00
Wyatt Johnson 2e72fec4d7 Merge branch 'master' into ioredis 2017-08-30 09:34:14 -06:00
Chi Vinh Le 1f489ff0e5 Always refetch when switching to FlaggedAccounts 2017-08-30 22:30:53 +07:00
Chi Vinh Le f582cc2534 Add transition effect 2017-08-30 22:25:03 +07:00
Wyatt Johnson a2536eb41b updated pubsub 2017-08-30 09:06:48 -06:00
Chi Vinh Le c66fb053e7 Implement mutation updators 2017-08-30 21:40:28 +07:00
Chi Vinh Le 797f0fd0e3 Docs in schema 2017-08-30 21:05:03 +07:00
Chi Vinh Le 02ec1f69ee More graphql fixes 2017-08-30 20:42:13 +07:00
Chi Vinh Le 37638cab86 Fix center styling 2017-08-30 20:41:49 +07:00
Chi Vinh Le 4ce6206516 Add deprecation and refactor notices to legacy styles 2017-08-30 19:56:24 +07:00
Chi Vinh Le 67b0624c90 Refactor styles 2017-08-30 19:48:54 +07:00
Chi Vinh Le 9348985191 Refactor into a ComponentClass 2017-08-30 19:04:30 +07:00
Chi Vinh Le 0df7df9dd6 Wrap ul before li 2017-08-30 19:03:55 +07:00
Chi Vinh Le ffab9ca8d5 Refactor users query 2017-08-30 18:48:20 +07:00
Kim Gardner b21eae78b0 Merge pull request #900 from coralproject/cli-fixes
Fixed errors when launching not-defined commands
2017-08-30 11:50:34 +01:00
Wyatt Johnson 8f3bfd2bd4 Replaced node_redis with ioredis 2017-08-29 17:52:29 -06:00
Wyatt Johnson 27661a9294 fixed errors when launching not-defined commands 2017-08-29 14:15:05 -06:00
Kim Gardner 2c3f5385ae Merge pull request #898 from coralproject/bump-version
Update version to 3.4.0
2017-08-29 19:31:42 +01:00
Wyatt Johnson 7bedfefecb Merge branch 'master' into bump-version 2017-08-29 12:10:56 -06:00
Wyatt Johnson 5ab78491b5 Merge pull request #890 from coralproject/lint-upgrade
Upgraded/pruned deps, applied linting fixes
2017-08-29 12:10:47 -06:00
Kim Gardner a83261397d Update version to 3.4.0 2017-08-29 19:06:46 +01:00
Wyatt Johnson c8d1871418 Merge branch 'master' into lint-upgrade 2017-08-29 11:56:59 -06:00
Kim Gardner 081cdfa145 Merge pull request #897 from coralproject/sort-enhancements
Sort enhancements
2017-08-29 18:50:34 +01:00
Kiwi fecfcf91ef Merge branch 'master' into sort-enhancements 2017-08-29 19:39:59 +02:00
Chi Vinh Le 90a0446afa Move Spinner down to replacing TabContent 2017-08-30 00:35:55 +07:00
Kim Gardner ec5c7f32b0 Merge pull request #895 from coralproject/my-comment-reactions
My comment reactions
2017-08-29 18:32:54 +01:00
Chi Vinh Le 21e4cf5865 Live update replies on all sort and always refetch 2017-08-30 00:27:14 +07:00
Kim Gardner 6894060648 Merge branch 'master' into my-comment-reactions 2017-08-29 18:24:54 +01:00
Kim Gardner 95d14fc5a6 Merge pull request #896 from coralproject/fix-popup-interval
Fix endless interval
2017-08-29 18:11:58 +01:00
Chi Vinh Le 46c031b35d Fix endless interval 2017-08-29 23:53:00 +07:00
Chi Vinh Le ddced5b8c9 Refactor styling 2017-08-29 23:43:58 +07:00
Wyatt Johnson c786805505 small bug fix 2017-08-29 10:28:30 -06:00
Wyatt Johnson 75125993b9 Added patch for missing action summary type 2017-08-29 10:21:31 -06:00
Kim Gardner 393005fce6 Merge pull request #889 from coralproject/my-comment-reactions
My Comment Reactions
2017-08-29 16:33:00 +01:00
Kim Gardner 952b253969 Merge branch 'master' into my-comment-reactions 2017-08-29 16:24:49 +01:00
Kim Gardner ad06d5136b Merge pull request #893 from coralproject/email-regex-fix
Changed email verification regex
2017-08-29 16:24:17 +01:00
Chi Vinh Le 18016ad23a Refactor and implement pagination 2017-08-29 22:23:35 +07:00
Kim Gardner d987724c7d Merge branch 'master' into email-regex-fix 2017-08-29 16:18:40 +01:00
Chi Vinh Le 87e90e956d Port current frontend to connections 2017-08-29 19:06:58 +07:00
Belen Curcio 9140688c65 Merge branch 'my-comment-reactions' of github.com:coralproject/talk into my-comment-reactions 2017-08-29 09:03:02 -03:00
Belen Curcio 1053c55d19 Adding zero state style for 0 counts 2017-08-29 09:01:40 -03:00
Chi Vinh Le eb67d86163 Implement UserConnection on the graph 2017-08-29 18:57:20 +07:00
Belen Curcio da92a8e7c2 Plural and singular translations 2017-08-29 08:47:23 -03:00
Kiwi d82932306d Merge branch 'master' into lint-upgrade 2017-08-29 12:47:47 +02:00
Kim Gardner a76464ea16 Merge branch 'master' into my-comment-reactions 2017-08-29 10:22:58 +01:00
Kim Gardner b76f1956dc Merge pull request #892 from coralproject/asset-tests
Added duplicated comments tests
2017-08-29 10:12:52 +01:00
Wyatt Johnson 7823b65c7d Changed email verification regex, fixes #891 2017-08-28 16:51:41 -06:00
Wyatt Johnson 38f98c7c43 Added duplicated comments tests 2017-08-28 16:41:58 -06:00
Wyatt Johnson 30e6f1d169 Merge branch 'master' into lint-upgrade 2017-08-28 16:19:10 -06:00
Wyatt Johnson df6f6a72ef Merge pull request #885 from coralproject/subscriber
Adding new Slot: commentAuthorTags
2017-08-28 16:18:32 -06:00
Wyatt Johnson c09381ff84 Merge branch 'master' into subscriber 2017-08-28 15:40:31 -06:00
Wyatt Johnson a172f5be51 Merge pull request #881 from coralproject/feature/heroku-deploy
Heroku Deploy
2017-08-28 15:39:52 -06:00
Wyatt Johnson 2fec9e3c0f Merge branch 'master' into feature/heroku-deploy 2017-08-28 15:33:24 -06:00
Wyatt Johnson eb586f217b Upgraded/pruned deps, applied linting fixes 2017-08-28 14:06:54 -06:00
Belen Curcio e77bdf0b90 linting 2017-08-28 16:31:46 -03:00
Belen Curcio 3e045291ee Translations added 2017-08-28 16:30:17 -03:00
Belen Curcio 93c1ecd12d My Comment Reactions 2017-08-28 16:25:26 -03:00
Kim Gardner a570626787 Merge pull request #888 from coralproject/debugging-optim
Increased debugging and optimized a query path
2017-08-28 18:55:07 +01:00
Wyatt Johnson e92f75d4ad Merge branch 'master' into debugging-optim 2017-08-28 11:50:02 -06:00
Wyatt Johnson 057d8cabd2 Increased debugging and optimized a query path 2017-08-28 11:47:51 -06:00
Kiwi 4f901d0d56 Merge branch 'master' into subscriber 2017-08-28 19:43:54 +02:00
Kim Gardner 9af0c5e01a Merge pull request #887 from coralproject/fix-action-type-query
Fix `action_type` query
2017-08-28 18:28:54 +01:00
Kiwi afd3b1b3fe Merge branch 'master' into fix-action-type-query 2017-08-28 19:21:19 +02:00
Chi Vinh Le 44c6798959 Fix action_type query 2017-08-29 00:20:36 +07:00
Belén Curcio e14d6007a7 Merge branch 'master' into subscriber 2017-08-28 13:12:19 -03:00
Wyatt Johnson 1aa4f83c89 Merge pull request #886 from coralproject/query-fix
Fixed query
2017-08-28 09:47:28 -06:00
Wyatt Johnson 8cfccb7b00 fixed query 2017-08-28 09:42:37 -06:00
Kim Gardner c05bee2316 Merge pull request #880 from coralproject/load-more-history
Only load my own comments in history
2017-08-28 16:37:11 +01:00
Kiwi f4365eb848 Merge branch 'master' into load-more-history 2017-08-28 17:27:04 +02:00
Belén Curcio 96006a0196 Merge branch 'master' into subscriber 2017-08-28 12:26:08 -03:00
Chi Vinh Le 9436188487 Merge branch 'master' into load-more-history
Conflicts:
	client/coral-settings/containers/ProfileContainer.js
2017-08-28 22:25:20 +07:00
Kim Gardner 733d8e1eae Merge pull request #882 from coralproject/fix-permalink-liveupdates
Fix live update false comments in permalink view
2017-08-28 16:23:57 +01:00
Belén Curcio 3108f3dac3 Merge branch 'master' into subscriber 2017-08-28 12:22:05 -03:00
Kim Gardner 1e2c617775 Merge branch 'master' into fix-permalink-liveupdates 2017-08-28 16:17:40 +01:00
Belen Curcio 12d66a49d0 Adding commentAuthorTags 2017-08-28 12:17:01 -03:00
Kim Gardner 22945d49df Merge pull request #884 from coralproject/rm-semicolon
Remove semicolon
2017-08-28 16:15:07 +01:00
Kiwi b8c6305939 Merge branch 'master' into rm-semicolon 2017-08-28 17:09:42 +02:00
Chi Vinh Le 7911e366e5 Remove semicolon 2017-08-28 22:04:21 +07:00
Kim Gardner 1ba5fd0028 Merge pull request #870 from coralproject/user-sort
Optimizations and User Sort
2017-08-28 15:30:31 +01:00
Chi Vinh Le 7856c9d947 Fix live update false comments in permalink view 2017-08-28 21:05:32 +07:00
Jeff Nelson 7c9e2297df don't hardcode branch 2017-08-28 09:54:20 -04:00
Chi Vinh Le 153c5acce1 Only load my own comments in history 2017-08-28 19:15:29 +07:00
Chi Vinh Le bb42cffc17 Add sort plugins for other reactions 2017-08-28 18:50:01 +07:00
Chi Vinh Le 0dffc15836 Hover styles 2017-08-28 18:22:30 +07:00
Chi Vinh Le e4490e20a7 Merge branch 'master' into user-sort
Conflicts:
	package.json
2017-08-28 18:16:18 +07:00
Chi Vinh Le f9bc5f79ec Don't sort and ignore duplicate comments (embed-stream) 2017-08-28 18:08:33 +07:00
Kim Gardner 5ec65c86ce Merge pull request #879 from coralproject/questionbox
Question Box Builder
2017-08-25 20:58:41 +01:00
Wyatt Johnson 8a5db167ff changed sort -> sortOrder 2017-08-25 09:59:37 -06:00
Chi Vinh Le 48595a1bbf Merge branch 'master' into user-sort
Conflicts:
	client/coral-admin/src/reducers/moderation.js
	client/coral-embed-stream/src/actions/stream.js
	client/coral-embed-stream/src/components/Stream.js
	client/coral-framework/components/Slot.js
	plugin-api/beta/client/utils/index.js
	test/server/routes/api/account/index.js
2017-08-25 21:55:00 +07:00
Belén Curcio ce63942902 Merge branch 'master' into questionbox 2017-08-25 11:52:11 -03:00
Kim Gardner 0ecf1cedc3 Merge pull request #874 from coralproject/refactor-global-dependencies
Dependency Management
2017-08-25 15:41:53 +01:00
Chi Vinh Le 7f899b0440 Attach to fragments tree 2017-08-25 21:40:57 +07:00
Chi Vinh Le 88b2798718 Merge branch 'user-sort' of ssh://github.com/coralproject/talk into user-sort 2017-08-25 21:30:24 +07:00
Chi Vinh Le e3227322a5 Some docs 2017-08-25 21:22:48 +07:00
Kim Gardner a767ab73da Update default plugins to include new sorts 2017-08-25 15:20:05 +01:00
Kim Gardner 3e65eac7a0 Merge branch 'master' into refactor-global-dependencies 2017-08-25 15:18:46 +01:00
Kim Gardner 1b6b0a4b87 Merge pull request #878 from coralproject/bump-version-3-3-0
Update version to 3.3.0
2017-08-25 15:18:36 +01:00
Chi Vinh Le 42951aaf0b Adding proptypes 2017-08-25 21:17:15 +07:00
Chi Vinh Le 6b96fcfad1 Only unsubscribe from newest comments 2017-08-25 21:05:30 +07:00
Jeff Nelson fe0c32898d Update README.md 2017-08-25 10:03:46 -04:00
Jeff Nelson 091d787e34 Update README.md 2017-08-25 10:02:16 -04:00
Jeff Nelson 7ee2794344 Merge branch 'master' into feature/heroku-deploy 2017-08-25 09:56:55 -04:00
Jeff Nelson cc66f1b459 update 2017-08-25 09:47:59 -04:00
Jeff Nelson 50caddee56 update 2017-08-25 09:47:40 -04:00
Jeff Nelson 28dafd3e07 update 2017-08-25 09:27:52 -04:00
Chi Vinh Le 38c290958c Merge branch 'user-sort' of ssh://github.com/coralproject/talk into user-sort 2017-08-25 20:22:09 +07:00
Jeff Nelson 2f3d7c5329 set default root url 2017-08-25 09:17:51 -04:00
Chi Vinh Le e51e8c543f Add more sort options and improve styling 2017-08-25 20:15:07 +07:00
Jeff Nelson 4b7cc9ffcc whoops 2017-08-25 08:55:26 -04:00
Kim Gardner 59ddcea592 Update version to 3.3.0 2017-08-25 13:55:19 +01:00
Jeff Nelson faecd80543 better handling for heroku dynamic port 2017-08-25 08:51:36 -04:00
Chi Vinh Le 9e2f6f1f63 Use factory 2017-08-25 19:40:55 +07:00
Jeff Nelson e73c113e0d use mailgun since postmark has been deprecated 2017-08-25 08:30:29 -04:00
Chi Vinh Le 6ae68a4f90 First working version 2017-08-25 19:21:57 +07:00
Chi Vinh Le f0931b4dc6 Fix asset.comment resolver 2017-08-25 19:21:14 +07:00
Kim Gardner 62687f307a Merge pull request #877 from coralproject/own-user-comments
Cleaned up user query logic
2017-08-25 12:37:16 +01:00
Kim Gardner a83ea98978 Merge branch 'master' into own-user-comments 2017-08-25 12:22:53 +01:00
Kim Gardner 7aadaa6f8b Merge pull request #873 from coralproject/banned-words
Banned Words / Suspect Words Fix
2017-08-25 12:22:28 +01:00
Belen Curcio 46f17f5381 Fully working :) 2017-08-24 19:10:30 -03:00
Belen Curcio de7ab8f7ac With fancy lazy load for the Markdown Editor :D 2017-08-24 19:05:57 -03:00
Belen Curcio 5302d6d935 Markdown Editor Added and Working 2017-08-24 18:21:52 -03:00
Belen Curcio 3d0d5df139 Markdown Added 2017-08-24 18:16:32 -03:00
Belen Curcio 575a3030f9 Smooth transition 2017-08-24 16:40:16 -03:00
Belen Curcio 73058c624f Linting 2017-08-24 16:32:51 -03:00
Belen Curcio 5cb71eaee8 Icon List for Question Box 2017-08-24 16:32:07 -03:00
Wyatt Johnson 60677e3d2a cleaned up user query logic 2017-08-24 13:07:44 -06:00
Belen Curcio 7991188f8d deleting old code from default.css and building QuestionBox component 2017-08-24 14:45:47 -03:00
Wyatt Johnson 13796fb268 added more test cases 2017-08-24 11:18:17 -06:00
Wyatt Johnson 308449a67c added more messaging for cli tool 2017-08-24 10:24:56 -06:00
Kim Gardner c733b7760d Merge branch 'master' into banned-words 2017-08-24 17:22:49 +01:00
Kiwi a56240675d Merge branch 'master' into refactor-global-dependencies 2017-08-24 18:17:19 +02:00
Kim Gardner 1381395dde Merge pull request #875 from coralproject/moderator-community
Moderator Community
2017-08-24 17:14:46 +01:00
Wyatt Johnson 9a1374070f small fixes 2017-08-24 09:47:27 -06:00
Kim Gardner 1e8ff9de7e Merge branch 'master' into moderator-community 2017-08-24 15:35:48 +01:00
Chi Vinh Le ddb555e07f Add new sorting variables 2017-08-24 21:30:16 +07:00
Chi Vinh Le d1a3a67b35 Fix featured comments 2017-08-24 20:12:08 +07:00
Chi Vinh Le df60ac4714 Merge branch 'master' into user-sort 2017-08-24 20:09:44 +07:00
Chi Vinh Le ca31dedca8 Make comments query optional 2017-08-24 20:07:45 +07:00
Kiwi cb7213ea58 Merge branch 'master' into refactor-global-dependencies 2017-08-24 13:49:46 +02:00
Kiwi 2d7d505913 Merge branch 'master' into banned-words 2017-08-24 13:49:31 +02:00
Chi Vinh Le a42d668329 Merge branch 'refactor-global-dependencies' of ssh://github.com/coralproject/talk into refactor-global-dependencies 2017-08-24 18:47:09 +07:00
Chi Vinh Le 645aaa09ae Add some docs 2017-08-24 18:40:38 +07:00
Kim Gardner 8b04da9cf4 Merge pull request #876 from coralproject/useful-errors
Added message to Extendable Errors
2017-08-24 12:24:42 +01:00
Wyatt Johnson acf792089d added more db optims 2017-08-23 15:42:30 -06:00
Wyatt Johnson 38469a7e69 Added message to Extendable Error's 2017-08-23 13:30:23 -06:00
Wyatt Johnson 763f753928 Merge branch 'master' into refactor-global-dependencies 2017-08-23 11:56:35 -06:00
Wyatt Johnson 1a27f0e00f Merge branch 'master' into moderator-community 2017-08-23 11:56:06 -06:00
Wyatt Johnson 6d8185c33e modified middleware to change AND -> OR, added mod to com pages 2017-08-23 11:52:02 -06:00
Belén Curcio 3f83a8f1a9 Merge pull request #868 from coralproject/slot-username
New Slot: commentAuthorName
2017-08-23 14:20:41 -03:00
Kiwi f5eac91e0e Merge branch 'master' into refactor-global-dependencies 2017-08-23 18:02:17 +02:00
Wyatt Johnson a68513227a adjusted tokenizer to use regex based tokenizer instead 2017-08-23 09:54:00 -06:00
Chi Vinh Le ce360a47f8 Add slot to fragments in featured comments 2017-08-23 22:40:48 +07:00
Chi Vinh Le 1bcf31d22d Merge branch 'slot-username' of ssh://github.com/coralproject/talk into slot-username 2017-08-23 20:15:44 +07:00
Chi Vinh Le ceba418e28 created_at is a Date 2017-08-23 20:14:55 +07:00
Belén Curcio 0961797f8f Merge branch 'master' into banned-words 2017-08-23 10:10:04 -03:00
Belen Curcio 0342b6c826 rolling back 2017-08-23 10:06:39 -03:00
Chi Vinh Le c2b0a60d8a Remove global dependency on router history 2017-08-23 20:02:36 +07:00
Chi Vinh Le bf89409f60 Remove global dependency on Storage 2017-08-23 19:53:06 +07:00
Kim Gardner 994923cae0 Merge branch 'master' into user-sort 2017-08-23 13:00:12 +01:00
Kim Gardner 2a839250ed Merge branch 'master' into slot-username 2017-08-23 13:00:06 +01:00
Kim Gardner dcf09adc1c Merge pull request #872 from coralproject/missing-fragment
Add missing fragment field to Comment
2017-08-23 12:59:38 +01:00
Kiwi 7ff4a2e75d Merge branch 'master' into slot-username 2017-08-23 13:09:46 +02:00
Kim Gardner 2d1bf32464 Merge branch 'master' into missing-fragment 2017-08-23 12:04:24 +01:00
Chi Vinh Le 0811c9302d Ignore access from react dev tools 2017-08-23 17:50:35 +07:00
Kim Gardner a5f7a7cd75 Merge pull request #871 from coralproject/graphiql-fix
Template bug fix
2017-08-23 11:41:29 +01:00
Chi Vinh Le 4dd20260a8 Unprotect User.created_at 2017-08-23 17:32:29 +07:00
Chi Vinh Le 5eafb672ef Add missing fragment field to Comment 2017-08-23 17:07:28 +07:00
Chi Vinh Le 61e601a2a5 Merge branch 'master' into refactor-global-dependencies 2017-08-23 04:05:56 +07:00
Chi Vinh Le a9e3115bd9 Expose getErrorMessages in plugin-api 2017-08-23 03:53:37 +07:00
Chi Vinh Le a23bad49c6 Remove global dependency on notification on admin 2017-08-23 03:48:20 +07:00
Chi Vinh Le 89037a2e05 Refactor notification 2017-08-23 03:22:56 +07:00
Chi Vinh Le f428bfa6c9 WIP 2017-08-23 03:16:19 +07:00
Belen Curcio b6951488c4 Tokenizer is messing with escaped regexes 2017-08-22 16:37:24 -03:00
Chi Vinh Le 1ee7c81675 Remove admin store impl 2017-08-23 02:24:26 +07:00
Chi Vinh Le a934722672 Cleanup 2017-08-23 02:23:18 +07:00
Wyatt Johnson 35f8b0dc5f Merge branch 'graphiql-fix' into user-sort 2017-08-22 13:22:35 -06:00
Chi Vinh Le a882b36501 Fix connection bug 2017-08-23 02:20:43 +07:00
Wyatt Johnson 23f4724f99 fixed template bug 2017-08-22 13:18:48 -06:00
Belen Curcio 9fa1f3d958 Adding auto escape 2017-08-22 16:15:41 -03:00
Wyatt Johnson f85480c0ae Merge branch 'master' into user-sort 2017-08-22 13:04:32 -06:00
Chi Vinh Le fccbcce7a8 Remove global dependency on registry and plugins 2017-08-23 01:41:58 +07:00
Wyatt Johnson ef0c0ed628 reduced image size by 65% 2017-08-22 12:05:25 -06:00
Wyatt Johnson ffc6a72017 added docs for Comments.Sort, some changes to interface 2017-08-22 11:47:52 -06:00
Wyatt Johnson b719168b18 Implemented sortBy 2017-08-22 10:11:47 -06:00
Kiwi 4eac739568 Merge branch 'master' into slot-username 2017-08-22 17:41:13 +02:00
Kim Gardner 8ecf243d2d Merge pull request #869 from coralproject/admin-reply
Reply Badge - Admin Side
2017-08-22 16:37:42 +01:00
Chi Vinh Le 54ea0b1a3f Remove global dependency on store, rest, pym and apollo client 2017-08-22 22:04:49 +07:00
Belen Curcio cb4d2998f6 Removing logic from ReplyBadge 2017-08-22 09:40:49 -03:00
Wyatt Johnson e0f9bb4165 optimized to use new query path 2017-08-21 16:08:30 -06:00
Wyatt Johnson eec0e18600 bug fixes, removed dead code 2017-08-21 16:05:35 -06:00
Wyatt Johnson 78d4740abe drastically improved queries involving actions + comments 2017-08-21 15:54:14 -06:00
Wyatt Johnson 01ed9880f4 comment count optim, prep for sortBy 2017-08-21 15:26:04 -06:00
Belen Curcio c402627280 Adding Reply Badge to User History 2017-08-21 17:27:32 -03:00
Belen Curcio 647b5ed745 Adding Reply Badge 2017-08-21 17:09:26 -03:00
Belen Curcio a540dfe8d4 Adding hasParent on Comment to the Graph 2017-08-21 16:21:39 -03:00
Belen Curcio 19dfe86118 Updates 2017-08-21 14:53:24 -03:00
Wyatt Johnson 15111c81a2 improved messaging 2017-08-21 11:32:16 -06:00
Belen Curcio cea6f7b8e8 Deleting Reference to FakeComment 2017-08-21 14:26:22 -03:00
Wyatt Johnson 7552e34a15 Merge branch 'master' into user-sort 2017-08-21 10:58:18 -06:00
Wyatt Johnson 7da7db1eb4 package upgrades 2017-08-21 10:57:57 -06:00
Wyatt Johnson b3e15de9f3 extended verify tool with reply count fixing 2017-08-21 10:40:18 -06:00
Belen Curcio 9bff90bd4d Adding Slot to Featured Comments 2017-08-21 13:18:46 -03:00
Belen Curcio 22f473a9ea Merge branch 'slot-username' of github.com:coralproject/talk into slot-username 2017-08-21 12:28:24 -03:00
Belen Curcio 663a46896e Merge branch 'master' of github.com:coralproject/talk into slot-username 2017-08-21 12:28:05 -03:00
Belén Curcio bfa195d7cb Merge branch 'master' into slot-username 2017-08-21 12:27:00 -03:00
Belen Curcio 9028c1b4eb Adding fragment Slot 2017-08-21 12:15:53 -03:00
Wyatt Johnson d43d359048 ensure that events are handled sync 2017-08-21 08:49:56 -06:00
Wyatt Johnson ec2c62a607 Merge branch 'master' into user-sort 2017-08-21 08:48:26 -06:00
Chi Vinh Le d47287fbd5 Remove unused import 2017-08-21 21:39:40 +07:00
Chi Vinh Le 74f778aef7 Merge branch 'master' into talk-provider
Conflicts:
	client/coral-embed-stream/src/containers/Embed.js
	plugin-api/beta/client/hocs/withReaction.js
2017-08-21 21:38:39 +07:00
Chi Vinh Le 9566f83eac Remove coral-framework reducers
Some of them were only related to the embed stream.
Auth code is too convoluted with the embed stream and can't be reused atm.
2017-08-21 21:31:54 +07:00
Chi Vinh Le 84e4091ea6 Implement TalkProvider 2017-08-21 21:23:59 +07:00
Belen Curcio 0ea2660c97 Adding new Slot: authorName - Deleting Old Code 2017-08-21 10:44:03 -03:00
Wyatt Johnson 7125250cd6 updated tests 2017-08-20 12:54:46 -06:00
Wyatt Johnson 5036a2665a fixes to tests and impl edit hooks 2017-08-20 12:25:22 -06:00
Wyatt Johnson b2e871f4a6 added event for edit to manage reply count 2017-08-19 10:49:50 -06:00
Wyatt Johnson 77b7ddf337 updated dockerfile 2017-08-19 10:40:06 -06:00
Wyatt Johnson 3804a7afae removed more outdated test code 2017-08-19 10:34:52 -06:00
Wyatt Johnson d38dbc87b6 simplified test layout 2017-08-19 10:22:26 -06:00
Wyatt Johnson a7a80a452b removed old deps + outdated e2e 2017-08-19 09:59:46 -06:00
Wyatt Johnson 6c19b90652 Merge branch 'master' into user-sort 2017-08-18 09:14:19 -06:00
Wyatt Johnson 686eb614f7 changed cursor to Cursor scalar type, changed SORT_ORDER 2017-08-17 16:43:55 -06:00
Wyatt Johnson a9abd55895 added group_id support to action counts, cli verifier 2017-08-17 15:32:47 -06:00
Wyatt Johnson 165d07cae9 improved event layout, added db indicies 2017-08-17 11:56:58 -06:00
Wyatt Johnson d4536cb1a2 replaced emit with emitAsync 2017-08-16 16:01:22 -06:00
Wyatt Johnson b126cc3cb3 initial impl of cached action counts using ee 2017-08-16 15:47:56 -06:00
415 changed files with 8770 additions and 8263 deletions
+2 -1
View File
@@ -6,7 +6,7 @@ node_modules
scripts
!scripts/generateIntrospectionResult.js
# documentation should not be visable in production.
# documentation should not be visible in production.
docs
# static assets are rebuild in the docker container.
@@ -14,6 +14,7 @@ dist
# tests are not run in the docker container.
test
__tests__
# we won't use the .git folder in production.
.git
+9 -1
View File
@@ -1,6 +1,5 @@
dist
docs
client/lib
**/*.html
plugins/*
!plugins/talk-plugin-facebook-auth
@@ -14,5 +13,14 @@ plugins/*
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-sort-newest
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-sort-most-replied
!plugins/talk-plugin-sort-most-liked
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
node_modules
+35 -26
View File
@@ -7,51 +7,53 @@
"parserOptions": {
"ecmaVersion": 2017
},
"plugins": [
"promise",
"json"
],
"rules": {
"indent": ["error",
2
],
"no-console": [
0
],
"no-console": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-template-curly-in-string": [1],
"no-unsafe-negation": [1],
"array-callback-return": [1],
"no-template-curly-in-string": "warn",
"no-unsafe-negation": "warn",
"array-callback-return": "warn",
"arrow-parens": ["warn", "always"],
"template-curly-spacing": "warn",
"eqeqeq": [2, "smart"],
"no-eval": [2],
"no-global-assign": [2],
"no-implied-eval": [2],
"eqeqeq": ["error", "smart"],
"no-eval": "error",
"no-global-assign": "error",
"no-implied-eval": "error",
"lines-around-comment": ["warn", {"beforeLineComment": true}],
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
"no-script-url": [2],
"no-throw-literal": [2],
"yoda": [1],
"no-path-concat": [2],
"eol-last": [1],
"no-nested-ternary": [1],
"no-tabs": [2],
"no-unneeded-ternary": [1],
"object-curly-spacing": [1],
"no-script-url": "error",
"no-throw-literal": "error",
"yoda": "warn",
"no-path-concat": "error",
"eol-last": "warn",
"no-nested-ternary": "warn",
"no-tabs": "error",
"no-unneeded-ternary": "warn",
"object-curly-spacing": "warn",
"space-infix-ops": ["error"],
"space-in-parens": ["error", "never"],
"space-unary-ops": ["error", {
"words": true,
"nonwords": false
}],
"no-const-assign": [2],
"no-duplicate-imports": [2],
"prefer-template": [1],
"no-const-assign": "error",
"no-duplicate-imports": "error",
"prefer-template": "warn",
"comma-spacing": ["error", {
"after": true
}],
"no-var": [2],
"no-lonely-if": [2],
"curly": [2],
"no-var": "error",
"no-lonely-if": "error",
"curly": "error",
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_|next",
"varsIgnorePattern": "^_"
@@ -61,6 +63,13 @@
}],
"newline-per-chained-call": ["error", {
"ignoreChainWithDepth": 2
}]
}],
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn"
}
}
+9 -1
View File
@@ -14,7 +14,6 @@ client/coral-framework/graphql/introspection.json
*.swp
*.DS_STORE
test/e2e/reports
coverage/
plugins.json
@@ -30,5 +29,14 @@ plugins/*
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-sort-newest
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-sort-most-replied
!plugins/talk-plugin-sort-most-liked
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
**/node_modules/*
+4 -4
View File
@@ -1,4 +1,4 @@
FROM node:8
FROM node:8-alpine
# Create app directory
RUN mkdir -p /usr/src/app
@@ -12,6 +12,9 @@ EXPOSE 5000
# Bundle app source
COPY . /usr/src/app
# Ensure the runtime of the container is in production mode.
ENV NODE_ENV production
# Install app dependencies and build static assets.
RUN yarn global add node-gyp && \
yarn install --frozen-lockfile && \
@@ -19,7 +22,4 @@ RUN yarn global add node-gyp && \
yarn build && \
yarn cache clean
# Ensure the runtime of the container is in production mode.
ENV NODE_ENV production
CMD ["yarn", "start"]
+2 -5
View File
@@ -7,8 +7,5 @@ ONBUILD COPY . /usr/src/app
# we need to have webpack available. We then build the new dependancies and
# clear out the development dependancies again. After this we of course need to
# clear out the yarn cache, this saves quite a lot of size.
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
NODE_ENV=production cli plugins reconcile && \
NODE_ENV=production yarn build && \
NODE_ENV=production yarn install --production --force && \
yarn cache clean
ONBUILD RUN cli plugins reconcile && \
yarn build
+2 -1
View File
@@ -1,4 +1,5 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
@@ -19,7 +20,7 @@ endpoint when the server is running with built assets.
- Blog: https://blog.coralproject.net
- Community Guides for Journalism: https://guides.coralproject.net/
- Community Guides for Journalism: https://guides.coralproject.net/
## License
+9 -5
View File
@@ -1,10 +1,15 @@
{
"name": "The Coral Project: Talk",
"env": {
"TALK_SESSION_SECRET": {
"TALK_JWT_SECRET": {
"description": "The session secret",
"generator": "secret"
},
"TALK_ROOT_URL": {
"description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.",
"value":"https://<COPY APP NAME HERE>.herokuapp.com",
"required": true
},
"TALK_FACEBOOK_APP_ID": {
"value": "",
"required": true
@@ -14,8 +19,7 @@
"required": true
},
"NODE_ENV": "production",
"TALK_SMTP_PORT": "2525",
"REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN",
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
"NPM_CONFIG_PRODUCTION": "false"
},
"addons": [{
@@ -25,8 +29,8 @@
"plan": "rediscloud:30",
"as": "REDIS"
}, {
"plan": "postmark:10k",
"as": "POSTMARK"
"plan": "mailgun:starter",
"as": "MAILGUN"
}],
"image": "heroku/nodejs",
"success_url": "/admin/install"
+20 -1
View File
@@ -4,8 +4,9 @@
* Module dependencies.
*/
// const util = require('./util');
const program = require('./commander');
const {head, map} = require('lodash');
const Matcher = require('did-you-mean');
program
.command('serve', 'serve the application')
@@ -16,12 +17,28 @@ program
.command('token', 'work with the access tokens')
.command('users', 'work with the application auth')
.command('migration', 'provides utilities for migrating the database')
.command('verify', 'provides utilities for performing data verification')
.command(
'plugins',
'provides utilities for interacting with the plugin system'
)
.parse(process.argv);
// If the command wasn't found, output help.
const cmds = map(program.commands, '_name');
const cmd = head(program.args);
if (!cmds.includes(cmd)) {
const m = new Matcher(cmds);
const similarCMDs = m.list(cmd);
console.error(`cli '${cmd}' is not a talk cli command. See 'cli --help'.`);
if (similarCMDs.length > 0) {
const sc = similarCMDs.map(({value}) => `\t${value}\n`).join('');
console.error(`\nThe most similar commands are\n${sc}`);
}
process.exit(1);
}
/**
* When this provess exists, check to see if we have a running command, if we do
* check to see if it is still running. If it is, then kill it with a SIGINT
@@ -30,6 +47,8 @@ program
*/
process.once('exit', () => {
if (
// program.runningCommand &&
program.runningCommand.killed === false &&
program.runningCommand.exitCode === null
) {
+13 -8
View File
@@ -78,18 +78,23 @@ function rangeJobsByState(state = 'complete', limit) {
/**
* Cleans up the jobs that are in the queue.
*/
function cleanupJobs(options) {
async function cleanupJobs(options) {
const n = 100;
Promise.all([
rangeJobsByState('complete', n),
options.stuck ? rangeJobsByState('failed', n) : false
])
.then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs))
.then(() => {
try {
const joblists = await Promise.all([
rangeJobsByState('complete', n),
options.stuck ? rangeJobsByState('failed', n) : false
]);
await joblists.filter((jobs) => jobs).map(removeJobs);
util.shutdown();
console.log('Removed old jobs');
});
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
//==============================================================================
+1 -1
View File
@@ -53,7 +53,7 @@ function versionMatch(name, version) {
}
}
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
function reconcilePackages({quiet = false, upgradeRemote = false}) {
const fetchable = [];
+38 -45
View File
@@ -98,39 +98,32 @@ function getUserCreateAnswers(options) {
/**
* Prompts for input and registers a user based on those.
*/
function createUser(options) {
getUserCreateAnswers(options)
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
async function createUser(options) {
try {
const answers = await getUserCreateAnswers(options);
if (answers.password !== answers.confirmPassword) {
throw new Error('Passwords do not match');
}
return answers;
})
.then((answers) => {
return UsersService
.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim())
.then((user) => {
console.log(`Created user ${user.id}.`);
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
console.log(`Created user ${user.id}.`);
if (answers.roles.length > 0) {
return Promise.all(answers.roles.map((role) => {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the role ${role} to User ${user.id}.`);
});
}));
}
});
})
.then(() => {
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown();
});
if (answers.roles.length > 0) {
return Promise.all(answers.roles.map((role) => {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the role ${role} to User ${user.id}.`);
});
}));
}
util.shutdown();
} catch (err) {
console.error(err);
util.shutdown();
}
}
/**
@@ -169,21 +162,21 @@ function passwd(userID) {
validate: validateRequired('Confirm Password is required')
}
])
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
return Promise.reject(new Error('Password mismatch'));
}
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
throw new Error('Password mismatch');
}
return UsersService.changePassword(userID, answers.password);
})
.then(() => {
console.log('Password changed.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
return UsersService.changePassword(userID, answers.password);
})
.then(() => {
console.log('Password changed.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
Executable
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const program = require('./commander');
const mongoose = require('../services/mongoose');
const util = require('./util');
const databaseVerifications = require('./verifications/database');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
async function database({fix = false, limit = Infinity, batch = 1000}) {
try {
for (const verification of databaseVerifications) {
await verification({fix, limit, batch});
}
} catch (err) {
console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err);
util.shutdown(1);
return;
}
util.shutdown();
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('db')
.description('verifies the database integrity')
.option('-f, --fix', 'fix the problems found with database inconsistencies')
.option('-l, --limit [size]', 'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]', parseInt)
.option('-b, --batch [size]', 'batch size to process verifications and repairs of documents [default: 1000]', parseInt)
.action(database);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
util.shutdown();
}
+193
View File
@@ -0,0 +1,193 @@
const CommentModel = require('../../../models/comment');
const ActionsService = require('../../../services/actions');
const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util');
const sc = require('snake-case');
const debug = require('debug')('talk:cli:verify');
const getBatch = async (limit, offset) => CommentModel
.find({})
.select({'id': 1, 'action_counts': 1, 'reply_count': 1})
.limit(limit)
.skip(offset)
.sort('created_at');
module.exports = async ({fix, limit, batch}) => {
let operations = [];
// Count how many comments there are to process.
const totalCount = await CommentModel.count();
let offset = 0;
let comments = [];
let commentIDs = [];
console.log(`Processing ${totalCount} comments in batches of ${limit}...`);
// Keep processing documents until there are is none left.
while (offset < totalCount) {
// Get a batch of comments.
comments = await getBatch(batch, offset);
commentIDs = comments.map(({id}) => id);
// Get their reply counts.
let allReplyCounts = await CommentModel
.aggregate([
{
$match: {
parent_id: {
$in: commentIDs,
},
status: {
$in: ['NONE', 'ACCEPTED']
}
}
},
{
$group: {
_id: '$parent_id',
count: {
$sum: 1
}
}
}
])
.then(singleJoinBy(commentIDs, '_id'))
.then((results) => results.map((result) => result ? result.count : 0));
// Get their action summaries.
let allActionSummaries = await ActionsService
.getActionSummaries(commentIDs)
.then(arrayJoinBy(commentIDs, 'item_id'));
// Loop over the comments, with their action summaries.
for (let i = 0; i < comments.length; i++) {
let comment = comments[i];
let actionSummaries = allActionSummaries[i];
let replyCount = allReplyCounts[i];
// And check to see if the action summaries we just computed match what is
// currently set for the comments.
let commentOperations = [];
// If the reply count needs to be updated, then update it!
if (comment.reply_count !== replyCount) {
commentOperations.push({
reply_count: replyCount,
});
}
// First we process all the group id's.
for (let actionSummary of actionSummaries) {
if (actionSummary.group_id === null) {
continue;
}
// And we generate the group id.
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
const GROUP_ID = sc(actionSummary.group_id.toLowerCase());
if (GROUP_ID.length <= 0) {
continue;
}
// And we add a new batch operation if the action summary is associated
// with a group.
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
// Check that the action summaries match the cached counts.
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) {
// Batch updates for those changes.
commentOperations.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
});
}
}
// Group all the action summaries together from all the different group
// ids.
let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
if (!(ACTION_TYPE in acc)) {
acc[ACTION_TYPE] = 0;
}
acc[ACTION_TYPE] += actionSummary.count;
return acc;
}, {});
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
// Check that the action summaries match the cached counts.
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
// Batch updates for those changes.
commentOperations.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
});
}
}
// If this comment has action summaries that should be updated, then
// perform an update!
if (commentOperations.length > 0) {
operations.push({
updateOne: {
filter: {
id: comment.id
},
update: {
$set: Object.assign({}, ...commentOperations),
},
},
});
}
}
debug(`Processed batch of ${comments.length} comments.`);
if (operations.length >= limit) {
debug(`Queued operations are ${operations.length}, reached limit of ${limit}, not processing any more.`);
if (operations.length > limit) {
debug(`${operations.length - limit} operations have been truncated to enforce the limit`);
}
break;
}
offset += batch;
}
const OPERATIONS_LENGTH = operations.length;
if (limit < Infinity && offset + comments.length < totalCount) {
console.log(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`);
} else {
console.log(`Processed all ${totalCount} comments.`);
}
console.log(`${OPERATIONS_LENGTH} documents need fixing.`);
// If fix was enabled, execute the batch writes.
if (OPERATIONS_LENGTH > 0) {
if (fix) {
debug(`Fixing ${OPERATIONS_LENGTH} documents...`);
while (operations.length) {
let batchOperations = operations.splice(0, batch);
let result = await CommentModel.collection.bulkWrite(batchOperations);
debug(`Fixed batch of ${result.modifiedCount} documents.`);
}
console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`);
} else {
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
}
}
};
+12
View File
@@ -0,0 +1,12 @@
// This will import all the verifications that should be run by the:
//
// cli verify database
//
// command. They exist in the form:
//
// async ({fix = false, batch = 1000}) => {}
//
// where their options are derrived.
module.exports = [
require('./comments'),
];
-2
View File
@@ -40,8 +40,6 @@ test:
override:
# Run the tests using the junit reporter.
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
# Run the e2e test suite.
# - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e yarn e2e
deployment:
release:
+2 -1
View File
@@ -9,6 +9,7 @@
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
"transform-react-jsx",
"syntax-dynamic-import"
]
}
+1
View File
@@ -4,6 +4,7 @@
"es6": true,
"mocha": true
},
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
+10 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import {history} from 'coral-framework/helpers/router';
import PropTypes from 'prop-types';
import Configure from 'routes/Configure';
import Dashboard from 'routes/Dashboard';
@@ -47,6 +47,14 @@ const routes = (
</div>
);
const AppRouter = () => <Router history={history} routes={routes}/>;
class AppRouter extends React.Component {
static contextTypes = {
history: PropTypes.object,
};
render() {
return <Router history={this.context.history} routes={routes} />;
}
}
export default AppRouter;
+4 -5
View File
@@ -8,7 +8,6 @@ import {
UPDATE_ASSETS
} from '../constants/assets';
import coralApi from '../../../coral-framework/helpers/request';
import t from 'coral-framework/services/i18n';
/**
@@ -17,9 +16,9 @@ import t from 'coral-framework/services/i18n';
// Fetch a page of assets
// Get comments to fill each of the three lists on the mod queue
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => {
dispatch({type: FETCH_ASSETS_REQUEST});
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
.then(({result, count}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
assets: result,
@@ -34,9 +33,9 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch) => {
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch((error) => {
console.error(error);
+43 -16
View File
@@ -1,16 +1,13 @@
import bowser from 'bowser';
import * as actions from '../constants/auth';
import coralApi from 'coral-framework/helpers/request';
import * as Storage from 'coral-framework/helpers/storage';
import {handleAuthToken} from 'coral-framework/actions/auth';
import {resetWebsocket} from 'coral-framework/services/client';
import t from 'coral-framework/services/i18n';
import jwtDecode from 'jwt-decode';
//==============================================================================
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
dispatch({type: actions.LOGIN_REQUEST});
const params = {
@@ -27,18 +24,18 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
};
}
return coralApi('/auth/local', params)
return rest('/auth/local', params)
.then(({user, token}) => {
if (!user) {
if (!bowser.safari && !bowser.ios) {
Storage.removeItem('token');
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
}
return dispatch(checkLoginFailure('not logged in'));
}
dispatch(handleAuthToken(token));
resetWebsocket();
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
@@ -84,11 +81,11 @@ const forgotPasswordFailure = (error) => ({
error,
});
export const requestPasswordReset = (email) => (dispatch) => {
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
.then(() => dispatch(forgotPasswordSuccess()))
.catch((error) => {
console.error(error);
@@ -116,18 +113,18 @@ const checkLoginFailure = (error) => ({
error
});
export const checkLogin = () => (dispatch) => {
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
dispatch(checkLoginRequest());
return coralApi('/auth')
return rest('/auth')
.then(({user}) => {
if (!user) {
if (!bowser.safari && !bowser.ios) {
Storage.removeItem('token');
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
}
return dispatch(checkLoginFailure('not logged in'));
}
resetWebsocket();
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
@@ -136,3 +133,33 @@ export const checkLogin = () => (dispatch) => {
dispatch(checkLoginFailure(errorMessage));
});
};
//==============================================================================
// LOGOUT
//==============================================================================
export const logout = () => (dispatch, _, {rest, client, storage}) => {
return rest('/auth', {method: 'DELETE'}).then(() => {
if (storage) {
storage.removeItem('token');
}
// Reset the websocket.
client.resetWebsocket();
dispatch({type: actions.LOGOUT});
});
};
//==============================================================================
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
if (storage) {
storage.setItem('exp', jwtDecode(token).exp);
storage.setItem('token', token);
}
dispatch({type: 'HANDLE_AUTH_TOKEN'});
};
+12 -13
View File
@@ -14,13 +14,12 @@ import {
HIDE_REJECT_USERNAME_DIALOG
} from '../constants/community';
import coralApi from '../../../coral-framework/helpers/request';
import t from 'coral-framework/services/i18n';
export const fetchAccounts = (query = {}) => (dispatch) => {
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
dispatch(requestFetchAccounts());
coralApi(`/users?${qs.stringify(query)}`)
rest(`/users?${qs.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>{
dispatch({
type: FETCH_COMMENTERS_SUCCESS,
@@ -51,18 +50,18 @@ export const newPage = () => ({
type: COMMENTERS_NEW_PAGE
});
export const setRole = (id, role) => (dispatch) => {
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
export const setRole = (id, role) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
};
export const setCommenterStatus = (id, status) => (dispatch) => {
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
// Ban User Dialog
+14 -15
View File
@@ -1,4 +1,3 @@
import coralApi from 'coral-framework/helpers/request';
import * as actions from '../constants/install';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
@@ -27,19 +26,19 @@ const validation = (formData, dispatch, next) => {
// Required Validation
const empty = validKeys
.filter((name) => {
const cond = !formData[name].length;
.filter((name) => {
const cond = !formData[name].length;
if (cond) {
if (cond) {
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (empty.length) {
dispatch(hasError());
@@ -106,10 +105,10 @@ export const submitUser = () => (dispatch, getState) => {
});
};
export const finishInstall = () => (dispatch, getState) => {
export const finishInstall = () => (dispatch, getState, {rest}) => {
const data = getState().install.data;
dispatch(installRequest());
return coralApi('/setup', {method: 'POST', body: data})
return rest('/setup', {method: 'POST', body: data})
.then(() => {
dispatch(installSuccess());
dispatch(nextStep());
@@ -129,9 +128,9 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
export const checkInstall = (next) => (dispatch) => {
export const checkInstall = (next) => (dispatch, _, {rest}) => {
dispatch(checkInstallRequest());
coralApi('/setup')
rest('/setup')
.then(({installed}) => {
dispatch(checkInstallSuccess(installed));
if (installed) {
+5 -3
View File
@@ -4,15 +4,17 @@ export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
// hide shortcuts note
export const hideShortcutsNote = () => {
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
try {
window.localStorage.setItem('coral:shortcutsNote', 'hide');
if (storage) {
storage.setItem('coral:shortcutsNote', 'hide');
}
} catch (e) {
// above will fail in Safari private mode
}
return {type: actions.HIDE_SHORTCUTS_NOTE};
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
};
export const setSortOrder = (order) => ({
+4 -5
View File
@@ -1,4 +1,3 @@
import coralApi from '../../../coral-framework/helpers/request';
import t from 'coral-framework/services/i18n';
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
@@ -14,9 +13,9 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
export const fetchSettings = () => (dispatch) => {
export const fetchSettings = () => (dispatch, _, {rest}) => {
dispatch({type: SETTINGS_LOADING});
coralApi('/settings')
rest('/settings')
.then((settings) => {
dispatch({type: SETTINGS_RECEIVED, settings});
})
@@ -41,13 +40,13 @@ export const updateDomainlist = (listName, list) => {
return {type: DOMAINLIST_UPDATED, listName, list};
};
export const saveSettingsToServer = () => (dispatch, getState) => {
export const saveSettingsToServer = () => (dispatch, getState, {rest}) => {
let settings = getState().settings;
if (settings.charCount) {
settings.charCount = parseInt(settings.charCount);
}
dispatch({type: SAVE_SETTINGS_LOADING});
coralApi('/settings', {method: 'PUT', body: settings})
rest('/settings', {method: 'PUT', body: settings})
.then(() => {
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
})
+6 -7
View File
@@ -1,4 +1,3 @@
import coralApi from '../../../coral-framework/helpers/request';
import * as userTypes from '../constants/users';
import t from 'coral-framework/services/i18n';
@@ -7,9 +6,9 @@ import t from 'coral-framework/services/i18n';
*/
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch) => {
return (dispatch, _, {rest}) => {
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch((error) => {
console.error(error);
@@ -21,8 +20,8 @@ export const userStatusUpdate = (status, userId, commentId) => {
// change status of a user
export const sendNotificationEmail = (userId, subject, body) => {
return (dispatch) => {
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
@@ -33,8 +32,8 @@ export const sendNotificationEmail = (userId, subject, body) => {
// let a user edit their username
export const enableUsernameEdit = (userId) => {
return (dispatch) => {
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/username-enable`, {method: 'POST'})
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
+14 -14
View File
@@ -69,23 +69,23 @@ class AdminLogin extends React.Component {
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
location.href = location.href;
}}>
? <p className={styles.passwordRequestSuccess} onClick={() => {
location.href = location.href;
}}>
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
<Success />
</p>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
);
return (
<Layout fixedDrawer restricted={true}>
@@ -17,6 +17,7 @@ export default ({suspectWords, bannedWords, body, ...rest}) => {
return (
<Highlighter
{...rest}
autoEscape={true}
searchWords={searchWords}
textToHighlight={body}
/>
@@ -3,12 +3,11 @@
color: white;
background: grey;
box-sizing: border-box;
padding: 0px 5px;
border-radius: 2px;
padding: 2px 5px;
font-size: 12px;
height: 24px;
letter-spacing: 0.4px;
margin-bottom: 1px;
line-height: 22px;
> i {
font-size: 14px;
@@ -0,0 +1,11 @@
import React from 'react';
import {Badge} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ReplyBadge = () => (
<Badge icon="reply">
{t('modqueue.reply')}
</Badge>
);
export default ReplyBadge;
@@ -65,7 +65,7 @@ class SuspendUserDialog extends React.Component {
{t('suspenduser.title_suspend')}
</h1>
<p className={styles.description}>
{t('suspenduser.description_suspend', username)}
{t('suspenduser.description_suspend', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
@@ -103,7 +103,7 @@ class SuspendUserDialog extends React.Component {
{t('suspenduser.title_notify')}
</h1>
<p className={styles.description}>
{t('suspenduser.description_notify', username)}
{t('suspenduser.description_notify', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
+24 -24
View File
@@ -139,29 +139,29 @@ export default class UserDetail extends React.Component {
<hr/>
{
selectedCommentIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<Button
onClick={bulkAccept}
className={styles.bulkAction}
cStyle='approve'
icon='done'>
</Button>
<Button
onClick={bulkReject}
className={styles.bulkAction}
cStyle='reject'
icon='close'>
</Button>
{`${selectedCommentIds.length} comments selected`}
</div>
)
? (
<ul className={styles.commentStatuses}>
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<Button
onClick={bulkAccept}
className={styles.bulkAction}
cStyle='approve'
icon='done'>
</Button>
<Button
onClick={bulkReject}
className={styles.bulkAction}
cStyle='reject'
icon='close'>
</Button>
{`${selectedCommentIds.length} comments selected`}
</div>
)
}
<div>
@@ -190,7 +190,7 @@ export default class UserDetail extends React.Component {
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
/>
</Drawer>
</ClickOutside>
);
@@ -55,7 +55,7 @@
position: relative;
}
.commentType {
.badgeBar {
position: absolute;
right: 0px;
}
@@ -3,6 +3,7 @@ import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
import ReplyBadge from './ReplyBadge';
import styles from './UserDetailComment.css';
import CommentType from './CommentType';
import {getActionSummary} from 'coral-framework/utils';
@@ -53,10 +54,14 @@ class UserDetailComment extends React.Component {
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
<CommentType type={commentType} className={styles.commentType}/>
<div className={styles.badgeBar}>
{comment.hasParent && <ReplyBadge/>}
<CommentType type={commentType}/>
</div>
</div>
<div className={styles.story}>
Story: {comment.asset.title}
@@ -116,10 +121,10 @@ class UserDetailComment extends React.Component {
</div>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
);
+30 -30
View File
@@ -13,9 +13,9 @@ const CoralHeader = ({
}) => (
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
<IndexLink
id='dashboardNav'
@@ -63,36 +63,36 @@ const CoralHeader = ({
</Navigation>
:
null
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank">
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank">
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</div>
</Header>
);
+5 -6
View File
@@ -3,12 +3,11 @@ import {connect} from 'react-redux';
import Layout from '../components/ui/Layout';
import {fetchConfig} from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import {logout} from 'coral-framework/actions/auth';
import {FullLoading} from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
@@ -56,10 +55,10 @@ class LayoutContainer extends Component {
toggleShortcutModal={toggleShortcutModal}
{...this.props}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{this.props.children}
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{this.props.children}
</Layout>
);
} else if (loggedIn) {
@@ -5,22 +5,24 @@ import SuspendUserDialog from '../components/SuspendUserDialog';
import {hideSuspendUserDialog} from '../actions/suspendUserDialog';
import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations';
import {compose, gql} from 'react-apollo';
import * as notification from 'coral-admin/src/services/notification';
import t, {timeago} from 'coral-framework/services/i18n';
import withQuery from 'coral-framework/hocs/withQuery';
import {getErrorMessages} from 'coral-framework/utils';
import get from 'lodash/get';
import {notify} from 'coral-framework/actions/notification';
class SuspendUserDialogContainer extends Component {
suspendUser = async ({message, until}) => {
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser} = this.props;
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
hideSuspendUserDialog();
try {
const result = await suspendUser({id: userId, message, until});
if (result.data.suspendUser.errors) {
throw result.data.suspendUser.errors;
}
notification.success(
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until)),
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
@@ -33,7 +35,7 @@ class SuspendUserDialogContainer extends Component {
}
}
catch(err) {
notification.showMutationErrors(err);
notify('error', getErrorMessages(err));
}
};
@@ -52,8 +54,9 @@ class SuspendUserDialogContainer extends Component {
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
settings {
organizationName
organizationName
}
}
`);
@@ -69,6 +72,7 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideSuspendUserDialog,
notify,
}, dispatch),
});
@@ -88,13 +88,13 @@ class UserDetailContainer extends React.Component {
});
}
})
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
this.isLoadingMore = false;
throw err;
});
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
this.isLoadingMore = false;
throw err;
});
};
componentWillReceiveProps(next) {
@@ -124,7 +124,7 @@ class UserDetailContainer extends React.Component {
}
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
...CoralAdmin_Moderation_CommentConnection
}
@@ -9,6 +9,7 @@ export default withFragments({
body
created_at
status
hasParent
asset {
id
title
+27 -7
View File
@@ -1,14 +1,34 @@
import {add} from 'coral-framework/services/graphqlRegistry';
import update from 'immutability-helper';
const extension = {
export default {
mutations: {
SetUserStatus: () => ({
refetchQueries: ['CoralAdmin_Community'],
SetUserStatus: ({variables: {status, userId}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
if (status !== 'APPROVED') {
return prev;
}
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
}
}),
RejectUsername: () => ({
refetchQueries: ['CoralAdmin_Community'],
RejectUsername: ({variables: {input: {id: userId}}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
}
}),
},
};
add(extension);
+22 -20
View File
@@ -1,32 +1,34 @@
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 'coral-framework/services/client';
import store from './services/store';
import TalkProvider from 'coral-framework/components/TalkProvider';
import {createContext} from 'coral-framework/services/bootstrap';
import reducers from './reducers';
import App from './components/App';
import 'react-mdl/extra/material.js';
import './graphql';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
import graphqlExtension from './graphql';
import pluginsConfig from 'pluginsConfig';
import {toast} from 'react-toastify';
import {createNotificationService} from './services/notification';
import {hideShortcutsNote} from './actions/moderation';
const eventEmitter = new EventEmitter();
function hidrateStore({store, storage}) {
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
store.dispatch(hideShortcutsNote());
}
}
// TODO: pass redux actions through the emitter.
loadPluginsTranslations();
injectPluginsReducers();
smoothscroll.polyfill();
const notification = createNotificationService(toast);
const context = createContext({reducers, graphqlExtension, pluginsConfig, notification});
// hidrate Store with external data.
hidrateStore(context);
render(
<EventEmitterProvider eventEmitter={eventEmitter}>
<ApolloProvider client={getClient()} store={store}>
<App />
</ApolloProvider>
</EventEmitterProvider>
<TalkProvider {...context}>
<App />
</TalkProvider>
, document.querySelector('#root')
);
@@ -5,14 +5,17 @@ const initialState = {
modalOpen: false,
storySearchVisible: false,
storySearchString: '',
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
sortOrder: 'REVERSE_CHRONOLOGICAL',
shortcutsNoteVisible: 'show',
sortOrder: 'DESC',
};
export default function moderation (state = initialState, action) {
switch (action.type) {
case actions.MODERATION_CLEAR_STATE:
return initialState;
return {
...initialState,
shortcutsNoteVisible: state.shortcutsNoteVisible,
};
case actions.TOGGLE_MODAL:
return {
...state,
@@ -0,0 +1,4 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
@@ -1,10 +1,11 @@
import React from 'react';
import styles from './Community.css';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
// TODO: Needs refactoring.
const ActionButton = ({type = '', user, ...props}) => {
return (
<Button
@@ -1,299 +0,0 @@
@custom-media --big-viewport (min-width: 780px);
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.mainFlaggedContent {
width: 100%;
padding: 34px 14px;
box-sizing: border-box;
}
.roleButton {
display: block;
}
.searchBox {
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
i {
color: #A1A1A1
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
.list {
padding: 8px 0;
list-style: none;
display: block;
&.singleView .listItem {
display: none;
}
&.singleView .listItem.activeItem {
display: block;
height: 100%;
font-size: 1.5em;
line-height: 1.5em;
border: none;
.actions {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
margin: 0;
}
.actionButton {
transform: scale(1.4);
}
}
}
.listItem {
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: all 200ms;
padding: 10px 0 0;
min-height: 220px;
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
&:last-child {
border-bottom: none;
}
.context {
a {
color: #f36451;
text-decoration: underline;
float: right;
}
}
.sideActions {
position: absolute;
right: 0;
height: 100%;
top: 0;
padding: 50px 18px;
box-sizing: border-box;
}
.itemHeader {
display: flex;
align-items: center;
justify-content: space-between;
.author {
font-size: 16px;
font-weight: bold;
min-width: 230px;
display: flex;
align-items: center;
}
}
.itemBody {
display: flex;
justify-content: space-between;
}
.avatar {
margin-right: 16px;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: #757575;
font-size: 40px;
color: #fff;
}
.created {
color: #666;
font-size: 13px;
margin-left: 40px;
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
}
.flagged {
color: rgba(255, 0, 0, .5);
padding-top: 15px;
padding-left: 10px;
}
.flagCount{
font-size: 12px;
color: #d32f2f;
}
}
.empty {
color: #444;
margin-top: 50px;
text-align: center;
}
@media (--big-viewport) {
.listItem {
border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
&.activeItem {
border: 2px solid #333;
}
}
}
.hasLinks {
color: #f00;
text-align: right;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.banned {
color: #f00;
text-align: left;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.ban {
display: block;
text-align: center;
margin-top: 5px;
}
.banButton {
width: 114px;
letter-spacing: 1px;
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
}
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.flaggedByCount {
display: block;
text-align: left;
margin-top: 5px;
}
.flaggedByCount i {
font-size: 14px;
margin-right: 10px;
}
.flaggedBy {
display: inline;
padding: 3px;
font-size: 14px;
margin-left: 5px;
}
.flaggedByLabel {
font-weight: 600;
font-size: 14px;
}
.flaggedReasons {
margin-left: 24px;
margin-top: 10px;
}
p.flaggedByReason {
font-size: 1tpx;
margin: 0px;
line-height: 1.4;
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
vertical-align: text-bottom;
&:hover {
background-color: #E0E0E0;
}
}
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import CommunityMenu from './CommunityMenu';
import People from './People';
import FlaggedAccounts from './FlaggedAccounts';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import RejectUsernameDialog from './RejectUsernameDialog';
export default class Community extends Component {
@@ -54,7 +54,7 @@ export default class Community extends Component {
}
getTabContent(searchValue, props) {
const {community, root: {users}, viewUserDetail} = props;
const {community} = props;
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
if (activeTab === 'people') {
@@ -76,16 +76,7 @@ export default class Community extends Component {
return (
<div>
<FlaggedAccounts
commenters={users}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
showRejectUsernameDialog={props.showRejectUsernameDialog}
approveUser={props.approveUser}
rejectUsername={props.rejectUsername}
currentUser={this.props.currentUser}
viewUserDetail={viewUserDetail}
/>
<FlaggedAccounts />
<RejectUsernameDialog
open={community.rejectUsernameDialog}
handleClose={props.hideRejectUsernameDialog}
@@ -0,0 +1,21 @@
.tab {
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: border-bottom 200ms;
}
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 5px #F36451;
}
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './styles.css';
import styles from './CommunityMenu.css';
import t from 'coral-framework/services/i18n';
import {Link} from 'react-router';
@@ -0,0 +1,34 @@
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.mainFlaggedContent {
width: 100%;
padding: 34px 14px;
box-sizing: border-box;
}
.list {
margin: 0;
padding: 0;
}
.userLeave {
opacity: 1.0;
}
.userLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.userEnter {
opacity: 0;
}
.userEnterActive {
opacity: 1.0;
transition: opacity 800ms;
}
@@ -1,38 +1,74 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import styles from './Community.css';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import User from './User';
import LoadMore from '../../../components/LoadMore';
import FlaggedUser from '../containers/FlaggedUser';
import {CSSTransitionGroup} from 'react-transition-group';
import styles from './FlaggedAccounts.css';
const FlaggedAccounts = (props) => {
const {commenters} = props;
const hasResults = commenters && !!commenters.length;
class FlaggedAccounts extends React.Component {
render() {
const {
users,
loadMore,
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
approveUser,
me,
viewUserDetail,
} = this.props;
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{
hasResults
? commenters.map((commenter, index) => {
return <User
user={commenter}
key={index}
index={index}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
showRejectUsernameDialog={props.showRejectUsernameDialog}
approveUser={props.approveUser}
currentUser={props.currentUser}
viewUserDetail={props.viewUserDetail}
/>;
})
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{
hasResults
? <CSSTransitionGroup
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.userEnter,
enterActive: styles.userEnterActive,
leave: styles.userLeave,
leaveActive: styles.userLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{
users.nodes.map((user) => {
return (
<FlaggedUser
user={user}
key={user.id}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
viewUserDetail={viewUserDetail}
/>
);
})
}
</CSSTransitionGroup>
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
<LoadMore
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</div>
</div>
</div>
);
};
);
}
}
export default FlaggedAccounts;
@@ -0,0 +1,104 @@
@custom-media --big-viewport (min-width: 780px);
.root {
composes: mdl-shadow--2dp mdl-card from global;
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: box-shadow 200ms, margin-bottom 200ms;
padding: 10px 0 0;
min-height: 220px;
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
}
.rootSelected {
composes: mdl-shadow--16dp from global;
}
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.author {
display: flex;
justify-content: flex-start;
}
.body {
display: flex;
align-items: center;
justify-content: space-between;
}
.flaggedByCount {
display: block;
text-align: left;
margin-top: 5px;
}
.flagIcon {
font-size: 14px;
margin-right: 10px;
}
.flaggedBy {
display: inline;
padding: 3px;
font-size: 14px;
margin-left: 5px;
}
.flaggedByLabel {
font-weight: 600;
font-size: 14px;
}
.flaggedReasons {
margin-left: 24px;
margin-top: 10px;
}
.flaggedByReason {
font-size: 1tpx;
margin: 0px;
line-height: 1.4;
}
@media (--big-viewport) {
.root {
border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
}
&.rootSelected {
border: 2px solid #333;
}
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
vertical-align: text-bottom;
&:hover {
background-color: #E0E0E0;
}
}
@@ -0,0 +1,145 @@
import React from 'react';
import styles from './FlaggedUser.css';
import ActionButton from './ActionButton';
// TODO: Should not rely on plugin.
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 cn from 'classnames';
import t from 'coral-framework/services/i18n';
// TODO: Should work with custom flags too.
const shortReasons = {
[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
class User extends React.Component {
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
render() {
const {
user,
modActionButtons,
viewUserDetail,
selected,
approveUser,
showRejectUsernameDialog,
me,
className,
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
className={styles.button}>
{user.username}
</button>
{me.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
</div>
</div>
<div className={styles.body}>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
<span className={styles.flaggedByLabel}>
{t('community.flags')}({ user.actions.length })
</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user &&
<button onClick={() => {viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
}
return null;
}
)}
</div>;
}
)}
</div>
</div>
<div className={styles.sideActions}>
<div className={styles.actions}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={approveUser}
showRejectUsernameDialog={showRejectUsernameDialog}
/>
)}
</div>
</div>
</div>
</div>
</li>
);
}
}
export default User;
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './Community.css';
import styles from './styles.css';
import Table from '../containers/Table';
import {Pager, Icon} from 'coral-ui';
import EmptyCard from '../../../components/EmptyCard';
@@ -45,12 +45,12 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
<div className={styles.mainContent}>
{
hasResults
? <Table
? <Table
headers={tableHeaders}
commenters={commenters}
onHeaderClickHandler={props.onHeaderClickHandler}
/>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
}
<Pager
totalPages={props.totalPages}
@@ -50,9 +50,9 @@ class RejectUsernameDialog extends Component {
const next = () => this.setState({stage: stage + 1});
const suspend = () => {
rejectUsername({id: user.user.id, message: this.state.email})
.then(() => {
this.props.handleClose();
});
.then(() => {
this.props.handleClose();
});
};
const suspendModalActions = [
@@ -71,21 +71,21 @@ class RejectUsernameDialog extends Component {
const {stage} = this.state;
return <Dialog
className={styles.suspendDialog}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div className={styles.container}>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
</div>
{
stage === 1 &&
className={styles.suspendDialog}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div className={styles.container}>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
</div>
{
stage === 1 &&
<div className={styles.writeContainer}>
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
<div className={styles.emailContainer}>
@@ -96,16 +96,16 @@ class RejectUsernameDialog extends Component {
onChange={this.onEmailChange}/>
</div>
</div>
}
<div className={styles.modalButtons}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button key={i} onClick={this.onActionClick(stage, i)}>
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
</div>
</div>
</Dialog>;
}
<div className={styles.modalButtons}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button key={i} onClick={this.onActionClick(stage, i)}>
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
</div>
</div>
</Dialog>;
}
}
@@ -9,9 +9,9 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
<tr>
{headers.map((header, i) =>(
<th
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
@@ -1,117 +0,0 @@
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 = {
[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
const User = (props) => {
const {user, modActionButtons} = props;
let userStatus = user.status;
const showSuspenUserDialog = () => props.showSuspendUserDialog({
userId: user.id,
username: user.username,
});
const showBanUserDialog = () => props.showBanUserDialog({
userId: user.id,
username: user.username,
});
// Do not display unless the user status is 'pending' or 'banned'.
// This means that they have already been reviewed and approved.
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<button onClick={() => {props.viewUserDetail(user.id);}} className={styles.button}>{user.username}</button>
{props.currentUser.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={showSuspenUserDialog}>
Suspend User</ActionsMenuItem>
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
</div>
</div>
<div className={styles.itemBody}>
<div className={styles.body}>
<div className={styles.flaggedByCount}>
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{t('community.flags')}({ user.actions.length })</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{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;
}
)}
</div>;
}
)}
</div>
</div>
<div className={styles.sideActions}>
<div className={`actions ${styles.actions}`}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={props.approveUser}
showRejectUsernameDialog={props.showRejectUsernameDialog}
/>
)}
</div>
</div>
</div>
</div>
</li>;
};
export default User;
@@ -1,119 +1,56 @@
/**
* deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
@custom-media --big-viewport (min-width: 780px);
.listContainer {
max-width: 860px;
margin: 0 auto;
}
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
}
.tab {
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: border-bottom 200ms;
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 5px #F36451;
}
.active > span {
color: white;
}
.active:after {
background: transparent !important;
}
.showShortcuts {
position: absolute;
right: 130px;
.container {
padding: 10px;
display: flex;
align-items: center;
font-size: 13px;
span {
margin-left: 7px;
}
padding-bottom: 200px;
}
@media (--big-viewport) {
.tab {
flex: none;
}
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.approve {
margin-top: 10px;
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.notFound {
position: relative;
margin: 20px auto;
text-align: center;
padding: 68px 45px;
vertical-align: middle;
min-width: 500px;
a {
color: rgb(244, 126, 107);
font-weight: 500;
&.goToStreams {
position: absolute;
right: 10px;
bottom: 10px;
}
}
.roleButton {
display: block;
}
.header {
background-color: #2c2c2c;
color: white;
margin-bottom: -1px;
.searchBox {
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
.settingsButton {
i {
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
}
i {
color: #A1A1A1
}
.moderateAsset {
a {
text-align: center;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: opacity 200ms;
opacity: 1;
&:hover {
opacity: .8;
cursor: pointer;
}
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
@custom-media --big-viewport (min-width: 780px);
.list {
padding: 8px 0;
list-style: none;
@@ -134,15 +71,12 @@ span {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
margin: 0;
}
.actionButton {
transform: scale(1.4);
}
}
}
@@ -159,6 +93,7 @@ span {
min-height: 220px;
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
@@ -184,9 +119,9 @@ span {
right: 0;
height: 100%;
top: 0;
padding: 40px 18px;
padding: 50px 18px;
box-sizing: border-box;
}
}
.itemHeader {
display: flex;
@@ -194,6 +129,8 @@ span {
justify-content: space-between;
.author {
font-size: 16px;
font-weight: bold;
min-width: 230px;
display: flex;
align-items: center;
@@ -221,18 +158,12 @@ span {
margin-left: 40px;
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
}
.flagged {
@@ -245,11 +176,8 @@ span {
font-size: 12px;
color: #d32f2f;
}
}
.empty {
color: #444;
margin-top: 50px;
@@ -301,34 +229,14 @@ span {
margin-top: 5px;
}
.Comment {
.moderateArticle {
font-size: 12px;
a {
display: inline-block;
color: #679af3;
text-decoration: none;
font-size: 1em;
font-weight: 400;
letter-spacing: .5px;
font-size: 12px;
margin-left: 10px;
.banButton {
width: 114px;
letter-spacing: 1px;
&:hover {
text-decoration: underline;
opacity: .9;
cursor: pointer;
}
}
}
}
.flagBox {
max-width: 480px;
border-top: 1px solid rgba(66, 66, 66, 0.12);
h3 {
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
margin: 0;
font-weight: 500;
}
}
@@ -1,23 +1,15 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
import {compose} from 'react-apollo';
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {
fetchAccounts,
updateSorting,
newPage,
showRejectUsernameDialog,
hideRejectUsernameDialog
} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import Community from '../components/Community';
@@ -27,85 +19,26 @@ class CommunityContainer extends Component {
this.props.fetchAccounts({});
}
approveUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'APPROVED'});
}
banUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'BANNED'});
}
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (!('users' in this.props.root)) {
return <div><Spinner/></div>;
}
return (
<Community {...this.props} approveUser={this.approveUser} banUser={this.banUser}/>
<Community {...this.props} />
);
}
}
export const withCommunityQuery = withQuery(gql`
query CoralAdmin_Community($action_type: ACTION_TYPE) {
users(query:{action_type: $action_type}){
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
}
`, {
options: ({params: {action_type = 'FLAG'}}) => {
return {
variables: {
action_type: action_type
}
};
}
});
const mapStateToProps = (state) => ({
community: state.community,
currentUser: state.auth.user,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
fetchAccounts,
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
hideRejectUsernameDialog,
updateSorting,
newPage,
viewUserDetail,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withCommunityQuery,
withSetUserStatus,
withRejectUsername,
)(CommunityContainer);
@@ -0,0 +1,128 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
import {withSetUserStatus} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {showRejectUsernameDialog} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import {getDefinitionName} from 'coral-framework/utils';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
class FlaggedAccountsContainer extends Component {
constructor(props) {
super(props);
}
approveUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'APPROVED'});
}
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.users.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
const updated = update(previous, {
users: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
},
});
return updated;
},
});
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
}
return (
<FlaggedAccounts
showBanUserDialog={this.props.showBanUserDialog}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showRejectUsernameDialog={this.props.showRejectUsernameDialog}
viewUserDetail={this.props.viewUserDetail}
approveUser={this.approveUser}
loadMore={this.loadMore}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
me={this.props.root.me}
/>
);
}
}
const LOAD_MORE_QUERY = gql`
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
hasNextPage
endCursor
nodes {
__typename
...${getDefinitionName(FlaggedUser.fragments.user)}
}
}
}
${FlaggedUser.fragments.user}
`;
export const withFlaggedAccountsyQuery = withQuery(gql`
query TalkAdmin_FlaggedAccounts {
...${getDefinitionName(FlaggedUser.fragments.root)}
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
hasNextPage
endCursor
nodes {
__typename
...${getDefinitionName(FlaggedUser.fragments.user)}
}
}
me {
__typename
...${getDefinitionName(FlaggedUser.fragments.me)}
}
}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.user}
${FlaggedUser.fragments.me}
`, {
options: {
fetchPolicy: 'network-only',
},
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
viewUserDetail,
}, dispatch);
export default compose(
connect(null, mapDispatchToProps),
withFlaggedAccountsyQuery,
withSetUserStatus,
)(FlaggedAccountsContainer);
@@ -0,0 +1,42 @@
import {gql} from 'react-apollo';
import FlaggedUser from '../components/FlaggedUser';
import {withFragments} from 'plugin-api/beta/client/hocs';
export default withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
__typename
}
`,
me: gql`
fragment TalkAdminCommunity_FlaggedUser_me on User {
id
}
`,
user: gql`
fragment TalkAdminCommunity_FlaggedUser_user on User {
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
`
})(FlaggedUser);
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
.container {
display: flex;
@@ -111,20 +111,20 @@ export default class Configure extends Component {
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
<List onChange={this.changeSection} activeItem={activeSection}>
<Item itemId='stream' icon='speaker_notes'>
{t('configure.stream_settings')}
</Item>
<Item itemId='moderation' icon='thumbs_up_down'>
{t('configure.moderation_settings')}
</Item>
<Item itemId='tech' icon='code'>
{t('configure.tech_settings')}
</Item>
</List>
<div className={styles.saveBox}>
<div className={styles.container}>
<div className={styles.leftColumn}>
<List onChange={this.changeSection} activeItem={activeSection}>
<Item itemId='stream' icon='speaker_notes'>
{t('configure.stream_settings')}
</Item>
<Item itemId='moderation' icon='thumbs_up_down'>
{t('configure.moderation_settings')}
</Item>
<Item itemId='tech' icon='code'>
{t('configure.tech_settings')}
</Item>
</List>
<div className={styles.saveBox}>
{
showSave ?
<Button
@@ -136,25 +136,25 @@ export default class Configure extends Component {
>
{t('configure.save_changes')}
</Button>
:
:
<Button
raised
disabled
icon='check'
full
>
{t('configure.save_changes')}
</Button>
{t('configure.save_changes')}
</Button>
}
</div>
</div>
</div>
<div className={styles.mainContent}>
{ this.props.saveFetchingError }
{ this.props.fetchSettingsError }
{ section }
</div>
</div>
<div className={styles.mainContent}>
{ this.props.saveFetchingError }
{ this.props.fetchSettingsError }
{ section }
</div>
</div>
);
}
}
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import t from 'coral-framework/services/i18n';
import {join} from 'path';
import styles from './Configure.css';
import {Button, Card} from 'coral-ui';
import {BASE_URL} from 'coral-framework/constants/url';
@@ -25,7 +26,7 @@ class EmbedLink extends Component {
}
render () {
const coralJsUrl = [BASE_URL, '/embed.js'].join('');
const coralJsUrl = join(BASE_URL, '/embed.js');
const nonce = String(Math.random()).slice(2);
const streamElementId = `coral_talk_${nonce}`;
const embedText = `
@@ -4,7 +4,7 @@ import t from 'coral-framework/services/i18n';
import styles from './Configure.css';
import {Checkbox, Textfield} from 'react-mdl';
import {Card, Icon, TextArea} from 'coral-ui';
import MarkdownEditor from 'coral-admin/src/components/MarkdownEditor';
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
const TIMESTAMPS = {
weeks: 60 * 60 * 24 * 7,
@@ -93,15 +93,15 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
value={settings.charCount}
disabled={settings.charCountEnable ? '' : 'disabled'}
/>
<span>{t('configure.comment_count_text_post')}</span>
{
errors.charCount &&
<span>{t('configure.comment_count_text_post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{t('configure.comment_count_error')}
</span>
}
}
</p>
</div>
</Card>
@@ -14,19 +14,19 @@ const ActivityWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{asset.commentCount}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
? assets.map((asset) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{asset.commentCount}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
}
</div>
</div>
@@ -5,17 +5,24 @@ import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const refreshIntervalSeconds = 60 * 5;
// TODO: refactor out storage code into redux.
class CountdownTimer extends React.Component {
static contextTypes = {
storage: PropTypes.object,
};
static propTypes = {
handleTimeout: PropTypes.func.isRequired
}
constructor (props) {
super(props);
constructor (props, context) {
super(props, context);
const {storage} = context;
try {
if (window.localStorage.getItem('coral:dashboardNote') === null) {
window.localStorage.setItem('coral:dashboardNote', 'show');
if (storage && storage.getItem('coral:dashboardNote') === null) {
storage.setItem('coral:dashboardNote', 'show');
}
} catch (e) {
@@ -24,7 +31,7 @@ class CountdownTimer extends React.Component {
this.state = {
secondsUntilRefresh: refreshIntervalSeconds,
dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show'
dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show'
};
}
@@ -54,8 +61,11 @@ class CountdownTimer extends React.Component {
}
dismissNote = () => {
const {storage} = this.context;
try {
window.localStorage.setItem('coral:dashboardNote', 'hide');
if (storage) {
storage.setItem('coral:dashboardNote', 'hide');
}
} catch (e) {
// when setItem fails in Safari Private mode
@@ -64,7 +74,8 @@ class CountdownTimer extends React.Component {
}
render () {
const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' ||
const {storage} = this.context;
const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') ||
this.state.dashboardNote === 'hide'; // for Safari Incognito
return (
<p
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
.Dashboard {
display: flex;
}
@@ -16,24 +16,24 @@ const FlagWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
}
? assets.map((asset) => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
}
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
}
</div>
</div>
@@ -15,20 +15,20 @@ const LikeWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
? assets.map((asset) => {
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
}
</div>
</div>
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
:root {
--row-height: 60px;
}
@@ -22,10 +22,10 @@ class DashboardContainer extends React.Component {
export const witDashboardQuery = withQuery(gql`
query CoralAdmin_Dashboard($from: Date!, $to: Date!) {
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) {
...CoralAdmin_Metrics
}
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
assetsByActivity: assetMetrics(from: $from, to: $to, sortBy: ACTIVITY) {
...CoralAdmin_Metrics
}
}
@@ -42,15 +42,15 @@ export const witDashboardQuery = withQuery(gql`
}
}
`, {
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
return {
variables: {
from: dashboardWindowStart,
to: dashboardWindowEnd
}
};
}
});
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
return {
variables: {
from: dashboardWindowStart,
to: dashboardWindowEnd
}
};
}
});
const mapStateToProps = (state) => {
return {
@@ -1,12 +1,12 @@
.Install {
.install {
max-width: 900px;
margin: 0 auto;
text-align: center;
padding: 50px 0;
}
h2 {
font-size: 2em;
font-weight: 500;
margin: 0;
}
.header {
font-size: 2em;
font-weight: 500;
margin: 0;
}
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
import styles from './style.css';
import styles from './Install.css';
import {Wizard, WizardNav} from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
@@ -37,11 +37,11 @@ export default class Install extends Component {
return (
<Layout restricted={true}>
<div className={styles.Install}>
<div className={styles.install}>
{
!install.alreadyInstalled ? (
<div>
<h2>Welcome to the Coral Project</h2>
<h2 className={styles.header}>Welcome to the Coral Project</h2>
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
<Wizard
currentStep={install.step}
@@ -19,7 +19,7 @@ const InitialStep = (props) => {
showErrors={install.showErrors}
errorMsg={install.errors.email}
noValidate
/>
/>
<TextField
className={styles.textField}
@@ -29,7 +29,7 @@ const InitialStep = (props) => {
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.username}
/>
/>
<TextField
className={styles.textField}
@@ -39,7 +39,7 @@ const InitialStep = (props) => {
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.password}
/>
/>
<TextField
className={styles.textField}
@@ -49,13 +49,13 @@ const InitialStep = (props) => {
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.confirmPassword}
/>
/>
{
!props.install.isLoading ?
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
:
<Spinner />
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
:
<Spinner />
}
{props.install.installRequest === 'FAILURE' && <div className={styles.error}>Error: {props.install.installRequestError}</div>}
</form>
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
.step {
padding: 20px 0;
@@ -2,6 +2,7 @@ import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import ReplyBadge from 'coral-admin/src/components/ReplyBadge';
import FlagBox from 'coral-admin/src/components/FlagBox';
import styles from './styles.css';
import CommentType from 'coral-admin/src/components/CommentType';
@@ -66,7 +67,7 @@ class Comment extends React.Component {
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const commentType = getCommentType(comment);
let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const queryData = {root, comment, asset: comment.asset};
@@ -90,8 +91,8 @@ class Comment extends React.Component {
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{currentUserId !== comment.user.id &&
<ActionsMenu icon="not_interested">
@@ -107,6 +108,7 @@ class Comment extends React.Component {
</ActionsMenu>
}
<div className={styles.adminCommentInfoBar}>
{comment.hasParent && <ReplyBadge/>}
<CommentType type={commentType} className={styles.commentType}/>
<Slot
fill="adminCommentInfoBar"
@@ -190,10 +192,10 @@ class Comment extends React.Component {
/>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
);
@@ -36,8 +36,8 @@ const ModerationMenu = ({
label="Sort"
value={sort}
onChange={(sort) => selectSort(sort)}>
<Option value={'REVERSE_CHRONOLOGICAL'}>{t('modqueue.newest_first')}</Option>
<Option value={'CHRONOLOGICAL'}>{t('modqueue.oldest_first')}</Option>
<Option value={'DESC'}>{t('modqueue.newest_first')}</Option>
<Option value={'ASC'}>{t('modqueue.oldest_first')}</Option>
</SelectField>
</div>
</div>
@@ -97,7 +97,7 @@ class ModerationQueue extends React.Component {
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
currentUserId={this.props.currentUserId}
/>;
/>;
})
}
</CSSTransitionGroup>
@@ -110,7 +110,7 @@ class ModerationQueue extends React.Component {
<LoadMore
loadMore={this.loadMore}
showLoadMore={comments.length < commentCount}
/>
/>
</div>
);
}
@@ -61,20 +61,20 @@ const StorySearch = (props) => {
{
loading
? <Spinner />
: assets.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
? <Spinner />
: assets.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
return <Story
key={i}
id={story.id}
title={story.title}
createdAt={new Date(story.created_at).toISOString()}
open={storyOpen}
author={story.author}
goToStory={props.goToStory}
/>;
})
return <Story
key={i}
id={story.id}
title={story.title}
createdAt={new Date(story.created_at).toISOString()}
open={storyOpen}
author={story.author}
goToStory={props.goToStory}
/>;
})
}
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
@custom-media --big-viewport (min-width: 780px);
.listContainer {
@@ -444,20 +449,20 @@ span {
.commentLeave {
opacity: 1.0;
transition: opacity 800ms;
}
.commentLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.commentEnter {
opacity: 0;
transition: opacity 800ms;
}
.commentEnterActive {
opacity: 1.0;
transition: opacity 800ms;
}
.editedMarker {
@@ -37,6 +37,7 @@ export default withFragments({
count
... on FlagActionSummary {
reason
__typename
}
}
actions {
@@ -48,11 +49,14 @@ export default withFragments({
id
username
}
__typename
}
__typename
}
editing {
edited
}
hasParent
${getSlotFragmentSpreads(slots, 'comment')}
}
`
@@ -26,25 +26,27 @@ import {
storySearchChange,
clearState
} from 'actions/moderation';
import withQueueConfig from '../hoc/withQueueConfig';
import {notify} from 'coral-framework/actions/notification';
import {Spinner} from 'coral-ui';
import Moderation from '../components/Moderation';
import Comment from './Comment';
import queueConfig from '../queueConfig';
import baseQueueConfig from '../queueConfig';
function prepareNotificationText(text) {
return truncate(text, {length: 50}).replace('\n', ' ');
}
function getAssetId(props) {
if (props.params.tabOrId && !(props.params.tabOrId in queueConfig)) {
if (props.params.tabOrId && !(props.params.tabOrId in props.queueConfig)) {
return props.params.tabOrId;
}
return props.params.id || null;
}
function getTab(props) {
if (props.params.tabOrId && props.params.tabOrId in queueConfig) {
if (props.params.tabOrId && props.params.tabOrId in props.queueConfig) {
return props.params.tabOrId;
}
return props.params.tab || null;
@@ -53,8 +55,15 @@ function getTab(props) {
class ModerationContainer extends Component {
subscriptions = [];
handleCommentChange = (root, comment, notify) => {
return handleCommentChange(root, comment, this.props.data.variables.sort, notify, queueConfig, this.activeTab);
handleCommentChange = (root, comment, notifyText) => {
return handleCommentChange(
root,
comment,
this.props.data.variables.sortOrder,
() => notifyText && this.props.notify('info', notifyText),
this.props.queueConfig,
this.activeTab
);
};
get activeTab() {
@@ -78,10 +87,10 @@ class ModerationContainer extends Component {
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notify = this.props.auth.user.id === user.id
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
return this.handleCommentChange(prev, comment, notifyText);
},
});
@@ -90,10 +99,10 @@ class ModerationContainer extends Component {
variables,
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notify = this.props.auth.user.id === user.id
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
return this.handleCommentChange(prev, comment, notifyText);
},
});
@@ -101,8 +110,8 @@ class ModerationContainer extends Component {
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
const notify = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
});
@@ -111,8 +120,8 @@ class ModerationContainer extends Component {
variables,
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
const user = comment.actions[comment.actions.length - 1].user;
const notify = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
});
@@ -159,10 +168,10 @@ class ModerationContainer extends Component {
const variables = {
limit: 10,
cursor: this.props.root[tab].endCursor,
sort: this.props.data.variables.sort,
sortOrder: this.props.data.variables.sortOrder,
asset_id: this.props.data.variables.asset_id,
statuses: queueConfig[tab].statuses,
action_type: queueConfig[tab].action_type,
statuses: this.props.queueConfig[tab].statuses,
action_type: this.props.queueConfig[tab].action_type,
};
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
@@ -199,17 +208,22 @@ class ModerationContainer extends Component {
// Still loading.
return <Spinner />;
}
} else if(asset !== undefined || !('premodCount' in root)) {
} else if (asset !== undefined || !('premodCount' in root)) {
// loading.
return <Spinner />;
}
const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation);
const currentQueueConfig = Object.assign({}, queueConfig);
if (premodEnabled) {
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
isPremod(settings.moderation);
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
if (premodEnabled && root.newCount === 0) {
delete currentQueueConfig.new;
} else {
}
if (!premodEnabled && root.premodCount === 0) {
delete currentQueueConfig.premod;
}
@@ -279,8 +293,8 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
`;
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type}) {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
}
@@ -304,15 +318,15 @@ const commentConnectionFragment = gql`
${Comment.fragments.comment}
`;
const withModQueueQuery = withQuery(gql`
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) {
const withModQueueQuery = withQuery(({queueConfig}) => gql`
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!) {
${Object.keys(queueConfig).map((queue) => `
${queue}: comments(query: {
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
asset_id: $asset_id,
sort: $sort
sortOrder: $sortOrder
}) {
...CoralAdmin_Moderation_CommentConnection
}
@@ -345,14 +359,15 @@ const withModQueueQuery = withQuery(gql`
return {
variables: {
asset_id: id,
sort: props.moderation.sortOrder,
allAssets: id === null
}
sortOrder: props.moderation.sortOrder,
allAssets: id === null,
},
fetchPolicy: 'network-only'
};
},
});
const withQueueCountPolling = withQuery(gql`
const withQueueCountPolling = withQuery(({queueConfig}) => gql`
query CoralAdmin_ModerationCountPoll($asset_id: ID) {
${Object.keys(queueConfig).map((queue) => `
${queue}Count: commentCount(query: {
@@ -393,11 +408,13 @@ const mapDispatchToProps = (dispatch) => ({
viewUserDetail,
setSortOrder,
storySearchChange,
clearState
clearState,
notify,
}, dispatch),
});
export default compose(
withQueueConfig(baseQueueConfig),
connect(mapStateToProps, mapDispatchToProps),
withSetCommentStatus,
withQueueCountPolling,
@@ -98,14 +98,14 @@ export const withAssetSearchQuery = withQuery(gql`
}
}
`, {
options: ({moderation: {storySearchString = ''}}) => {
return {
variables: {
value: storySearchString
}
};
}
});
options: ({moderation: {storySearchString = ''}}) => {
return {
variables: {
value: storySearchString
}
};
}
});
export default compose(
withRouter,
@@ -1,5 +1,4 @@
import update from 'immutability-helper';
import * as notification from 'coral-admin/src/services/notification';
const limit = 10;
@@ -29,29 +28,29 @@ function removeCommentFromQueue(root, queue, id) {
});
}
function shouldCommentBeAdded(root, queue, comment, sort) {
function shouldCommentBeAdded(root, queue, comment, sortOrder) {
if (root[`${queue}Count`] < limit) {
// Adding all comments until first limit has reached.
return true;
}
const cursor = new Date(root[queue].endCursor);
return sort === 'CHRONOLOGICAL'
return sortOrder === 'ASC'
? new Date(comment.created_at) <= cursor
: new Date(comment.created_at) >= cursor;
}
function addCommentToQueue(root, queue, comment, sort) {
function addCommentToQueue(root, queue, comment, sortOrder) {
if (queueHasComment(root, queue, comment.id)) {
return root;
}
const sortAlgo = sort === 'CHRONOLOGICAL' ? ascending : descending;
const sortAlgo = sortOrder === 'ASC' ? ascending : descending;
const changes = {
[`${queue}Count`]: {$set: root[`${queue}Count`] + 1},
};
if (shouldCommentBeAdded(root, queue, comment, sort)) {
if (shouldCommentBeAdded(root, queue, comment, sortOrder)) {
const nodes = root[queue].nodes.concat(comment).sort(sortAlgo);
changes[queue] = {
nodes: {$set: nodes},
@@ -70,15 +69,15 @@ function getCommentQueues(comment, queueConfig) {
const queues = [];
Object.keys(queueConfig).forEach((key) => {
const {action_type, statuses, tags} = queueConfig[key];
let addToQueues = false;
if (statuses && statuses.indexOf(comment.status) >= 0) {
addToQueues = true;
let addToQueues = true;
if (statuses && statuses.indexOf(comment.status) === -1) {
addToQueues = false;
}
if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) {
addToQueues = true;
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
addToQueues = false;
}
if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) {
addToQueues = true;
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
addToQueues = false;
}
if (addToQueues) {
queues.push(key);
@@ -91,16 +90,13 @@ function getCommentQueues(comment, queueConfig) {
* Assimilate comment changes into current store.
* @param {Object} root current state of the store
* @param {Object} comment comment that was changed
* @param {string} sort current sort order of the queues
* @param {Object} [notify] show know notifications if set
* @param {string} notify.activeQueue current active queue
* @param {string} notify.text notification text to show
* @param {bool} notify.anyQueue if true show the notification when the comment is shown
* @param {string} sortOrder current sort order of the queues
* @param {string} notify callback to show notification
* in the current active queue besides the 'all' queue.
* @param {Object} queueConfig queue configuration
* @return {Object} next state of the store
*/
export function handleCommentChange(root, comment, sort, notify, queueConfig, activeQueue) {
export function handleCommentChange(root, comment, sortOrder, notify, queueConfig, activeQueue) {
let next = root;
const nextQueues = getCommentQueues(comment, queueConfig);
@@ -110,22 +106,22 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
if (notificationShown) {
return;
}
notification.info(notify);
notify();
notificationShown = true;
};
Object.keys(queueConfig).forEach((queue) => {
if (nextQueues.indexOf(queue) >= 0) {
if (!queueHasComment(next, queue, comment.id)) {
next = addCommentToQueue(next, queue, comment, sort);
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) {
showNotificationOnce(comment);
next = addCommentToQueue(next, queue, comment, sortOrder);
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sortOrder)) {
showNotificationOnce();
}
}
} else if(queueHasComment(next, queue, comment.id)){
next = removeCommentFromQueue(next, queue, comment.id);
if (notify && activeQueue === queue) {
showNotificationOnce(comment);
showNotificationOnce();
}
}
@@ -134,7 +130,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
&& queueHasComment(next, queue, comment.id)
&& activeQueue === queue
) {
showNotificationOnce(comment);
showNotificationOnce();
}
});
return next;
@@ -0,0 +1,26 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
/**
* WithQueueConfig takes a `queueConfig` parameter that is
* passed down as a prop enriched with queue config data from plugins.
*/
export default (queueConfig) => hoistStatics((WrappedComponent) => {
class WithQueueConfig extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
pluginsConfig = this.context.plugins.getModQueueConfigs();
render() {
return <WrappedComponent
{...this.props}
queueConfig={{...queueConfig, ...this.pluginsConfig}}
/>;
}
}
return WithQueueConfig;
});
@@ -1,17 +1,16 @@
import t from 'coral-framework/services/i18n';
import {getModQueueConfigs} from 'coral-framework/helpers/plugins';
export default {
new: {
statuses: ['NONE'],
icon: 'question_answer',
name: t('modqueue.new'),
},
premod: {
statuses: ['PREMOD'],
icon: 'access_time',
name: t('modqueue.premod'),
},
new: {
statuses: ['NONE', 'PREMOD'],
icon: 'question_answer',
name: t('modqueue.new'),
},
reported: {
action_type: 'FLAG',
statuses: ['NONE', 'PREMOD'],
@@ -33,5 +32,4 @@ export default {
icon: 'question_answer',
name: t('modqueue.all'),
},
...getModQueueConfigs(),
};
@@ -134,20 +134,20 @@ export default class Stories extends Component {
<Radio value='closed'>{t('streams.closed')}</Radio>
</RadioGroup>
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}
className={styles.radioGroup}
>
<Radio value='desc'>{t('streams.newest')}</Radio>
<Radio value='asc'>{t('streams.oldest')}</Radio>
</RadioGroup>
</div>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}
className={styles.radioGroup}
>
<Radio value='desc'>{t('streams.newest')}</Radio>
<Radio value='asc'>{t('streams.oldest')}</Radio>
</RadioGroup>
</div>
{
assetsIds.length
? <div className={styles.mainContent}>
? <div className={styles.mainContent}>
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
<TableHeader name="title" cellFormatter={this.renderTitle}>{t('streams.article')}</TableHeader>
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
@@ -162,7 +162,7 @@ export default class Stories extends Component {
page={this.state.page}
onNewPageHandler={this.onPageClick} />
</div>
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
}
</div>
);
+17 -25
View File
@@ -1,26 +1,18 @@
import t from 'coral-framework/services/i18n';
import {toast} from 'react-toastify';
export function success(msg) {
return toast(msg, {type: 'success'});
}
export function error(msg) {
return toast(msg, {type: 'error'});
}
export function info(msg) {
return toast(msg, {type: 'info'});
}
export function showMutationErrors(error) {
console.error(error);
if (error.errors) {
error.errors.forEach((err) => {
toast(
err.translation_key ? t(`error.${err.translation_key}`) : err,
{type: 'error'}
);
});
}
/**
* createNotificationService returns a notification services based on toast.
* @param {Object} toast
* @return {Object} notification service
*/
export function createNotificationService(toast) {
return {
success(msg) {
toast(msg, {type: 'success'});
},
error(msg) {
toast(msg, {type: 'error'});
},
info(msg) {
toast(msg, {type: 'info'});
},
};
}
-31
View File
@@ -1,31 +0,0 @@
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from '../reducers';
import {getClient} from 'coral-framework/services/client';
const middlewares = [
applyMiddleware(getClient().middleware()),
applyMiddleware(thunk)
];
if (window.devToolsExtension) {
// we can't have the last argument of compose() be undefined
middlewares.push(window.devToolsExtension());
}
const coralReducers = {
...mainReducer,
apollo: getClient().reducer()
};
const store = createStore(
combineReducers(coralReducers),
{},
compose(...middlewares)
);
store.coralReducers = coralReducers;
window.coralStore = store;
export default store;
@@ -12,10 +12,6 @@
padding: 0;
}
.wrapper ul ul {
padding-left: 20px
}
.checkbox {
vertical-align: top;
margin: 12px 12px 12px 0;
@@ -36,4 +32,4 @@
.hidden {
display: none;
}
}
@@ -1,5 +1,6 @@
import React from 'react';
import {Button, Checkbox, TextField} from 'coral-ui';
import {Button, Checkbox} from 'coral-ui';
import QuestionBoxBuilder from './QuestionBoxBuilder';
import styles from './ConfigureCommentStream.css';
@@ -55,17 +56,14 @@ export default ({handleChange, handleApply, changed, ...props}) => (
title: t('configure.enable_questionbox'),
description: t('configure.enable_questionbox_description')
}} />
<div className={`${props.questionBoxEnable ? null : styles.hidden}`} >
<TextField
id="qboxcontent"
onChange={handleChange}
rows={3}
value={props.questionBoxContent}
label={t('configure.include_question_here')}
{
props.questionBoxEnable && <QuestionBoxBuilder
questionBoxIcon={props.questionBoxIcon}
questionBoxContent={props.questionBoxContent}
handleChange={handleChange}
/>
</div>
}
</li>
</ul>
</div>
</form>
@@ -0,0 +1,27 @@
.iconBubble{
position: absolute;
top: 8px;
left: 10px;
color: #9E9E9E;
font-size: 24px;
z-index: 0;
}
.iconPerson{
z-index: 2;
top: 12px;
left: 12px;
position: absolute;
font-size: 33px;
color: #262626;
}
.qbIconContainer {
position: relative;
border: 0;
color: white;
display: inline-block;
padding: 5px 20px;
vertical-align: middle;
width: 10px;
}
@@ -0,0 +1,13 @@
import React from 'react';
import cn from 'classnames';
import styles from './DefaultIcon.css';
import {Icon} from 'coral-ui';
const DefaultIcon = ({className}) => (
<div className={cn(styles.qbIconContainer, className)}>
<Icon name="chat_bubble" className={cn(styles.iconBubble)} />
<Icon name="person" className={cn(styles.iconPerson)} />
</div>
);
export default DefaultIcon;
@@ -0,0 +1,45 @@
.qbBuilder {
margin-left: 50px;
}
.qbItemIconList {
padding: 0;
margin: 10px 0;
}
.qbItemIcon {
background: #F0F0F0;
width: 45px;
height: 45px;
font-size: 24px;
text-align: center;
line-height: 45px;
color: #252525;
border-radius: 3px;
display: inline-block;
overflow: hidden;
margin-right: 10px;
position: relative;
border: solid 2px #F0F0F0;
transition: border 0.3s cubic-bezier(.4,0,.2,1);
}
.qbItemIcon:hover {
cursor: pointer;
}
.qbItemIconActive {
border: solid 2px #00796B;
}
.defaultIcon {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.qb {
margin: 10px 0;
}
@@ -0,0 +1,98 @@
import React from 'react';
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import {Icon, Spinner} from 'coral-ui';
import DefaultIcon from './DefaultIcon';
import cn from 'classnames';
import styles from './QuestionBoxBuilder.css';
class QuestionBoxBuilder extends React.Component {
constructor() {
super();
this.state = {
loading: true
};
}
componentWillMount() {
this.loadEditor();
}
async loadEditor() {
const MarkdownEditor = await import('coral-framework/components/MarkdownEditor');
return this.setState({
loading : false,
MarkdownEditor
});
}
render() {
const {handleChange, questionBoxIcon, questionBoxContent} = this.props;
const {loading, MarkdownEditor} = this.state;
if (loading) {
return <Spinner/>;
}
return (
<div className={styles.qbBuilder}>
<h4>Include an Icon</h4>
<ul className={styles.qbItemIconList}>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'default'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="default" >
<DefaultIcon className={styles.defaultIcon} />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'forum'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="forum" >
<Icon name="forum" />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'build'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="build" >
<Icon name="build" />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="format_quote" >
<Icon name="format_quote" />
</li>
</ul>
<QuestionBox
className={styles.qb}
enable={true}
icon={questionBoxIcon}
content={questionBoxContent}
/>
<MarkdownEditor
value={questionBoxContent}
onChange={(value) => handleChange({}, {questionBoxContent: value})}
/>
</div>
);
}
}
export default QuestionBoxBuilder;
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {updateOpenStatus, updateConfiguration} from 'coral-framework/actions/asset';
import {updateOpenStatus, updateConfiguration} from 'coral-embed-stream/src/actions/asset';
import CloseCommentsInfo from '../components/CloseCommentsInfo';
import ConfigureCommentStream from '../components/ConfigureCommentStream';
@@ -27,10 +27,9 @@ class ConfigureStreamContainer extends Component {
handleApply (e) {
e.preventDefault();
const {elements} = e.target;
const {questionBoxIcon, questionBoxContent} = this.state.dirtySettings;
const premod = elements.premod.checked;
const questionBoxEnable = elements.qboxenable.checked;
const questionBoxContent = elements.qboxcontent.value;
const premodLinksEnable = elements.plinksenable.checked;
const {changed} = this.state;
@@ -38,6 +37,7 @@ class ConfigureStreamContainer extends Component {
moderation: premod ? 'PRE' : 'POST',
questionBoxEnable,
questionBoxContent,
questionBoxIcon,
premodLinksEnable
};
@@ -51,14 +51,18 @@ class ConfigureStreamContainer extends Component {
}
}
handleChange (e) {
const changes = {};
handleChange (e, newChanges) {
let changes = {};
if (changes) {
changes = {...newChanges};
}
if (e.target && e.target.id === 'qboxenable') {
changes.questionBoxEnable = e.target.checked;
}
if (e.target && e.target.id === 'qboxcontent') {
changes.questionBoxContent = e.target.value;
if (e.currentTarget && e.currentTarget.id === 'qboxicon') {
changes.questionBoxIcon = e.currentTarget.dataset.icon;
}
if (e.target && e.target.id === 'plinksenable') {
changes.premodLinksEnable = e.target.value;
@@ -105,12 +109,13 @@ class ConfigureStreamContainer extends Component {
changed={this.state.changed}
premodLinksEnable={dirtySettings.premodLinksEnable}
premod={premod}
questionBoxIcon={dirtySettings.questionBoxIcon}
questionBoxEnable={dirtySettings.questionBoxEnable}
questionBoxContent={dirtySettings.questionBoxContent}
/>
<hr />
<h3>{closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}</h3>
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
<CloseCommentsInfo
onClick={this.toggleStatus}
status={closedAt}
+10 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
import {Router, Route} from 'react-router';
import {history} from 'coral-framework/helpers/router';
import PropTypes from 'prop-types';
import Embed from './containers/Embed';
import {LoginContainer} from 'coral-sign-in/containers/LoginContainer';
@@ -12,6 +12,14 @@ const routes = (
</div>
);
const AppRouter = () => <Router history={history} routes={routes} />;
class AppRouter extends React.Component {
static contextTypes = {
history: PropTypes.object,
};
render() {
return <Router history={this.context.history} routes={routes} />;
}
}
export default AppRouter;
@@ -1,6 +1,5 @@
import * as actions from '../constants/asset';
import coralApi from '../helpers/request';
import {addNotification} from '../actions/notification';
import {notify} from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
@@ -12,12 +11,12 @@ const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_R
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error});
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) => {
const assetId = getState().asset.id;
dispatch(updateAssetSettingsRequest());
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
.then(() => {
dispatch(addNotification('success', t('framework.success_update_settings')));
dispatch(notify('success', t('framework.success_update_settings')));
dispatch(updateAssetSettingsSuccess(newConfig));
})
.catch((error) => {
@@ -26,12 +25,12 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => {
});
};
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => {
const assetId = getState().asset.id;
dispatch(fetchAssetRequest());
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
.then(() => {
dispatch(addNotification('success', t('framework.success_update_settings')));
dispatch(notify('success', t('framework.success_update_settings')));
dispatch(fetchAssetSuccess(closedBody));
})
.catch((error) => {
@@ -1,12 +1,8 @@
import jwtDecode from 'jwt-decode';
import bowser from 'bowser';
import * as actions from '../constants/auth';
import * as Storage from '../helpers/storage';
import coralApi, {base} from '../helpers/request';
import pym from '../services/pym';
import {addNotification} from '../actions/notification';
import {notify} from 'coral-framework/actions/notification';
import {resetWebsocket} from 'coral-framework/services/client';
import t from 'coral-framework/services/i18n';
export const showSignInDialog = () => ({
@@ -59,9 +55,9 @@ export const updateUsername = ({username}) => ({
username
});
export const createUsername = (userId, formData) => (dispatch) => {
export const createUsername = (userId, formData) => (dispatch, _, {rest}) => {
dispatch(createUsernameRequest());
coralApi('/account/username', {method: 'PUT', body: formData})
rest('/account/username', {method: 'PUT', body: formData})
.then(() => {
dispatch(createUsernameSuccess());
dispatch(hideCreateUsernameDialog());
@@ -111,9 +107,11 @@ const signInFailure = (error) => ({
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch) => {
Storage.setItem('exp', jwtDecode(token).exp);
Storage.setItem('token', token);
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
if (storage) {
storage.setItem('exp', jwtDecode(token).exp);
storage.setItem('token', token);
}
dispatch({type: 'HANDLE_AUTH_TOKEN'});
};
@@ -123,10 +121,10 @@ export const handleAuthToken = (token) => (dispatch) => {
//==============================================================================
export const fetchSignIn = (formData) => {
return (dispatch) => {
return (dispatch, _, {rest}) => {
dispatch(signInRequest());
return coralApi('/auth/local', {method: 'POST', body: formData})
return rest('/auth/local', {method: 'POST', body: formData})
.then(({token}) => {
if (!bowser.safari && !bowser.ios) {
dispatch(handleAuthToken(token));
@@ -171,10 +169,10 @@ const signInFacebookFailure = (error) => ({
error
});
export const fetchSignInFacebook = () => (dispatch) => {
export const fetchSignInFacebook = () => (dispatch, _, {rest}) => {
dispatch(signInFacebookRequest());
window.open(
`${base}/auth/facebook`,
`${rest.uri}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
@@ -188,10 +186,10 @@ const signUpFacebookRequest = () => ({
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST
});
export const fetchSignUpFacebook = () => (dispatch) => {
export const fetchSignUpFacebook = () => (dispatch, _, {rest}) => {
dispatch(signUpFacebookRequest());
window.open(
`${base}/auth/facebook`,
`${rest.uri}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
@@ -220,11 +218,11 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error});
export const fetchSignUp = (formData) => (dispatch, getState) => {
export const fetchSignUp = (formData) => (dispatch, getState, {rest}) => {
const redirectUri = getState().auth.redirectUri;
dispatch(signUpRequest());
coralApi('/users', {
rest('/users', {
method: 'POST',
body: formData,
headers: {'X-Pym-Url': redirectUri}
@@ -256,10 +254,10 @@ const forgotPasswordFailure = (error) => ({
error,
});
export const fetchForgotPassword = (email) => (dispatch, getState) => {
export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = getState().auth.redirectUri;
coralApi('/account/password/reset', {
rest('/account/password/reset', {
method: 'POST',
body: {email, loc: redirectUri}
})
@@ -275,16 +273,18 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => {
// LOGOUT
//==============================================================================
export const logout = () => (dispatch) => {
return coralApi('/auth', {method: 'DELETE'}).then(() => {
Storage.removeItem('token');
export const logout = () => async (dispatch, _, {rest, client, pym, storage}) => {
await rest('/auth', {method: 'DELETE'});
// Reset the websocket.
resetWebsocket();
if (storage) {
storage.removeItem('token');
}
dispatch({type: actions.LOGOUT});
pym.sendMessage('coral-auth-changed');
});
// Reset the websocket.
client.resetWebsocket();
dispatch({type: actions.LOGOUT});
pym.sendMessage('coral-auth-changed');
};
//==============================================================================
@@ -300,17 +300,19 @@ const checkLoginSuccess = (user, isAdmin) => ({
isAdmin
});
export const checkLogin = () => (dispatch) => {
export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
dispatch(checkLoginRequest());
coralApi('/auth')
rest('/auth')
.then((result) => {
if (!result.user) {
Storage.removeItem('token');
if (storage) {
storage.removeItem('token');
}
throw new Error('Not logged in');
}
// Reset the websocket.
resetWebsocket();
client.resetWebsocket();
dispatch(checkLoginSuccess(result.user));
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
@@ -322,10 +324,10 @@ export const checkLogin = () => (dispatch) => {
})
.catch((error) => {
console.error(error);
if (error.status && error.status === 401) {
if (error.status && error.status === 401 && storage) {
// Unauthorized.
Storage.removeItem('token');
storage.removeItem('token');
}
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(checkLoginFailure(errorMessage));
@@ -351,10 +353,10 @@ const verifyEmailFailure = () => ({
type: actions.VERIFY_EMAIL_FAILURE
});
export const requestConfirmEmail = (email) => (dispatch, getState) => {
export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => {
const redirectUri = getState().auth.redirectUri;
dispatch(verifyEmailRequest());
return coralApi('/users/resend-verify', {
return rest('/users/resend-verify', {
method: 'POST',
body: {email},
headers: {'X-Pym-Url': redirectUri}
@@ -387,11 +389,11 @@ export const setRedirectUri = (uri) => ({
const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error});
const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS});
export const editName = (username) => (dispatch) => {
return coralApi('/account/username', {method: 'PUT', body: {username}})
export const editName = (username) => (dispatch, _, {rest}) => {
return rest('/account/username', {method: 'PUT', body: {username}})
.then(() => {
dispatch(editUsernameSuccess());
dispatch(addNotification('success', t('framework.success_name_update')));
dispatch(notify('success', t('framework.success_name_update')));
})
.catch((error) => {
console.error(error);

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