Compare commits

..
749 Commits
Author SHA1 Message Date
Wyatt Johnson 08b1cada7e Merge pull request #1293 from coralproject/deploy-script
Fixed deploy script
2018-01-17 15:01:57 -07:00
Wyatt Johnson 4960b34748 fixed deploy script 2018-01-17 14:56:36 -07:00
Wyatt Johnson e048491221 Merge pull request #1291 from coralproject/webpack-sourcemaps
Webpack Sourcemap Configuration
2018-01-17 11:40:48 -07:00
Wyatt Johnson 68598d3c45 cleanup for logic 2018-01-17 11:34:09 -07:00
Wyatt Johnson 7ca89435c6 added configuration options for webpack 2018-01-17 11:24:36 -07:00
Kiwi 13fc91b355 Merge pull request #1287 from coralproject/ci-time-sync
CI Time Sync
2018-01-16 20:53:13 +01:00
Wyatt Johnson 9eec280868 added time syncing 2018-01-16 12:31:45 -07:00
Kiwi 1cf2bda36c Merge pull request #1286 from coralproject/e2e-pause
Add a little pause in e2e test
2018-01-16 19:44:21 +01:00
Chi Vinh Le b1adfe8c46 Add a little pause in e2e test 2018-01-16 19:33:07 +01:00
Wyatt Johnson 7885b025ee Merge pull request #1262 from coralproject/firefox-actionmenu-deselect
Fix deselect bug of action menu in ff
2018-01-12 16:49:04 -07:00
Wyatt Johnson 3606726c4b Merge branch 'master' into firefox-actionmenu-deselect 2018-01-12 16:33:55 -07:00
Wyatt Johnson 93bda87ad0 Merge pull request #1153 from coralproject/next
Release 4.0.0
2018-01-12 09:43:40 -07:00
Chi Vinh Le 1ccd36e9e9 prettier 2018-01-12 14:07:05 +01:00
Chi Vinh Le 9762f34324 End of line 2018-01-12 11:45:52 +01:00
Chi Vinh Le 9ce14acc9a More syncs 2018-01-12 11:23:29 +01:00
Wyatt Johnson c19a6c8b10 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-11 20:25:18 -07:00
Wyatt Johnson 0abc2ca243 replaced eslint:recommended with prettier 2018-01-11 20:00:34 -07:00
Wyatt Johnson d56c19016a Merge pull request #1279 from coralproject/docs
Updated docs for 4.0
2018-01-11 17:53:12 -07:00
Kim Gardner bb9a93986e Small wording tweak 2018-01-11 19:41:45 -05:00
Kim Gardner c906b2df5e Merge branch 'next' into docs 2018-01-11 19:36:58 -05:00
Wyatt Johnson 204b442e2f added deprecation warning 2018-01-11 17:32:52 -07:00
Wyatt Johnson 876f4ef700 updated docs for 4.0 2018-01-11 16:51:08 -07:00
Wyatt Johnson 7f02d4f39c fixed circle.yml 2018-01-11 15:54:46 -07:00
Kim Gardner c1f5450b9a Merge branch 'master' into next 2018-01-11 17:43:43 -05:00
Wyatt Johnson 7f291cf056 Merge pull request #1276 from coralproject/email-send-fix
Missing Email Error
2018-01-11 15:28:18 -07:00
Wyatt Johnson 7706a0521c Merge branch 'next' into email-send-fix 2018-01-11 15:28:11 -07:00
Wyatt Johnson 81d2927c0f Merge pull request #1278 from coralproject/plugin-context
Plugin context
2018-01-11 15:27:50 -07:00
Wyatt Johnson f2698298f7 Merge branch 'next' into plugin-context 2018-01-11 15:27:04 -07:00
Wyatt Johnson a4e8ea93bc fixed import typo 2018-01-11 15:24:46 -07:00
Kim Gardner ca49b373bf Merge branch 'next' into email-send-fix 2018-01-11 17:23:42 -05:00
Kim Gardner 4c6ec75cd7 Merge pull request #1277 from coralproject/hide-user-status-auth
Hide user status auth
2018-01-11 17:23:23 -05:00
Wyatt Johnson 07826a9bf4 fixing spelling :( 2018-01-11 15:20:53 -07:00
Belén Curcio f79f521d3d Merge branch 'next' into plugin-context 2018-01-11 19:15:59 -03:00
Belén Curcio 7e465f7317 Merge branch 'next' into email-send-fix 2018-01-11 19:11:33 -03:00
Belén Curcio 943c3e93a4 Merge branch 'next' into hide-user-status-auth 2018-01-11 19:11:16 -03:00
Kim Gardner f070d47c2d Merge pull request #1267 from coralproject/perm-fix
Permission fix
2018-01-11 16:40:10 -05:00
Wyatt Johnson 4eda547734 removed config from context 2018-01-11 14:22:45 -07:00
Wyatt Johnson 5865c30155 added a plugin context 2018-01-11 14:20:51 -07:00
Kim Gardner 838083e76d Merge branch 'next' into perm-fix 2018-01-11 16:10:41 -05:00
Wyatt Johnson 4d65052eef allow moderators to view role 2018-01-11 14:00:26 -07:00
Wyatt Johnson eec7d0fb63 Merge branch 'next' into hide-user-status-auth 2018-01-11 13:57:21 -07:00
Wyatt Johnson d837562089 hides the user status histories and tokens from the auth endpoints, which were resulting in errors 2018-01-11 13:56:23 -07:00
Kim Gardner fddba1e059 Merge branch 'next' into email-send-fix 2018-01-11 15:49:44 -05:00
Kim Gardner 1e85894a0c Merge pull request #1274 from coralproject/account-history
Suspension duration, fixes and wording for Account History
2018-01-11 15:49:26 -05:00
Kim Gardner 9b4dcd8d32 Merge branch 'next' into account-history 2018-01-11 15:34:50 -05:00
Kim Gardner 54ec9ea9a6 Merge pull request #1275 from coralproject/flags-enh
Change Flags to Reports and make font a bit bigger
2018-01-11 15:34:18 -05:00
Kim Gardner f51eda4f5e Merge branch 'next' into flags-enh 2018-01-11 15:24:24 -05:00
Kim Gardner be40594ecf Merge pull request #1273 from coralproject/selected-bug
Restore shortcut functionality
2018-01-11 15:24:09 -05:00
Kim Gardner f340cdd4b7 Merge branch 'next' into selected-bug 2018-01-11 15:10:09 -05:00
Kim Gardner 24b6a4f16f Merge pull request #1272 from coralproject/fix-scrollbars
Fix scrollbars issues
2018-01-11 15:05:34 -05:00
Kim Gardner f2e63b7621 Merge branch 'next' into fix-scrollbars 2018-01-11 14:41:32 -05:00
Kim Gardner 6d3d717ab3 Merge pull request #1271 from coralproject/fix-no-status-history
Fix no status history error
2018-01-11 14:41:17 -05:00
Wyatt Johnson 1dcf297a45 added email fixes 2018-01-11 10:58:43 -07:00
Kiwi 8cdc881f3d Merge branch 'next' into flags-enh 2018-01-11 18:47:05 +01:00
Belén Curcio 9187231737 Merge branch 'next' into account-history 2018-01-11 14:45:23 -03:00
okbel 2bc60b1211 fixes 2018-01-11 14:44:53 -03:00
Chi Vinh Le 90f1ef050b Change Flags to Reports and make a bit bigger 2018-01-11 18:41:53 +01:00
Kim Gardner eb5b736484 Merge branch 'next' into fix-no-status-history 2018-01-11 12:27:37 -05:00
Kim Gardner 92b506c85a Merge pull request #1270 from coralproject/suspect-label
Fix suspect label
2018-01-11 12:17:38 -05:00
Chi Vinh Le 2269c91244 Restore shortcut functionality 2018-01-11 16:54:54 +01:00
Chi Vinh Le 210b0ab907 Only show scrollbars when necessary 2018-01-11 16:05:20 +01:00
Chi Vinh Le 1337873adf Remove unwanted scroll in configure 2018-01-11 16:05:02 +01:00
Kim Gardner d922761f85 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-11 09:51:40 -05:00
Kim Gardner 737b77a6ac Merge pull request #1269 from larrylizhao/feature/pluginTranslation
Translations refinement for plugins
2018-01-11 09:36:35 -05:00
Chi Vinh Le 11b38f7ea0 Fix no status history error 2018-01-11 13:55:09 +01:00
Kiwi f89a5013d2 Merge branch 'next' into suspect-label 2018-01-11 11:20:04 +01:00
Kiwi 139d319965 Merge branch 'next' into perm-fix 2018-01-11 11:19:13 +01:00
Chi Vinh Le 89c929f970 Fix suspect label 2018-01-11 11:16:30 +01:00
liz 6e813e9663 Translations refinement for plugins 2018-01-11 17:16:26 +08:00
Wyatt Johnson 14dd9acd3b changed copy for error 2018-01-10 16:49:40 -07:00
Wyatt Johnson d35d0e354e Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 15:35:27 -07:00
Wyatt Johnson ec38b837aa Merge branch 'next' into perm-fix 2018-01-10 14:48:08 -07:00
okbel d055e0b28b Feature Button 2018-01-10 17:51:21 -03:00
okbel 1f6d38c4ad Updating copy 2018-01-10 17:43:55 -03:00
okbel eef2578e61 ignore user box over flow, fixed 2018-01-10 17:41:18 -03:00
okbel 7127aeaa87 Icon User Drawer 2018-01-10 17:35:50 -03:00
Wyatt Johnson 44648fa901 fixed perms issue 2018-01-10 13:04:56 -07:00
Wyatt Johnson acc2e00c54 Merge pull request #1264 from coralproject/reply
Hide reply button when comment stream is closed
2018-01-10 11:55:05 -07:00
Belén Curcio 7d6fd69a3b Merge branch 'next' into reply 2018-01-10 15:53:39 -03:00
Wyatt Johnson 75d1bb2f22 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 10:40:52 -07:00
Wyatt Johnson 1e372b583a Merge pull request #1265 from coralproject/lodash-size-opt
Use lodash with es modules
2018-01-10 10:40:35 -07:00
Kiwi 1b373b5af4 Merge branch 'next' into lodash-size-opt 2018-01-10 18:39:57 +01:00
Kiwi d0d9a38177 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 18:39:54 +01:00
Kiwi 9d8867be9d Merge branch 'next' into reply 2018-01-10 18:39:45 +01:00
Wyatt Johnson fb7ff249b8 Merge pull request #1266 from coralproject/circle-next
Circle + Docker next
2018-01-10 10:38:56 -07:00
Wyatt Johnson bbdd69b2a0 removed debug... 2018-01-10 10:34:11 -07:00
Wyatt Johnson 3687fc1702 adjusted build script to build the next branch and release it 2018-01-10 10:31:43 -07:00
Chi Vinh Le caf2b88ee8 Fix typo 2018-01-10 18:29:03 +01:00
okbel 2ef3e29e7d Merge branch 'reply' of github.com:coralproject/talk into reply
* 'reply' of github.com:coralproject/talk: (193 commits)
  Add some margin to loadmore
  typo
  updated tests
  updated tests
  Fully implement SetUsername
  removed snakecase
  fix to optional required param
  Use correct mutation and show errors
  refactored mailer service with lodash!
  Fix live status updates
  Move ChangeUsername to the appropiate tab
  Fix change username not displaying errors
  Remove defunct refetch detection workaround
  Rename UnBan and UnSuspend
  hardened configuration
  user cli patches
  don't apply configuration from dotfiles during testing
  applied rename to graph mutation edges
  reintroduced the errors.ErrSameUsernameProvided
  fixed wrong query logic
  ...
2018-01-10 14:08:58 -03:00
Kiwi 6a3be92a4c Merge branch 'next' into reply 2018-01-10 18:08:12 +01:00
okbel 845c99de10 Updated tests 2018-01-10 13:57:16 -03:00
okbel 9c7b6e007a Merge branch 'reply' of github.com:coralproject/talk into reply
* 'reply' of github.com:coralproject/talk:
  Small grammar fixes
  refactor to cleanup
  extracted asset url
  extracted more funcs
  extracted events from opts
  Adding TALK_PERSPECTIVE_DO_NOT_STORE to the docs
  Adding doNotStore
  Translation update to make it more appropriate.
  Translation for hide off topic comments
2018-01-10 13:55:07 -03:00
okbel 4c3d3fb4b0 Updated tests 2018-01-10 13:54:43 -03:00
Chi Vinh Le 7036aa5bad Use lodash with es modules 2018-01-10 17:54:06 +01:00
Belén Curcio 1898941097 Merge branch 'master' into reply 2018-01-10 12:31:10 -03:00
okbel 24f7405065 remove uncommitted 2018-01-10 12:29:47 -03:00
okbel e1ead8f500 Adding e2e for the stream - open and closed asset features 2018-01-10 11:43:34 -03:00
okbel 5db8209182 Removing replyButton in Stream is closed 2018-01-10 11:21:24 -03:00
okbel 9c75f3fa59 Adding missing proptypes 2018-01-10 11:03:33 -03:00
Belén Curcio 25ac66ec59 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 10:46:48 -03:00
Chi Vinh Le bdcc9273f1 Add some margin to loadmore 2018-01-10 12:22:23 +01:00
Kiwi dca85ad127 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 12:19:58 +01:00
Chi Vinh Le 36f7c31a03 Fix deselect bug of action menu in ff 2018-01-10 12:16:28 +01:00
Kim Gardner 63bd6309d3 Merge branch 'master' into next 2018-01-09 17:22:47 -05:00
Kim Gardner bf76f0453b Merge pull request #1261 from coralproject/losowsky-patch-1
Small grammar fixes
2018-01-09 17:04:01 -05:00
Kim Gardner 00677148b9 Merge branch 'master' into losowsky-patch-1 2018-01-09 16:53:15 -05:00
Wyatt Johnson 7bdfb55a1c Merge pull request #1260 from coralproject/embed-events
Embed events
2018-01-09 14:53:04 -07:00
Andrew Losowsky c43051c6dd Small grammar fixes 2018-01-09 16:20:32 -05:00
Wyatt Johnson 20453dfddd refactor to cleanup 2018-01-09 14:13:02 -07:00
Wyatt Johnson 20f9bba845 Merge branch 'master' into embed-events 2018-01-09 11:25:22 -07:00
Wyatt Johnson 25e29cb79a extracted asset url 2018-01-09 11:01:10 -07:00
Wyatt Johnson 7f25f46ac0 extracted more funcs 2018-01-09 10:51:43 -07:00
Wyatt Johnson d3c1286d1a extracted events from opts 2018-01-09 10:47:58 -07:00
Kim Gardner 13bf20daf0 Merge pull request #1259 from coralproject/fix-set-username
Make Facebook login work again in next
2018-01-09 12:39:20 -05:00
Kim Gardner e61c09f017 Merge branch 'next' into fix-set-username 2018-01-09 11:52:29 -05:00
Kim Gardner 395793445c Merge branch 'master' into next 2018-01-09 11:52:20 -05:00
Kim Gardner abc6cf6366 Merge pull request #1258 from coralproject/toxic-update
Adding doNotStore param to our Toxic Comments Plugin
2018-01-09 11:51:59 -05:00
Kim Gardner eb33eb72d1 Merge branch 'master' into toxic-update 2018-01-09 11:44:44 -05:00
Kim Gardner 1a39635c0e Merge pull request #1257 from larrylizhao/feature/RefineTranslation
Feature/refine translation
2018-01-09 11:43:23 -05:00
Kiwi 13347a6baf Merge branch 'next' into fix-set-username 2018-01-09 15:07:56 +01:00
okbel ff016e22fe Adding TALK_PERSPECTIVE_DO_NOT_STORE to the docs 2018-01-09 10:42:28 -03:00
okbel 579644c910 Adding doNotStore 2018-01-09 10:34:24 -03:00
okbel 54b504eb8e typo 2018-01-09 10:24:58 -03:00
okbel 4490ea5202 Merge branch 'next' of github.com:coralproject/talk into next
* 'next' of github.com:coralproject/talk: (26 commits)
  removed snakecase
  fix to optional required param
  refactored mailer service with lodash!
  Docs typo
  Fix live status updates
  Move ChangeUsername to the appropiate tab
  Fix change username not displaying errors
  Remove defunct refetch detection workaround
  Rename UnBan and UnSuspend
  hardened configuration
  user cli patches
  don't apply configuration from dotfiles during testing
  applied rename to graph mutation edges
  reintroduced the errors.ErrSameUsernameProvided
  fixed wrong query logic
  resolving pr concerns
  Add proptype
  Mark onClickOutside not required
  Remove unused state
  Use null instead of boolean
  ...
2018-01-09 10:13:07 -03:00
okbel fb26d67e42 updated tests 2018-01-09 10:12:26 -03:00
okbel 07052824d7 updated tests 2018-01-09 10:10:37 -03:00
Chi Vinh Le a2eb474a5c Fully implement SetUsername 2018-01-09 12:25:44 +01:00
Chi Vinh Le 2bcb324582 Merge branch 'next' into fix-set-username 2018-01-09 11:41:56 +01:00
liz 37d1f3361c Translation update to make it more appropriate. 2018-01-09 16:54:13 +08:00
liz 6e8b28cc8a Translation for hide off topic comments 2018-01-09 14:03:09 +08:00
Wyatt Johnson 9513502667 Merge pull request #1255 from coralproject/fix-live-status-changes
Fix live status updates
2018-01-08 11:52:27 -07:00
Wyatt Johnson 55c2c00b49 Merge branch 'next' into fix-live-status-changes 2018-01-08 11:36:06 -07:00
Wyatt Johnson 8545be307d removed snakecase 2018-01-08 11:17:11 -07:00
Wyatt Johnson 04991a9856 Merge branch 'master' into next 2018-01-08 11:14:17 -07:00
Wyatt Johnson 978e1c8d4c fix to optional required param 2018-01-08 11:04:29 -07:00
Wyatt Johnson 6700698e52 Merge branch 'next' into fix-live-status-changes 2018-01-08 10:56:54 -07:00
Wyatt Johnson c0f266dfa5 Merge pull request #1256 from coralproject/docs-typo
Docs typo
2018-01-08 10:56:41 -07:00
Chi Vinh Le a40b287226 Use correct mutation and show errors 2018-01-08 18:52:54 +01:00
Wyatt Johnson 615be40b45 refactored mailer service with lodash! 2018-01-08 10:43:10 -07:00
Kim Gardner 99d66fef52 Docs typo 2018-01-08 11:27:25 -05:00
Chi Vinh Le c3a5da3ae6 Fix live status updates 2018-01-08 16:00:20 +01:00
Chi Vinh Le ec852601a4 Move ChangeUsername to the appropiate tab 2018-01-08 15:50:41 +01:00
Chi Vinh Le 791025b153 Fix change username not displaying errors 2018-01-08 15:48:49 +01:00
Chi Vinh Le be72e2effd Remove defunct refetch detection workaround 2018-01-08 15:01:09 +01:00
Chi Vinh Le 717d5e6566 Rename UnBan and UnSuspend 2018-01-08 14:43:14 +01:00
Wyatt Johnson c07ec23a20 hardened configuration
- moved env rewrite + dotenv code to single location
- fixed bug with system flag on user
2018-01-05 16:06:36 -07:00
Wyatt Johnson 6260339508 user cli patches
- removed unused user actions
- added system side graph context
- added user cli search
2018-01-05 15:32:36 -07:00
Wyatt Johnson 10754a65c6 don't apply configuration from dotfiles during testing 2018-01-05 12:12:54 -07:00
Wyatt Johnson c3970a46a2 applied rename to graph mutation edges 2018-01-05 12:11:32 -07:00
Wyatt Johnson c9bc9a4b10 reintroduced the errors.ErrSameUsernameProvided 2018-01-05 11:31:40 -07:00
Wyatt Johnson 7b5993bc60 fixed wrong query logic 2018-01-05 11:17:04 -07:00
Wyatt Johnson b42cce5747 resolving pr concerns 2018-01-05 11:12:35 -07:00
Wyatt Johnson 9c595b7f84 Merge branch 'master' into next 2018-01-05 11:10:32 -07:00
Chi Vinh Le acdaaffd4e Add proptype 2018-01-05 19:03:40 +01:00
Chi Vinh Le c01b192a73 Mark onClickOutside not required 2018-01-05 19:02:35 +01:00
Chi Vinh Le 7538e6e8fd Remove unused state 2018-01-05 18:57:12 +01:00
Chi Vinh Le 49c350376b Use null instead of boolean 2018-01-05 18:56:44 +01:00
Chi Vinh Le 4a259d09e4 Fix styling for new x button 2018-01-05 18:56:13 +01:00
okbel 918aa6988b Adding a UI reducer to manage UI state 2018-01-05 14:50:35 -03:00
okbel 7e4831e309 styles.css is de-pre-ca-ted 2018-01-05 14:30:28 -03:00
okbel 14034c1364 Using button in the Drawer 2018-01-05 14:24:21 -03:00
okbel 5a379b2cd8 Adding line back 2018-01-05 14:24:06 -03:00
Kim Gardner ad6a663179 Merge pull request #1237 from coralproject/admin-pages
CSS Customization and i18n fixes
2018-01-05 12:20:46 -05:00
okbel b185d61993 Adding clarifications 2018-01-05 07:49:37 -03:00
okbel 9f780a64cc Load more: Rejected Usernames -> Wrong name 2018-01-05 07:46:21 -03:00
okbel 88b44c3bf6 Moment settings based on users locale 2018-01-05 07:34:17 -03:00
okbel e83f99e389 Removing unused code 2018-01-05 07:28:51 -03:00
Kim Gardner ce4ab4e86c Merge branch 'master' into next 2018-01-04 17:28:13 -05:00
Kim Gardner 5cd6322284 Merge branch 'master' into admin-pages 2018-01-04 17:26:33 -05:00
Kim Gardner 0ab6cc3a0f Merge pull request #1230 from coralproject/safari-bs-changes
Safari Browserstack changes
2018-01-04 17:26:05 -05:00
Kim Gardner c617a3446d Merge branch 'master' into safari-bs-changes 2018-01-04 17:15:47 -05:00
Kim Gardner d76fe5350e Merge pull request #1238 from MEL27/master
Add initial Simplified Chinese translation
2018-01-04 17:15:23 -05:00
Kim Gardner 927aa54feb Merge branch 'master' into master 2018-01-04 17:03:10 -05:00
Kim Gardner fc272ed1e1 Merge branch 'master' into master 2018-01-04 17:02:32 -05:00
Kim Gardner 882251864a Merge pull request #1246 from coralproject/simplified-chinese-merge-fixes
Simplified Chinese merge fixes
2018-01-04 17:01:42 -05:00
Kim Gardner 4eaa234dd8 Merge branch 'master' into simplified-chinese-merge-fixes 2018-01-04 16:48:46 -05:00
Kim Gardner bf1fbfe03f Fix merge conflicts on master 2018-01-04 16:43:51 -05:00
Kim Gardner d3f93644db Merge branch 'master' into master 2018-01-04 16:07:18 -05:00
Kim Gardner b9412dcb10 Merge branch 'master' into master 2018-01-04 15:55:18 -05:00
Kim Gardner 1125351cd1 Merge pull request #1243 from larrylizhao/feature/zh_tw
Adding traditional Chinese locales for Coral talk
2018-01-04 15:35:22 -05:00
Wyatt Johnson c683dd8d32 fixed let -> const 2018-01-04 10:56:15 -07:00
Wyatt Johnson 843908dab2 changed beheviour of dotenv parsing 2018-01-04 10:52:15 -07:00
Wyatt Johnson ad35a04cdf pr review updates 2018-01-04 10:50:14 -07:00
Kiwi 2c87dac897 Merge branch 'master' into admin-pages 2018-01-04 18:42:21 +01:00
Chi Vinh Le d5bf44ada8 Keep localhost for mongodb and redis 2018-01-04 18:41:31 +01:00
okbel 7bfbeac368 Removing unused code 2018-01-04 13:59:37 -03:00
okbel ca55097b5c Removing unused code 2018-01-04 13:58:07 -03:00
okbel ee0c510569 Adding warning 2018-01-04 13:55:00 -03:00
okbel 3d9eec825c Merge branch 'next' of github.com:coralproject/talk into next
* 'next' of github.com:coralproject/talk:
  Up patch version to match tag.
2018-01-04 13:37:36 -03:00
okbel 6d777944aa Naming Conventions 2018-01-04 13:37:21 -03:00
okbel 841398c6c1 Removing pointer hover, adding proptypes 2018-01-04 13:23:41 -03:00
Kim Gardner f8c1f1e63d Merge branch 'master' into safari-bs-changes 2018-01-04 11:21:52 -05:00
Kim Gardner c7176a4660 Merge branch 'master' into feature/zh_tw 2018-01-04 11:21:07 -05:00
Kim Gardner e2534e7ca8 Merge branch 'master' into master 2018-01-04 11:20:57 -05:00
Kim Gardner d09948e7f6 Merge branch 'master' into next 2018-01-04 11:20:18 -05:00
Wyatt Johnson 270f865ccd Merge pull request #1245 from jheasly/version_fix
Up patch version to match tag.
2018-01-04 09:18:28 -07:00
okbel e52793e918 Moving bulk actions to User tab 2018-01-04 12:50:53 -03:00
okbel a8e09d4c89 Account History stable key 2018-01-04 12:25:29 -03:00
okbel 7bdaf74e1a SetUserRole using DataProxy 2018-01-04 12:08:26 -03:00
okbel f04c3cc059 Naming Conventions 2018-01-04 11:53:08 -03:00
okbel 8c5040d88b Naming conventions 2018-01-04 11:49:10 -03:00
okbel 3ca3b59802 Using user.id 2018-01-04 11:44:02 -03:00
okbel fa6aa4ae2d Search functionality on the Community People Section 2018-01-04 11:42:51 -03:00
okbel f6e5df2088 Merge branch 'next' of github.com:coralproject/talk into next
* 'next' of github.com:coralproject/talk:
  added user query by value to graph
2018-01-04 11:20:58 -03:00
okbel e343958b07 Merge branch 'master' of github.com:coralproject/talk into next
* 'master' of github.com:coralproject/talk:
  fixes default lang select bug
2018-01-04 11:02:50 -03:00
Belén Curcio 298cb5322e Merge pull request #1244 from coralproject/user-search
User Search
2018-01-04 11:02:28 -03:00
J. Heasly d7097ff729 Up patch version to match tag. 2018-01-03 17:51:58 -08:00
Wyatt Johnson 94f2b4397e Merge branch 'master' into feature/zh_tw 2018-01-03 14:57:50 -07:00
Wyatt Johnson 75e89591a8 Merge branch 'master' into safari-bs-changes 2018-01-03 14:54:58 -07:00
Wyatt Johnson 72b2211daa reduced complexity 2018-01-03 14:43:35 -07:00
Wyatt Johnson 0850b1f982 added some comments 2018-01-03 14:34:50 -07:00
Wyatt Johnson 375fbd3790 resolving code climate issues 2018-01-03 14:32:19 -07:00
Wyatt Johnson 82c84de33f code review 2018-01-03 14:22:52 -07:00
Wyatt Johnson 57972b3fb9 Merge branch 'master' into admin-pages 2018-01-03 14:08:55 -07:00
Wyatt Johnson 9dcb8f8f0e Merge branch 'next' into user-search 2018-01-03 14:08:38 -07:00
Wyatt Johnson 148d329ff8 Merge branch 'master' into next 2018-01-03 14:08:29 -07:00
Wyatt Johnson 517124cc93 Merge pull request #1236 from coralproject/i18n-bug-fix
i18n Default Language Fix
2018-01-03 14:08:11 -07:00
Wyatt Johnson 797cea4f63 added user query by value to graph 2018-01-03 14:01:51 -07:00
okbel cb2f032ae1 fix index for moderation actions, and view options 2018-01-03 17:38:22 -03:00
Belén Curcio 0572a1c97d Merge branch 'master' into next 2018-01-03 12:11:44 -03:00
liz bb8a75937a Fix for typo in plugin 2018-01-03 18:10:25 +08:00
Kiwi 8060d9c22e Merge branch 'master' into safari-bs-changes 2018-01-03 10:23:01 +01:00
liz 783e485aa5 Add traditional Chinese locales for plugins 2018-01-03 17:21:30 +08:00
liz 6f96bd416c Traditional Chinese locales 2018-01-03 17:19:11 +08:00
Wyatt Johnson 20e3d9065f Merge branch 'master' into i18n-bug-fix 2017-12-30 02:56:13 -07:00
Wyatt Johnson 7b6c59b6f1 Merge branch 'master' into admin-pages 2017-12-30 02:56:02 -07:00
Wyatt Johnson 0af8a31154 Merge pull request #1240 from gakhov/gakhov-patch-advanced-config
Fix a variable name for redis client configuration
2017-12-30 02:55:46 -07:00
Andrii Gakhov 140bba23e0 Fix a variable name for redis client configuration
The documentation incorrectly stated that the advanced Redis Client configuration could be set using `TALK_REDIS_CLIENT_CONFIG`, while, in fact, the correct variable is `TALK_REDIS_CLIENT_CONFIGURATION`, as it parsed in https://github.com/coralproject/talk/blob/master/config.js#L114
2017-12-28 13:04:37 +01:00
MEL27 ae0407ed5e Refine zh_CN translation 2017-12-26 12:08:47 +08:00
MEL27 a5d7bca8c0 Refine zh_CN translation 2017-12-26 00:20:58 +08:00
MEL27 8a40093399 Refine zh_CN translation 2017-12-25 22:22:46 +08:00
MEL27 b3d76bba05 Miscellaneous zh_CN translation changes 2017-12-25 17:45:03 +08:00
Kim Gardner e5a00d0793 Merge branch 'master' into master 2017-12-24 16:01:39 -05:00
MEL27 5f0f97edbb Refine locales/zh_CN.yml 2017-12-24 16:55:10 +08:00
MEL27 19c46d3b32 Refine Simplified Chinese translation 2017-12-24 12:51:19 +08:00
MEL27 5b356e6249 Add initial Simplified Chinese translation 2017-12-24 12:22:44 +08:00
Wyatt Johnson 20878ba040 css and standardization on all admin pages 2017-12-21 23:27:37 -07:00
Wyatt Johnson 69e4c5478d fixes default lang select bug 2017-12-21 23:26:34 -07:00
Kim Gardner 0d70c87ea9 Fix merge conflict 2017-12-21 17:01:48 -05:00
Wyatt Johnson d3ce68e078 Merge branch 'master' into next 2017-12-21 14:51:56 -07:00
Wyatt Johnson 4fa46c4144 Merge pull request #1154 from coralproject/user-status-refactor
User status refactor
2017-12-21 14:35:23 -07:00
Wyatt Johnson c2a8a3e4a3 fixed tests 2017-12-21 14:26:44 -07:00
Kim Gardner 01707a5150 Merge branch 'master' into safari-bs-changes 2017-12-21 15:26:31 -05:00
Kim Gardner 5488d5a7fa Merge pull request #1234 from coralproject/modqueue-enh
Modqueue fixes and enhancements
2017-12-21 15:26:09 -05:00
Wyatt Johnson c8ab5cdbf6 fixed tests 2017-12-21 12:43:26 -07:00
Wyatt Johnson 4fb9910221 updates to cli
- removed -c,--config flag in favor of env only or .env files
- loads .env always if found
- scripts re-written to call cli-serve directly
2017-12-21 12:03:34 -07:00
Wyatt Johnson 7ae0df2cbc fixed dist loading error 2017-12-21 11:55:09 -07:00
Wyatt Johnson 8bf2fe7b06 hid not logged in error 2017-12-21 11:51:16 -07:00
Wyatt Johnson c0f8fce442 fixed graphql error 2017-12-21 11:48:16 -07:00
Wyatt Johnson 47e32d5d6d Merge branch 'next' into user-status-refactor 2017-12-21 11:34:39 -07:00
Chi Vinh Le 111af0acf8 More refactor 2017-12-21 18:43:39 +01:00
Chi Vinh Le e34f96832a Refactor 2017-12-21 18:36:15 +01:00
Chi Vinh Le f9e289561e Refactor 2017-12-21 18:29:18 +01:00
Wyatt Johnson e409d071cb Merge branch 'master' into next 2017-12-21 09:37:39 -07:00
Chi Vinh Le 2d6a05433a Mark single dangling comments, reflow issues and scroll enhancement 2017-12-21 15:55:18 +01:00
Chi Vinh Le 3c97ce3f66 Merge branch 'safari-bs-changes' of ssh://github.com/coralproject/talk into safari-bs-changes 2017-12-21 14:12:43 +01:00
Chi Vinh Le f8c1733e14 Remove sendKey hack 2017-12-21 14:11:23 +01:00
Chi Vinh Le 2e726c9d6a Set window position 2017-12-21 14:10:28 +01:00
Kim Gardner 7f75255282 Merge branch 'master' of git+ssh://github.com/coralproject/talk into user-status-refactor 2017-12-21 07:04:27 -05:00
Kiwi 918b863820 Merge branch 'master' into safari-bs-changes 2017-12-21 12:47:22 +01:00
Chi Vinh Le 3c5991453d Merge branch 'safari-bs-changes' of ssh://github.com/coralproject/talk into safari-bs-changes 2017-12-21 12:32:33 +01:00
Chi Vinh Le 3776c4d050 Use private ip 2017-12-21 12:31:36 +01:00
okbel 5925a4b956 updated tests classes 2017-12-20 21:21:16 -03:00
okbel dafd703261 Merge branch 'user-status-refactor' of github.com:coralproject/talk into user-status-refactor
* 'user-status-refactor' of github.com:coralproject/talk:
  added mount path to the endpoint to respect the mounting config
  linting
  removed dev requirement for engine
2017-12-20 21:14:21 -03:00
okbel 5c0f546d50 Adding tests 2017-12-20 21:06:48 -03:00
okbel e8e9a4b5ed bounding fns 2017-12-20 21:06:37 -03:00
okbel 1371776f74 Updating the store mutations 2017-12-20 21:05:52 -03:00
Wyatt Johnson deb2709829 added mount path to the endpoint to respect the mounting config 2017-12-20 16:29:26 -07:00
okbel 4cc35e5b8f rejected usernames should not be able to comment 2017-12-20 20:13:36 -03:00
Wyatt Johnson ad68346332 linting 2017-12-20 16:11:39 -07:00
Wyatt Johnson 97406e1b18 removed dev requirement for engine 2017-12-20 16:10:02 -07:00
okbel 483b306e69 Updated tests 2017-12-20 20:09:51 -03:00
okbel 0bce215d33 Adding tests 2017-12-20 20:00:47 -03:00
okbel 73666399b4 Updating the store mutations 2017-12-20 20:00:34 -03:00
okbel 60e890c244 Adding Classes 2017-12-20 20:00:24 -03:00
okbel 16b5bd5d09 Updating the store mutations 2017-12-20 20:00:05 -03:00
okbel 37eafd2fed Adding Classes 2017-12-20 19:59:14 -03:00
Wyatt Johnson e2cb3fdd3d Merge pull request #1232 from coralproject/expiry-fix
Expiry Parsing Bug
2017-12-20 15:48:52 -07:00
Wyatt Johnson 2233d993c2 fixed expiry loading 2017-12-20 15:27:55 -07:00
okbel 5d51f71f0c Test 2017-12-20 18:48:43 -03:00
okbel a989a0b20e Adding Classes to the PageObject 2017-12-20 18:48:30 -03:00
okbel 4f3a400358 Classes for AccountHistory 2017-12-20 18:48:10 -03:00
okbel f67f1e3b52 Adding role, instead of roles. And disable banning and suspending for the user thats logged in 2017-12-20 18:10:13 -03:00
okbel 570c0edfb3 merge conficts 2017-12-20 16:06:55 -03:00
Kiwi c1b5216647 Merge branch 'master' into safari-bs-changes 2017-12-20 18:08:52 +01:00
Wyatt Johnson b2cc4bdb12 Merge pull request #1231 from coralproject/subscription-hooks
Subscription Connection Plugin Hooks
2017-12-20 10:07:21 -07:00
Wyatt Johnson 0a793938d6 Merge branch 'master' into subscription-hooks 2017-12-20 09:51:10 -07:00
Chi Vinh Le 7d71452ab5 Add ip package 2017-12-20 17:45:03 +01:00
Chi Vinh Le 29a60c1241 Use local IP instead of localhost 2017-12-20 17:41:55 +01:00
Wyatt Johnson b46b392af4 added subscription hooks 2017-12-20 09:31:59 -07:00
Wyatt Johnson 998cb8d380 Merge pull request #1227 from coralproject/body-parser
Deprecate body-parser
2017-12-20 08:58:10 -07:00
Kim Gardner b8ffe25212 Merge branch 'master' into next 2017-12-20 09:48:18 -05:00
Wyatt Johnson df0ab7c1dc removed body-parser in favor of express.json() 2017-12-19 15:11:11 -07:00
Kiwi 519464ce57 Merge pull request #1226 from coralproject/handle-empty-queue
Fix error when queue is empty
2017-12-19 20:58:34 +01:00
Chi Vinh Le f27de815e6 Minor syntax issue 2017-12-19 20:48:59 +01:00
Chi Vinh Le e8553c5035 Revert debug code :-D 2017-12-19 20:46:59 +01:00
Chi Vinh Le 349341400d Fix error when queue is empty 2017-12-19 20:39:41 +01:00
Kim Gardner 7b92e58ff3 Merge pull request #1225 from coralproject/portal-dialog
Use Portal to put dialogs at the end of the document
2017-12-19 13:48:30 -05:00
Chi Vinh Le f2f7742709 Adapt e2e 2017-12-19 19:40:16 +01:00
Chi Vinh Le 689fb4baf0 Remove z-index 2017-12-19 19:26:22 +01:00
Chi Vinh Le 650f98a080 Merge branch 'portal-dialog' of ssh://github.com/coralproject/talk into portal-dialog 2017-12-19 19:25:35 +01:00
Chi Vinh Le 88d3a53165 Make backdrop work 2017-12-19 19:24:39 +01:00
Kiwi 6e88976294 Merge branch 'master' into portal-dialog 2017-12-19 19:17:50 +01:00
Chi Vinh Le f25cc56ddb Use Portal to put dialogs to the end of the document 2017-12-19 19:15:45 +01:00
Kiwi 4134b2edb2 Merge pull request #1222 from coralproject/app-plugin
Added support for app plugin hook
2017-12-19 19:08:27 +01:00
Kiwi 5b886b6eea Merge branch 'master' into app-plugin 2017-12-19 18:57:31 +01:00
Kiwi 6c80de2981 Merge pull request #1224 from coralproject/e2e-stability
Increase e2e stability
2017-12-19 18:57:17 +01:00
Kiwi b9b3355317 Merge branch 'master' into e2e-stability 2017-12-19 18:49:35 +01:00
Chi Vinh Le 88e9faea00 Increase e2e stability 2017-12-19 18:48:44 +01:00
Kiwi b1c9fbedd8 Merge branch 'master' into app-plugin 2017-12-19 18:37:01 +01:00
Kiwi c9b7eb3494 Merge pull request #1223 from coralproject/fix-dialog
Fix dialog not rendering fully in firefox
2017-12-19 18:34:49 +01:00
Chi Vinh Le 66287f5b7d Fix dialog not rendering fully in firefox 2017-12-19 18:27:19 +01:00
Kiwi b61eeeba74 Merge branch 'master' into app-plugin 2017-12-19 18:02:43 +01:00
Kim Gardner 2bde40777d Merge branch 'master' into next 2017-12-19 11:44:41 -05:00
Kim Gardner 2179632c94 Merge pull request #1221 from coralproject/e2e-fixes
E2e improvements and fixes
2017-12-19 11:42:10 -05:00
Wyatt Johnson 4342161a70 Merge branch 'master' into app-plugin 2017-12-19 09:31:13 -07:00
Kim Gardner a48688ad35 Merge branch 'master' into e2e-fixes 2017-12-19 11:18:28 -05:00
Kim Gardner 1b4f783abd Merge pull request #1208 from coralproject/virtualized
Mod Queue improvements
2017-12-19 11:18:09 -05:00
Wyatt Johnson e2d8562868 added support for app plugin hook 2017-12-19 09:13:01 -07:00
Kim Gardner 91602b9d34 Merge branch 'master' into virtualized 2017-12-19 10:33:22 -05:00
Kim Gardner eba530eda0 Merge branch 'master' into e2e-fixes 2017-12-19 10:33:18 -05:00
Kim Gardner 8e1cfc3a29 Merge pull request #1219 from coralproject/bump-version-3-9-0
Version 3.9.0
2017-12-19 10:33:05 -05:00
Chi Vinh Le c78bb598e7 Merge branch 'e2e-fixes' of ssh://github.com/coralproject/talk into e2e-fixes 2017-12-19 16:27:20 +01:00
Chi Vinh Le f12abbf39e Default timeout to 10000 for ci 2017-12-19 16:26:55 +01:00
Kiwi 7dcb1dfbaa Merge branch 'master' into e2e-fixes 2017-12-19 16:16:59 +01:00
Chi Vinh Le 1fce2451c7 Remove some blank lines 2017-12-19 16:16:59 +01:00
Chi Vinh Le e4f1f5d08b Allow disabling e2e in ci 2017-12-19 16:14:59 +01:00
Chi Vinh Le d06ff8c93c Double default WaitForTimeout 2017-12-19 15:56:54 +01:00
Kim Gardner 9d9056ef86 Merge branch 'master' into bump-version-3-9-0 2017-12-19 08:28:01 -06:00
Kim Gardner 1b61ca8abf Merge branch 'master' into virtualized 2017-12-19 08:27:58 -06:00
Kim Gardner f75a743090 Merge pull request #1220 from coralproject/schema-level-plugin-support
Schema + Connector Plugin Support
2017-12-19 08:27:32 -06:00
Chi Vinh Le 82e617b19b Enable firefox and edge 2017-12-19 14:48:58 +01:00
Chi Vinh Le 7a4b87f743 Fix popup issues 2017-12-19 14:42:54 +01:00
Chi Vinh Le 71f0887a82 Make browsers configurable through ENV E2E_BROWSERS 2017-12-19 14:42:29 +01:00
Chi Vinh Le 4116e206a3 Configurable timeout 2017-12-19 14:41:51 +01:00
Chi Vinh Le e78f17460f Fix wrong spec 2017-12-19 14:27:34 +01:00
Kim Gardner 70bfd91359 Merge branch 'master' into virtualized 2017-12-18 17:37:04 -06:00
Kim Gardner b3432a5e0d Merge branch 'master' into bump-version-3-9-0 2017-12-18 17:36:56 -06:00
Wyatt Johnson 382cb42dcf Merge branch 'master' into schema-level-plugin-support 2017-12-18 16:27:38 -07:00
Wyatt Johnson dbd106d523 added support for plugging into schema level resolve 2017-12-18 16:24:28 -07:00
Kim Gardner e8f94d4d4a Merge pull request #1209 from coralproject/i18n-serverside-plugins
i18n + Email Support
2017-12-18 17:03:47 -06:00
Kim Gardner 3ea141d4b0 Merge branch 'master' into i18n-serverside-plugins 2017-12-18 16:44:23 -06:00
Kim Gardner f783c8423d Merge pull request #1211 from coralproject/premod-admin-comments
Fix new admin comments not showing up in premod
2017-12-18 16:44:15 -06:00
Kim Gardner 9d4a9861f2 Merge branch 'master' into premod-admin-comments 2017-12-18 16:34:13 -06:00
Kim Gardner 6d69286df6 Merge pull request #1214 from coralproject/confirmation-featured
Confirmation Dialog for Featuring Comments on the Admin Side
2017-12-18 16:34:01 -06:00
Kim Gardner 3121ffb55a Version 3.9.0 2017-12-18 17:31:25 -05:00
Kim Gardner c0af51fbc0 Merge branch 'master' into confirmation-featured 2017-12-18 16:27:00 -06:00
Kim Gardner ad85b0fcc8 Merge pull request #1216 from coralproject/change-permalink-share
Update permalink copy to "Share" on stream
2017-12-18 16:26:43 -06:00
Kim Gardner 5ad73b1c14 Merge branch 'master' into change-permalink-share 2017-12-18 16:17:31 -06:00
Kim Gardner bf83f462e0 Merge pull request #1217 from coralproject/fix-login-profile
Fix sign in on profile tab
2017-12-18 16:17:19 -06:00
Kim Gardner 8dd79a19c3 Merge branch 'master' into fix-login-profile 2017-12-18 16:09:37 -06:00
Kim Gardner a8368eb78d Merge pull request #1218 from coralproject/ci-fix
Disabling Firefox/Edge on E2E until fix pass
2017-12-18 13:58:02 -06:00
Wyatt Johnson e88bac6e0f Merge branch 'master' into ci-fix 2017-12-18 12:45:40 -07:00
Kim Gardner 281d1f65f1 Merge branch 'master' into i18n-serverside-plugins 2017-12-18 13:44:20 -06:00
Kim Gardner 2704d71b09 Merge branch 'master' into fix-login-profile 2017-12-18 13:43:45 -06:00
Wyatt Johnson 583ab0ff74 disabling firefox,edge 2017-12-18 12:42:58 -07:00
Kim Gardner ad5c0444f7 Merge branch 'master' into virtualized 2017-12-18 12:43:16 -06:00
Belén Curcio 2a74e75506 Merge branch 'master' into confirmation-featured 2017-12-18 15:39:55 -03:00
Wyatt Johnson fb7ad76b8b Merge pull request #1215 from coralproject/ci-fix
*Fixes for sorted window handler
2017-12-18 11:36:47 -07:00
Kiwi eddb471366 Merge branch 'master' into virtualized 2017-12-18 19:29:35 +01:00
okbel 35c88ffab4 spreading the rest first 2017-12-18 15:21:02 -03:00
okbel 1cdef8f864 bounded 2017-12-18 15:20:06 -03:00
Kim Gardner 82fd56dc67 Update permalink copy to Share on stream 2017-12-18 13:09:39 -05:00
Chi Vinh Le 0e01d40aa7 Fix sign in on profile tab 2017-12-18 19:01:43 +01:00
Wyatt Johnson f7d1543aa3 fixes for sorted window handler 2017-12-18 10:24:11 -07:00
Belén Curcio d2ebc81e25 Merge branch 'master' into confirmation-featured 2017-12-18 13:52:01 -03:00
Belen Curcio 278e1cec1d done 2017-12-18 13:42:59 -03:00
Kim Gardner 31436e627d Merge branch 'master' into i18n-serverside-plugins 2017-12-18 10:39:27 -06:00
Kim Gardner 4e50865927 Merge branch 'master' into premod-admin-comments 2017-12-18 10:39:06 -06:00
Belen Curcio 6ad218782e styling 2017-12-18 13:39:02 -03:00
Belen Curcio 8f1f875d76 less data to the store 2017-12-18 13:36:30 -03:00
Belen Curcio 9fb8e01cd2 working, still wip 2017-12-18 13:34:48 -03:00
Kim Gardner 950a35310b Merge pull request #1212 from coralproject/user-detail-load-more-fix
Fix load more in UserDetail issue
2017-12-18 10:23:50 -06:00
Kiwi 12884cce01 Merge branch 'master' into i18n-serverside-plugins 2017-12-18 17:12:54 +01:00
Kiwi f8d153cf95 Merge branch 'master' into virtualized 2017-12-18 17:12:49 +01:00
Kiwi c871f1c797 Merge branch 'master' into premod-admin-comments 2017-12-18 17:12:41 +01:00
Kiwi 9af7ae0357 Merge branch 'master' into user-detail-load-more-fix 2017-12-18 17:12:36 +01:00
Wyatt Johnson 073eea2847 Merge pull request #1213 from coralproject/version-bump
Version Bump
2017-12-18 09:11:52 -07:00
Kiwi 9cb6ff3847 Merge branch 'master' into virtualized 2017-12-18 16:59:31 +01:00
Wyatt Johnson c699e044fd Update package.json 2017-12-18 08:55:49 -07:00
Kiwi 7d3ec7da1e Merge branch 'master' into premod-admin-comments 2017-12-18 16:55:38 +01:00
Belen Curcio 5d288da69d Merge branch 'master' of github.com:coralproject/talk into confirmation-featured 2017-12-18 12:55:31 -03:00
Kiwi 4ef7abc132 Merge branch 'master' into user-detail-load-more-fix 2017-12-18 16:55:31 +01:00
Wyatt Johnson 9e486f3a9d Merge pull request #1210 from coralproject/redis-optim
Redis Optimization Pass
2017-12-18 08:55:18 -07:00
Chi Vinh Le fbcd390220 Fix load more issue 2017-12-18 16:52:57 +01:00
okbel 338c23dc3d unnecesary class 2017-12-18 12:46:24 -03:00
okbel 4d6f38a3d9 wip 2017-12-18 12:43:46 -03:00
Chi Vinh Le 5b8f335b2a Fix new admin comments not showing up in premod 2017-12-18 16:41:05 +01:00
Wyatt Johnson f0330ed21c fixed version paths 2017-12-18 08:35:17 -07:00
okbel 02da749c65 Adding containers and components 2017-12-18 12:20:20 -03:00
okbel 5a1393c039 Adding translations 2017-12-18 12:20:06 -03:00
okbel d7a5e613fa Adding reducer 2017-12-18 12:19:46 -03:00
Wyatt Johnson 75a944eba8 upgrades, fixes to comment count logic 2017-12-18 08:02:43 -07:00
Chi Vinh Le 9a0fb95d06 Pagination fixes 2017-12-18 15:03:54 +01:00
Wyatt Johnson 31655847a9 Merge branch 'next' into user-status-refactor 2017-12-16 14:58:59 -06:00
Wyatt Johnson 6655d06d80 Merge branch 'master' into next 2017-12-16 14:54:25 -06:00
Wyatt Johnson 6850dd1a53 redis optim
- removes call for comment count when we already have
  totalCommentCount
- removes job processor cleaner on non-processing nodes
- fixed some template issues
2017-12-15 15:49:56 -06:00
Wyatt Johnson ecb01224fd Merge branch 'master' into i18n-serverside-plugins 2017-12-15 10:40:08 -06:00
Wyatt Johnson fdd5cbb0d7 moved email contents to translation files 2017-12-14 16:56:24 -06:00
Wyatt Johnson eb9827a3ae more fixes for i18n 2017-12-14 16:48:03 -06:00
Wyatt Johnson d207b240f2 added support for changing the subject prefi 2017-12-14 16:28:08 -06:00
Wyatt Johnson d9654b52cf some fixes 2017-12-14 16:09:41 -06:00
Wyatt Johnson d3965924aa fixed template 2017-12-14 15:51:53 -06:00
Wyatt Johnson 36d535546c plugins serverside can now provide translations 2017-12-14 15:03:35 -06:00
Chi Vinh Le 19568afde5 Reduce view more movements 2017-12-14 21:08:13 +01:00
Chi Vinh Le 2b312dbf8e Next try 2017-12-14 20:41:05 +01:00
Chi Vinh Le 5446abe3a9 Fix bug with featured comments 2017-12-14 20:37:51 +01:00
Chi Vinh Le c66c100dcb Cache callbacks to preserve immutability 2017-12-14 20:27:19 +01:00
Chi Vinh Le d4051f6828 Fix view more styling 2017-12-14 20:08:09 +01:00
Chi Vinh Le 33781c3d16 Fix view new comments 2017-12-14 20:01:33 +01:00
Chi Vinh Le defc4049cb Sort correctly + refactors and comments 2017-12-14 19:47:42 +01:00
Chi Vinh Le 243d0c1493 Limit to 100 comments 2017-12-14 18:33:49 +01:00
Kiwi a448c69a2a Merge branch 'master' into virtualized 2017-12-14 18:32:28 +01:00
Chi Vinh Le 0b774402cf Introduce dangling comments and queue cleanup 2017-12-14 18:19:00 +01:00
Chi Vinh Le 0bda44e97f Fix cursor bug 2017-12-14 16:51:50 +01:00
Chi Vinh Le 60f6c63c8a Restrict notifications to comments which are rendered 2017-12-14 16:29:31 +01:00
Chi Vinh Le 62c2d551f8 Append comments only 2017-12-14 15:57:44 +01:00
Chi Vinh Le f32bb5ce8f Small adjustments 2017-12-14 13:55:08 +01:00
Chi Vinh Le 5e6d22f1af Handle removing comments from queue 2017-12-14 13:38:01 +01:00
Chi Vinh Le 34289e3340 Refactor view 2017-12-14 12:35:26 +01:00
Wyatt Johnson 98ef60a48e Merge pull request #1205 from stephendonner/patch-1
s/setup/setup
2017-12-13 12:34:30 -06:00
Wyatt Johnson 8323344d7d Merge branch 'master' into patch-1 2017-12-13 12:26:24 -06:00
Wyatt Johnson fbecc363cf Merge pull request #1206 from coralproject/feature/infer-asset-url
Fix logic to generate asset_url even if the asset_id is provided
2017-12-13 12:25:27 -06:00
Chi Vinh Le 2779734ec0 Workaround 2017-12-13 15:25:47 +01:00
Chi Vinh Le 5548deb6b5 Auto Load More and other bug fixes 2017-12-13 15:06:01 +01:00
Jeff Nelson df925f5a1a Fix logic to generate asset_url even if the asset_id is provided so
an error is avoided
2017-12-13 07:38:10 -06:00
Stephen Donner 30073b88f1 s/setup/setup 2017-12-13 03:49:35 -06:00
Chi Vinh Le 51bd109f13 Fix some bugs 2017-12-12 17:28:38 +01:00
Chi Vinh Le b12d5f6bd7 Reimplement keyboard navigation 2017-12-12 17:19:11 +01:00
Chi Vinh Le f32e4523e8 Make view more coments work with virtualized 2017-12-12 12:53:54 +01:00
Wyatt Johnson fadf3b5106 Fixed import error 2017-12-11 15:32:00 -06:00
Chi Vinh Le ed959ca07a Integrate react-virtualized 2017-12-11 20:21:27 +01:00
Belen Curcio 588c1c201c Avoiding refetch from Community 2017-12-11 11:37:35 -03:00
Belen Curcio f005d1e5f8 Styling, refactor and instant update after banning and unbanning from community 2017-12-11 11:33:26 -03:00
Belen Curcio 1d12c3629a Adding utils, and replacing REST old endpoints 2017-12-11 10:21:56 -03:00
Belen Curcio a1a0671382 unBan Update 2017-12-11 09:36:42 -03:00
Belen Curcio e66e23bbf6 Updated with User-Status-Refactor API - User Detail Updated 2017-12-11 09:10:50 -03:00
Belen Curcio 26abd02032 Updated with User-Status-Refactor API 2017-12-11 08:49:22 -03:00
Belen Curcio 8e582bff45 Adding unSuspendUser mutation 2017-12-11 08:37:09 -03:00
Belen Curcio 8df4b79f97 Updated with User-Status-Refactor API 2017-12-11 08:33:40 -03:00
Wyatt Johnson 27ecbd1bdd Merge pull request #1204 from coralproject/document-optimization-reapply
Reapply optimizations
2017-12-08 16:01:47 -07:00
Chi Vinh Le 4477c0981f Reapply optimizations 2017-12-08 23:38:10 +01:00
Wyatt Johnson 2c9eae9264 Merge pull request #1202 from coralproject/fix-load-more-replies
Fix load more replies
2017-12-08 15:19:27 -07:00
Wyatt Johnson 812a4ee704 Update package.json 2017-12-08 15:19:01 -07:00
Wyatt Johnson d3caf350ac Merge branch 'master' into fix-load-more-replies 2017-12-08 15:18:35 -07:00
Wyatt Johnson 864e3f1a1f Merge pull request #1203 from coralproject/revert-1201-document-optimization-v2
Revert "Optimizations v2"
2017-12-08 15:18:23 -07:00
Wyatt Johnson 61d16fbe08 Revert "Optimizations v2" 2017-12-08 15:17:28 -07:00
Wyatt Johnson 000e26e3e1 Merge pull request #1201 from coralproject/document-optimization-v2
Optimizations v2
2017-12-08 15:15:47 -07:00
Chi Vinh Le 71f0200557 Fix load more replies 2017-12-08 23:14:40 +01:00
Wyatt Johnson d79edcd412 Merge branch 'master' into document-optimization-v2 2017-12-08 15:08:15 -07:00
Wyatt Johnson 485b9d036f version bump 2017-12-08 15:07:15 -07:00
Wyatt Johnson 041ba95a5b Merge pull request #1199 from coralproject/less-lazy-assets
Less Lazy Assets
2017-12-08 15:06:33 -07:00
Wyatt Johnson d0d047f641 fixed bug with load more 2017-12-08 15:04:33 -07:00
Wyatt Johnson b057131d34 Merge branch 'master' into less-lazy-assets 2017-12-08 13:59:15 -07:00
Chi Vinh Le ba572fbf48 Typo 2017-12-07 20:24:45 +01:00
Chi Vinh Le e3c1f8c231 Merge branch 'document-optimization-v2' of ssh://github.com/coralproject/talk into document-optimization-v2 2017-12-07 20:23:38 +01:00
Chi Vinh Le 07ddb94327 Fix yarn.lock 2017-12-07 20:23:05 +01:00
Kiwi 0addb938ae Merge branch 'master' into document-optimization-v2 2017-12-07 19:54:17 +01:00
Wyatt Johnson c0d01e55e1 Merge pull request #1200 from coralproject/delay-subscription-handler
Delay throttled subscription handler until query has loaded
2017-12-07 11:44:37 -07:00
Chi Vinh Le 3ac301142e Update graphql-ast-tools 2017-12-07 19:43:32 +01:00
Wyatt Johnson 74c90d43a7 linting patch 2017-12-07 11:07:14 -07:00
Wyatt Johnson 454fa752e2 fixed template errors 2017-12-07 11:05:42 -07:00
Chi Vinh Le 7072aa013b document optimizations v2 2017-12-07 18:41:46 +01:00
Belen Curcio 83d4408ddf merge 2017-12-07 13:55:40 -03:00
Chi Vinh Le 091be25f36 Delay throttled subscription handler until query has loaded 2017-12-07 16:55:36 +01:00
Belen Curcio 1322db89bc Merge branch 'actions-user-drawer' of github.com:coralproject/talk into user-status-refactor 2017-12-07 12:36:53 -03:00
Wyatt Johnson 41a3639d31 Merge branch 'master' into less-lazy-assets 2017-12-06 16:33:13 -07:00
Wyatt Johnson f96146bfd9 Merge pull request #1195 from coralproject/graphql-ast-tools
Use graphql-ast-tools
2017-12-06 16:32:59 -07:00
Wyatt Johnson 2eec87aa80 modified asset loading to be less lazy 2017-12-06 16:10:42 -07:00
Wyatt Johnson 6f4f69ac3e Merge branch 'next' into user-status-refactor 2017-12-06 09:03:38 -07:00
Chi Vinh Le aabd40d53a Remove apollo-utilities 2017-12-06 11:45:16 +01:00
Wyatt Johnson 14b5ccb53e fixed small bug 2017-12-05 15:14:57 -07:00
Wyatt Johnson 2e4229ce18 removed reference 2017-12-05 15:14:57 -07:00
Wyatt Johnson 6bc9e6a0bb added support for deep route caching, settings cache, template optim 2017-12-05 15:14:57 -07:00
Kim Gardner a0ee5c6414 Update CSS classname 2017-12-05 14:38:43 -07:00
Wyatt Johnson 48ef9b162d Merge branch 'master' into next 2017-12-05 14:02:09 -07:00
Wyatt Johnson 9bce69a7cd Merge branch 'master' into graphql-ast-tools 2017-12-05 13:41:48 -07:00
Kim Gardner 7afa6c018b Merge pull request #1196 from coralproject/logic-fix
Staff Configuration Fix
2017-12-05 19:51:00 +00:00
Wyatt Johnson 94b56b547f fixed small bug 2017-12-05 12:31:36 -07:00
Kiwi 6329e9f618 Merge branch 'master' into graphql-ast-tools 2017-12-05 19:52:27 +01:00
Kim Gardner ea7266595d Merge pull request #1194 from coralproject/perf-pass
Performance Pass
2017-12-05 18:44:31 +00:00
Kiwi 4f2d97cd82 Merge branch 'master' into graphql-ast-tools 2017-12-05 19:42:24 +01:00
Chi Vinh Le 6a00d3d249 Remove unused code 2017-12-05 19:40:53 +01:00
Chi Vinh Le d3040804a2 Upgrade graphql-anywhere-optimized and graphql-ast-tools 2017-12-05 19:39:14 +01:00
Wyatt Johnson 24596a48bd Merge branch 'master' into perf-pass 2017-12-05 11:29:21 -07:00
Chi Vinh Le 04ae844a1a Use graphql-ast-tools 2017-12-05 19:25:32 +01:00
Wyatt Johnson a8a8455f40 removed reference 2017-12-05 11:23:36 -07:00
Wyatt Johnson d7302e4b3c added support for deep route caching, settings cache, template optim 2017-12-05 11:17:44 -07:00
Kim Gardner f09ca6d060 Merge pull request #1193 from coralproject/fix-incorrect-class-reason-popup
Update CSS classname
2017-12-05 17:57:43 +00:00
Kim Gardner 73f24fd75b Update CSS classname 2017-12-05 17:16:39 +00:00
Wyatt Johnson 2d70caf03d Merge branch 'next' into user-status-refactor 2017-12-04 15:42:57 -07:00
Wyatt Johnson 419784f6d1 Merge branch 'master' into next 2017-12-04 15:28:41 -07:00
Wyatt Johnson 001beb7a5b Merge pull request #1186 from coralproject/performance-enhancements
Performance enhancements
2017-12-04 15:16:57 -07:00
Chi Vinh Le 0b04a011c4 Merge branch 'performance-enhancements' of ssh://github.com/coralproject/talk into performance-enhancements 2017-12-04 22:50:06 +01:00
Chi Vinh Le 00fb2e277b Merge branch 'master' into performance-enhancements 2017-12-04 22:49:06 +01:00
Chi Vinh Le a79dd76f4b Use graphql document optimizations only in production 2017-12-04 22:46:33 +01:00
Chi Vinh Le 0d91396b1c Remove death star button 2017-12-04 22:45:23 +01:00
Wyatt Johnson e1324a168b Merge pull request #1191 from coralproject/graph-flags
Graph flags
2017-12-04 14:44:48 -07:00
Kiwi af9180f114 Merge branch 'master' into graph-flags 2017-12-04 22:28:36 +01:00
Wyatt Johnson 8f5e54520b Merge branch 'master' into performance-enhancements 2017-12-04 14:26:29 -07:00
Chi Vinh Le 2061a7b97d Use @coralproject/graphql-anywhere-optimized 2017-12-04 22:21:40 +01:00
Wyatt Johnson 3b4e69f06e fixed based on e2e finding a bug! 2017-12-04 14:06:32 -07:00
Kim Gardner f71be09bca Merge pull request #1189 from coralproject/user-badges-fix
User Badges Styling Fix
2017-12-04 19:31:17 +00:00
Wyatt Johnson 87ef0f1cb9 Adjusted flag beheviour for staff 2017-12-04 12:17:39 -07:00
Belen Curcio 0640be2a77 Timestamp no break 2017-12-04 15:00:43 -03:00
Belen Curcio fdf11fa277 Adding extra container and styling 2017-12-04 13:42:22 -03:00
Wyatt Johnson 8b71175c46 adjusted logic flow for comment flags 2017-12-04 09:27:56 -07:00
Wyatt Johnson 674ab19d06 Adjusted mongo indexes to support index scanning 2017-12-04 09:05:40 -07:00
Belen Curcio c53fe4b197 Updated tests and Drawer component 2017-12-04 00:37:41 -03:00
Belen Curcio 67c81c0168 Adding react-mdl 2017-12-03 23:16:54 -03:00
Belen Curcio e44b37538e wip 2017-12-03 16:39:48 -03:00
Belen Curcio 56beac9a1f Adding old user status 2017-12-03 16:31:08 -03:00
Belen Curcio 0596547223 Adding old user status 2017-12-03 16:29:12 -03:00
Belen Curcio ca6e839ca4 UserInfoTooltip, styling and func 2017-12-03 16:19:59 -03:00
Belen Curcio 6557c6036f Functionality and styling 2017-12-03 15:48:37 -03:00
Belen Curcio a3d83efd7d Styling and functionality 2017-12-03 15:01:06 -03:00
Belen Curcio 0483aae369 Moving ActionsMenu 2017-12-03 14:54:28 -03:00
Belen Curcio 6da4439b94 Adding header container 2017-12-02 00:20:59 -03:00
Belen Curcio 7731ec42de css tag label 2017-12-01 23:43:19 -03:00
Belen Curcio 67a4d63a23 Good margin 2017-12-01 15:31:05 -03:00
Belen Curcio ce9ee0a5b0 Adding styles for mob devs 2017-12-01 15:28:09 -03:00
Kiwi 2e8f5f2e69 Merge branch 'master' into performance-enhancements 2017-12-01 17:57:24 +01:00
Chi Vinh Le 099186d030 Make polling interval longer 2017-12-01 17:56:53 +01:00
Kim Gardner 99daa0b2c9 Merge pull request #1187 from coralproject/remove-dash
Remove Dashboard for the interim
2017-12-01 16:27:53 +00:00
Chi Vinh Le c2b488fa98 Don't use murmurhash 2017-12-01 17:11:38 +01:00
Chi Vinh Le 612d2596f9 Turn NamedFragments with directives always into InlineFragments 2017-12-01 17:02:45 +01:00
Chi Vinh Le 835d30d362 Put document optimizations behind a option 2017-12-01 16:54:45 +01:00
Chi Vinh Le 015e3d3ed9 Tiny refactor 2017-12-01 16:47:54 +01:00
Chi Vinh Le 4c7c852af7 Only merge definitions with directives when they are the same 2017-12-01 16:44:01 +01:00
Chi Vinh Le 72123cabe7 Rename getDefinitionName to getDefinitionID 2017-12-01 16:33:38 +01:00
Chi Vinh Le f17b296e2e Cache resolved fragments (globally) 2017-12-01 16:31:26 +01:00
Chi Vinh Le 2c72975137 Preoptimize mutation, fragments and batched subscription documents 2017-12-01 15:42:13 +01:00
Chi Vinh Le 5c1314165c Upgrade graphql-anywhere-optimized 2017-12-01 14:16:32 +01:00
Chi Vinh Le fd83fed7bb Preoptimize queries and subscriptions documents 2017-12-01 14:10:38 +01:00
Kim Gardner dff89ec6cd Remove Dashboard docs 2017-12-01 12:53:17 +00:00
Wyatt Johnson ef200a85cb removed backend for metrics 2017-11-30 17:16:32 -07:00
Wyatt Johnson cbf3e4734d improved query performance with indexes, added engine for debugging 2017-11-30 16:40:58 -07:00
Kim Gardner 9c0e6ccb36 Remove Dashboard 2017-11-30 23:21:01 +00:00
Kim Gardner 64276cd157 Remove Dashboard 2017-11-30 23:20:01 +00:00
Chi Vinh Le 214de045fb One subscription queue per query 2017-11-30 22:44:29 +01:00
Chi Vinh Le 92a9a535c6 Merge branch 'master' into performance-enhancements 2017-11-30 22:23:11 +01:00
Chi Vinh Le 3db7439d76 Use custom optimized graphql-anywhere 2017-11-30 21:55:16 +01:00
Chi Vinh Le 65be2b89a4 Throttle instead of debounce 2017-11-30 21:44:32 +01:00
Kim Gardner c9f6ec327a Merge pull request #1185 from coralproject/qbox-fix
Fix Stream: Question Box
2017-11-30 19:33:52 +00:00
Chi Vinh Le 06ddb30d77 Decouple apollo store 2017-11-30 20:26:08 +01:00
Belen Curcio af3542aec5 adding and enabling question box 2017-11-30 15:30:21 -03:00
Wyatt Johnson 0c484954df Merge branch 'next' into user-status-refactor 2017-11-30 11:29:44 -07:00
Wyatt Johnson b672b0a4e0 added better compression support 2017-11-30 11:20:13 -07:00
Kim Gardner 016eb472eb Merge pull request #1184 from coralproject/jobs-stream-fix
Jobs + Pending Box Fixes
2017-11-30 17:06:56 +00:00
Chi Vinh Le ab772839c5 Debounce and batch subscription updates 2017-11-30 16:38:38 +01:00
Belen Curcio c84408004c Updated comment user status path 2017-11-30 11:36:32 -03:00
Belen Curcio 4b0d64dfc6 New path 2017-11-30 11:18:34 -03:00
Belen Curcio a78bff5ee6 Merge branch 'master' of github.com:coralproject/talk into user-status-refactor 2017-11-30 11:12:53 -03:00
Kim Gardner a8c0ff0c0a Merge branch 'master' into next 2017-11-30 11:41:36 +00:00
Wyatt Johnson 1b9c2404db fixed job cli, reverted comment box pending beheviour 2017-11-29 15:57:28 -07:00
Chi Vinh Le cf96d07a7a Better death star 2017-11-29 11:54:51 +01:00
Kim Gardner 1c01a3320f Merge pull request #1179 from coralproject/firefox-hover-tab
Firefox hover tab
2017-11-28 20:16:22 +00:00
Wyatt Johnson 8d018df0d1 Merge branch 'master' into firefox-hover-tab 2017-11-28 13:05:50 -07:00
Wyatt Johnson fc824b1fbe Merge branch 'master' into next 2017-11-28 12:58:58 -07:00
Kim Gardner b2a2b135a4 Merge pull request #1173 from coralproject/refactor-configure
Rewrite/Refactor stream configure
2017-11-28 19:49:49 +00:00
Kim Gardner ccd22ea334 Merge branch 'master' of git+ssh://github.com/coralproject/talk into refactor-configure 2017-11-28 19:40:22 +00:00
Kim Gardner 83910f7fce Merge pull request #1163 from coralproject/extendable-embed-stream-tab
Extendable embed stream tabs
2017-11-28 19:39:36 +00:00
Belen Curcio 321f3117a4 Linting 2017-11-28 15:08:04 -03:00
Belen Curcio 09839ab024 Updating after approving a user 2017-11-28 15:00:01 -03:00
Belen Curcio da2b9aca03 Removing ternary 2017-11-28 14:56:29 -03:00
Belen Curcio 20d0cbaf27 Wrong filename 2017-11-28 14:52:31 -03:00
Belen Curcio c177403628 Merge branch 'user-status-refactor' of github.com:coralproject/talk into user-status-refactor 2017-11-28 14:37:50 -03:00
Belen Curcio b7704fb2ca Translations and Styling 2017-11-28 14:37:23 -03:00
Wyatt Johnson 2437401daf Merge branch 'next' into user-status-refactor 2017-11-28 09:47:34 -07:00
Wyatt Johnson 7f189cd4d4 Merge branch 'master' into next 2017-11-28 09:47:21 -07:00
Wyatt Johnson 28873ed961 Merge branch 'master' into refactor-configure 2017-11-28 09:46:11 -07:00
Belen Curcio 16e3924d3f Ban user dialog steps 2017-11-28 13:39:49 -03:00
Kim Gardner e3febf4a0c Merge branch 'master' into firefox-hover-tab 2017-11-28 16:32:57 +00:00
Kim Gardner c3b25d2d0a Merge branch 'master' into extendable-embed-stream-tab 2017-11-28 16:31:50 +00:00
Kim Gardner 96a375e38a Merge pull request #1183 from coralproject/bump-version-3-8-1
Version 3.8.1
2017-11-28 16:30:45 +00:00
Wyatt Johnson 417a7a3c36 added excemption as no fix available as of yet 2017-11-28 09:16:33 -07:00
Wyatt Johnson c903d8c0f9 Merge branch 'master' into bump-version-3-8-1 2017-11-28 08:44:46 -07:00
Belen Curcio d510905ba2 Adding ban message and setRole 2017-11-28 00:18:09 -03:00
Wyatt Johnson d4889eb52c more role fixes 2017-11-27 16:44:40 -07:00
Wyatt Johnson f2fe095b43 fixed bug with featuring comments doesn't work 2017-11-27 16:27:43 -07:00
Wyatt Johnson 13865ff29e roles -> role, null -> COMMENTER for user roles 2017-11-27 11:53:40 -07:00
Belen Curcio 9386912b9b Approve Username fix 2017-11-27 14:43:04 -03:00
Kim Gardner 24e895460b Merge branch 'master' into firefox-hover-tab 2017-11-27 17:23:36 +00:00
Kim Gardner d6a83201ad Merge branch 'master' into extendable-embed-stream-tab 2017-11-27 17:21:24 +00:00
Kim Gardner bbdde50858 Merge pull request #1177 from coralproject/graph-context-connectors
Graph Context Connectors
2017-11-27 17:20:48 +00:00
Kim Gardner 8730ada87c Merge branch 'master' into extendable-embed-stream-tab 2017-11-27 16:30:22 +00:00
Kim Gardner 506b938536 Merge branch 'master' into firefox-hover-tab 2017-11-27 16:30:12 +00:00
Kim Gardner 043e722d95 Merge branch 'master' into graph-context-connectors 2017-11-27 16:30:04 +00:00
Kim Gardner fb1dc23fa9 Version 3.8.1 2017-11-27 16:28:46 +00:00
Kim Gardner e882b51ed0 Merge pull request #1172 from coralproject/update-bug-report-link
Update Report Bug link in Admin Settings
2017-11-27 16:27:40 +00:00
Kim Gardner e52a6fd9c1 Merge branch 'master' into update-bug-report-link 2017-11-27 16:15:04 +00:00
Belen Curcio 95d356de10 Adding UserDetailCommentList 2017-11-27 12:20:22 -03:00
Kim Gardner 24af7b2812 Merge pull request #1180 from coralproject/autclose-firefox-bug
auto close input: fix styling bug in firefox
2017-11-27 13:17:30 +00:00
Kim Gardner 24b5a58584 Merge branch 'master' into autclose-firefox-bug 2017-11-27 13:07:10 +00:00
Kim Gardner a868cc9c67 Merge pull request #1178 from coralproject/more-padding-qbox
More padding for question box
2017-11-27 13:05:52 +00:00
Kim Gardner af83d32de1 Merge branch 'master' into more-padding-qbox 2017-11-27 12:51:50 +00:00
Kim Gardner fa82824d03 Merge pull request #1181 from coralproject/firefox-stories
Fix radiogroup not working properly in ff
2017-11-27 12:51:39 +00:00
Kim Gardner f0033a7912 Merge branch 'master' into firefox-stories 2017-11-27 12:41:14 +00:00
Kim Gardner 0924531677 Merge pull request #1182 from coralproject/sort-stories
Sort by publication_date
2017-11-27 12:40:50 +00:00
Kim Gardner 44b77597ed Merge branch 'master' into update-bug-report-link 2017-11-27 12:36:59 +00:00
Belen Curcio f2dccf7845 Adding addUserRole and removeUserRole 2017-11-24 18:36:59 -03:00
Belen Curcio e442961dbb UserDetail Refactor 2017-11-24 18:09:22 -03:00
Belen Curcio d58e54befa UserDetail Refactor 2017-11-24 17:35:53 -03:00
Belen Curcio 0fe9577abf Build User History 2017-11-24 16:24:11 -03:00
Belen Curcio 9703b86784 Working account history 2017-11-24 13:42:45 -03:00
Belen Curcio e2af3470dc Adding diff and fix to utils 2017-11-24 13:09:30 -03:00
Belen Curcio 741419f47b missing fragment 2017-11-24 12:58:05 -03:00
Chi Vinh Le ec2bf3cb7f Sort by publication_date 2017-11-24 12:41:18 +01:00
Chi Vinh Le 37f3c11104 Fix radiogroup not working properly in ff 2017-11-24 12:29:29 +01:00
Wyatt Johnson 4fc305ebff fixes based on feedback 2017-11-23 13:49:36 -07:00
Wyatt Johnson db8dd5a487 removed double await 2017-11-23 12:27:35 -07:00
Chi Vinh Le 9359bfc322 Fix styling bug in firefox 2017-11-23 20:09:16 +01:00
Wyatt Johnson d8d15f27ab added role management to graph 2017-11-23 11:20:55 -07:00
Wyatt Johnson 6119503057 added support for user assigned_by fields to get resolved 2017-11-23 10:53:20 -07:00
Kiwi 111ebd67ba Merge branch 'master' into extendable-embed-stream-tab 2017-11-23 14:49:54 +01:00
Chi Vinh Le 31f87dc192 Pass hover props to extendable tabs in order to support firefox 2017-11-23 14:46:23 +01:00
Chi Vinh Le c9dad30c71 Merge branch 'extendable-embed-stream-tab' into firefox-hover-tab 2017-11-23 14:46:18 +01:00
Chi Vinh Le 7f53375c67 More padding for question box 2017-11-23 13:40:19 +01:00
Chi Vinh Le df0c016e37 Merge branch 'refactor-configure' of ssh://github.com/coralproject/talk into refactor-configure 2017-11-23 13:33:21 +01:00
Chi Vinh Le bf3fe2ab8e Merge branch 'master' into refactor-configure 2017-11-23 13:27:18 +01:00
Belen Curcio 6e6ddbf378 Suspension response 2017-11-23 08:42:08 -03:00
Belen Curcio 5793ba45c3 Restoring message for suspend 2017-11-23 08:17:40 -03:00
Belen Curcio 903973ce4b Adding message 2017-11-23 08:13:03 -03:00
Belen Curcio c6c7961dc9 Merge branch 'mod-improv' of github.com:coralproject/talk into user-status-refactor 2017-11-23 08:08:44 -03:00
Belen Curcio fd0b475b84 banUser and unBanUser - removing setUserStatus 2017-11-23 08:07:13 -03:00
Belen Curcio b69195253b banUser and unBanUser wip - removing setStatus 2017-11-22 21:45:55 -03:00
Wyatt Johnson 9629ba2f83 added connector support 2017-11-22 14:48:34 -07:00
Wyatt Johnson a537d9ac17 added message to ban/suspend workflows 2017-11-22 14:08:18 -07:00
Kim Gardner ac7b4d56a1 Merge pull request #1174 from coralproject/show-all-comments-button
new design for show all comments button
2017-11-22 16:35:40 +00:00
Belen Curcio c742101250 Updating tests: dont wait for message 2017-11-22 12:46:43 -03:00
Belen Curcio dcbae2a117 Updating componets 2017-11-22 12:45:16 -03:00
Belen Curcio e530003c72 Hiding messages for suspension and bann 2017-11-22 12:38:07 -03:00
Belen Curcio 48e87c4659 Updating contations and deleting legacy code 2017-11-22 12:22:27 -03:00
Kim Gardner c29bb02b1c Merge branch 'master' into show-all-comments-button 2017-11-22 12:09:23 +00:00
Kim Gardner 8c72e32a25 Merge branch 'master' into update-bug-report-link 2017-11-22 12:09:17 +00:00
Kim Gardner 4a31bb7ba2 Merge branch 'master' into extendable-embed-stream-tab 2017-11-22 12:09:02 +00:00
Kim Gardner 3be1c94f8f Merge branch 'master' into refactor-configure 2017-11-22 12:07:26 +00:00
Belen Curcio 9ae32b9366 removing message 2017-11-22 02:51:16 -03:00
Belen Curcio e782776ab5 refactor changeUsername - Product question, 2017-11-22 02:37:58 -03:00
Belen Curcio 75d914b894 updated tests, changeUsername container, etc 2017-11-22 02:13:26 -03:00
Belen Curcio 69c93cd048 wip 2017-11-22 01:43:35 -03:00
Belen Curcio e3a8656e32 wip ChangeUsername 2017-11-22 01:32:32 -03:00
Belen Curcio 137c5889eb user should not be suspended if username is rejecte 2017-11-22 01:07:10 -03:00
Belen Curcio 3b768c144b Bug suspending/banning/rejecting users, 2017-11-22 00:58:59 -03:00
Belen Curcio f19d6b6800 changes ChangeUsername 2017-11-22 00:53:01 -03:00
Wyatt Johnson 90daab8219 added USERS_NEW event to SSE bus 2017-11-21 14:36:34 -07:00
Kim Gardner 7fba0428bb Merge pull request #1175 from coralproject/cli-users-fix
User CLI Fix
2017-11-21 19:24:35 +00:00
Wyatt Johnson c7d433deb5 added better logic around removing users for clearning out their stale data. 2017-11-21 12:13:28 -07:00
Chi Vinh Le a665dbd88e Reproduce modqueue freeze issue 2017-11-21 18:07:00 +01:00
Chi Vinh Le c990a812a4 new design for show all comments button 2017-11-21 15:59:20 +01:00
Kim Gardner 3f26a8d7ad Merge branch 'master' into extendable-embed-stream-tab 2017-11-21 12:31:45 +00:00
Kiwi 4a3e5942b0 Merge branch 'master' into refactor-configure 2017-11-21 12:58:16 +01:00
Chi Vinh Le 5360bdc8ec Add some docs 2017-11-21 12:42:17 +01:00
Chi Vinh Le 548a204665 Expose StreamConfiguration 2017-11-21 11:13:41 +01:00
Chi Vinh Le a8e8b78441 Pass updatePending and errors through slots 2017-11-21 11:08:18 +01:00
Chi Vinh Le 258788fd88 Add updatePending and errors to admin settings slots 2017-11-21 11:06:11 +01:00
Chi Vinh Le 8cc3d19819 Fix query 2017-11-21 10:35:38 +01:00
Wyatt Johnson 45e27fd6ad fixes to cli 2017-11-20 13:29:50 -07:00
Chi Vinh Le 0de5a328ab Slot streamSettings 2017-11-20 21:11:59 +01:00
Kim Gardner 4830aec138 Update Report Bug link in Admin Settings 2017-11-20 19:59:57 +00:00
Kim Gardner 4269fd1eb9 Merge pull request #1171 from coralproject/copy-updates
Update suspend copy
2017-11-20 19:53:00 +00:00
Kim Gardner 12e61bbc66 Update suspend copy 2017-11-20 19:41:14 +00:00
Chi Vinh Le 511e71f63a Change default settings=null to settings={} 2017-11-20 20:19:58 +01:00
Chi Vinh Le e675d716cb Remove deprecated code 2017-11-20 20:18:14 +01:00
Belen Curcio 2706e47f85 Now using changeUsername 2017-11-20 16:07:50 -03:00
Chi Vinh Le 2d6143af62 Fully implement mutation 2017-11-20 19:48:12 +01:00
Belen Curcio 43e9da9cbc setUsername refactor 2017-11-20 15:37:06 -03:00
Belen Curcio dbe88e58f4 setUsername refactor 2017-11-20 15:35:16 -03:00
Chi Vinh Le 873d1915c3 Implement apply 2017-11-20 19:20:09 +01:00
Wyatt Johnson debda80db1 fixed query issues 2017-11-20 10:32:46 -07:00
Chi Vinh Le 67f3f8cdef SimpleMDE don't trap keyboard 2017-11-20 18:14:17 +01:00
Chi Vinh Le 8f0eef7cb5 Reimplement QuestionBoxBuilder 2017-11-20 17:53:26 +01:00
Chi Vinh Le 4c7c3c9e42 Add premodLinks configuration 2017-11-20 12:36:49 +01:00
Chi Vinh Le 8f9724ed90 Implement configure and redux state 2017-11-20 12:29:23 +01:00
Belen Curcio 1756c7c050 Reject username 2017-11-20 02:04:19 -03:00
Belen Curcio 01b8c7aee6 Safe access to objects 2017-11-20 01:22:44 -03:00
Belen Curcio 7b9d7163e1 updated e2e 2017-11-20 01:08:42 -03:00
Belen Curcio e6e5e5506e removing pending 2017-11-20 00:57:01 -03:00
Belen Curcio 8f12fcd09b Safe access 2017-11-20 00:38:52 -03:00
Belen Curcio 7ec239fd68 Banned User Stream 2017-11-19 22:52:26 -03:00
Belen Curcio 5299ea674e bug error view options 2017-11-19 22:50:14 -03:00
Belen Curcio 6a3c80468b Missing proptype 2017-11-19 22:48:45 -03:00
Belen Curcio aa4b386ee6 Banned User Stream 2017-11-19 22:45:05 -03:00
Belen Curcio 0459eceee7 refetch status 2017-11-19 22:40:06 -03:00
Belen Curcio 367ec00a02 Refactor for setUserStatus to setUserBanStatus 2017-11-19 21:18:50 -03:00
Belen Curcio e34ba248b9 Refactor for setUserStatus to setUserBanStatus 2017-11-19 21:04:04 -03:00
Belen Curcio 6d6030d60a Removing setCommenterStatus 2017-11-19 20:38:13 -03:00
Belen Curcio e941f00384 Suspend User Mutation 2017-11-19 20:30:11 -03:00
Chi Vinh Le 9fb7aa2657 Start implementing settings ui 2017-11-18 11:40:44 +01:00
Wyatt Johnson e17b15ec2c Merge branch 'next' into user-status-refactor 2017-11-17 09:43:59 -07:00
Wyatt Johnson c98c9b3369 Merge branch 'master' into next 2017-11-17 09:29:31 -07:00
Wyatt Johnson 73cec00fb9 fixed migration issues 2017-11-17 09:17:43 -07:00
Kiwi d714586279 Merge branch 'master' into extendable-embed-stream-tab 2017-11-17 17:12:42 +01:00
Chi Vinh Le 3e4c9dcb48 Refactor query tree 2017-11-17 17:02:32 +01:00
Chi Vinh Le 285ec98d54 Refactor styles 2017-11-17 16:53:56 +01:00
Chi Vinh Le 853b0ca6dd Automatic asset closure 2017-11-17 15:42:06 +01:00
Chi Vinh Le c58d9068e3 Implement open and close asset 2017-11-17 12:39:34 +01:00
Chi Vinh Le 78e0fb6cc6 Add UpdateAssetStatus mutation 2017-11-16 20:31:08 +01:00
Chi Vinh Le dfbb9ad96f Implement AssetStatusInfo UI 2017-11-16 20:31:00 +01:00
Chi Vinh Le f7c46b1a42 Move asset fragment up one level 2017-11-16 16:19:36 +01:00
Chi Vinh Le b5b6b113d4 Restructure coral-embed-stream 2017-11-16 10:57:16 +01:00
Chi Vinh Le a13d384cee Merge branch 'extendable-embed-stream-tab' into refactor-configure 2017-11-16 10:42:15 +01:00
Kiwi 3a0a854317 Merge branch 'master' into extendable-embed-stream-tab 2017-11-16 10:39:52 +01:00
Chi Vinh Le 73c3d31fbc Merge branch 'master' into refactor-configure 2017-11-16 10:30:16 +01:00
Chi Vinh Le d3d1148ee6 Merge branch 'master' into refactor-configure 2017-11-15 19:49:09 +01:00
Kiwi ecd9219e17 Merge branch 'master' into extendable-embed-stream-tab 2017-11-15 19:46:04 +01:00
Chi Vinh Le ce4ad426a7 Extendable embed stream tabs 2017-11-15 19:38:25 +01:00
Belen Curcio 90a9ff251d wip 2017-11-15 14:39:59 -03:00
Belen Curcio a626dfafc8 Refactor Admin Side queries 2017-11-15 13:53:32 -03:00
Chi Vinh Le e02537df13 Fix mutation syntax 2017-11-15 17:53:24 +01:00
Belen Curcio b4aeee9433 Client Refactor 2017-11-15 13:21:20 -03:00
Chi Vinh Le e4a359e0b4 Add mutation to framework 2017-11-15 13:38:02 +01:00
Belen Curcio e8e103020c Merge branch 'user-status-refactor' of github.com:coralproject/talk into mod-improv 2017-11-14 13:54:21 -03:00
Belen Curcio ff6e41f2e1 styling 2017-11-14 13:13:02 -03:00
Belen Curcio aabd152314 Account History 2017-11-14 13:01:13 -03:00
Belen Curcio 6a17d04570 Merge branch 'master' of github.com:coralproject/talk into mod-improv 2017-11-14 13:00:20 -03:00
Belen Curcio 32547a214b Missing proptypes 2017-11-14 11:56:23 -03:00
Wyatt Johnson ef40e9dce5 spelling 2017-11-13 16:08:20 -07:00
Wyatt Johnson e090ba9123 Merge branch 'next' into user-status-refactor 2017-11-13 16:06:16 -07:00
Wyatt Johnson 81f825d4b3 Merge branch 'master' into next 2017-11-13 16:01:11 -07:00
Wyatt Johnson ecf02f454c staff members cannot be flagged 2017-11-13 15:57:54 -07:00
Wyatt Johnson 9f823c0b11 added action counts for users 2017-11-13 15:46:49 -07:00
Wyatt Johnson 401d8a423d added emails for status changes 2017-11-09 16:07:46 -07:00
Wyatt Johnson d80401a49b Merge branch 'next' into user-status-refactor 2017-11-09 14:59:47 -07:00
Wyatt Johnson cb768d3190 Merge branch 'master' into next 2017-11-09 14:59:32 -07:00
Kim Gardner 7be888bfa8 Bump release number for 4.0.0 2017-11-09 11:44:42 +00:00
Wyatt Johnson 77c927ac59 added migration, migration tests 🎉 2017-11-08 16:35:38 -07:00
Wyatt Johnson 0257e09a40 Merge branch 'next' into user-status-refactor 2017-11-08 13:52:48 -07:00
Wyatt Johnson 61df0e1eeb Merge branch 'master' into next 2017-11-08 13:49:21 -07:00
Wyatt Johnson 1b7f78b562 user queries 2017-11-08 13:40:46 -07:00
Wyatt Johnson 9e84c72148 added more mutations 2017-11-08 09:41:38 -07:00
Wyatt Johnson 7034003fd1 fixed changed package script 2017-11-02 17:55:31 -06:00
Wyatt Johnson b5bcbc4fc8 added username edit, tests 2017-11-02 17:54:56 -06:00
Wyatt Johnson 04e6f5b52d added prebuild 2017-11-02 17:23:09 -06:00
Wyatt Johnson 76a255fb7b initial pass at status support 2017-11-02 17:16:57 -06:00
Wyatt Johnson 1f3722edc1 Merge branch 'master' into user-status-refactor 2017-11-02 16:06:34 -06:00
Wyatt Johnson 3f7ddddba3 perms cleanup 2017-10-26 14:27:46 -06:00
Wyatt Johnson 91931b2e6f Merge branch 'master' into flags-locales 2017-10-10 11:22:59 -06:00
Wyatt Johnson cab94842aa small naming change 2017-10-10 11:22:19 -06:00
Wyatt Johnson 86c3c286ee Graph fixes and migrations
- Added migrations for mapping old keys
- Added migration for fixing old dont agree flags
- Removed enum requirement for output
2017-10-10 11:22:19 -06:00
Kim Gardner d6e7bb0a0a Merge branch 'master' into flags-locales 2017-10-10 13:08:19 +01:00
Wyatt Johnson 970a93360a Merge branch 'master' into flags-locales 2017-10-06 11:04:27 -06:00
Kim Gardner 7d9b588bd2 Merge branch 'master' into flags-locales 2017-10-05 11:34:06 +01:00
Wyatt Johnson 13c38c2215 added toxic comment translations 2017-10-04 13:48:10 -06:00
Wyatt Johnson 2b08ef5c42 updated translations 2017-10-04 11:08:34 -06:00
Wyatt Johnson b7ea9e7d97 Merge branch 'master' into flags-locales 2017-10-04 10:36:42 -06:00
Wyatt Johnson d85db01752 changed custom error to standardized error 2017-10-04 10:35:55 -06:00
Wyatt Johnson 69c03c9190 cleaned up graph mutators 2017-10-04 10:29:09 -06:00
Wyatt Johnson 00de72a6eb Initial work for locking flag type and providing translations 2017-10-03 17:20:40 -06:00
819 changed files with 26807 additions and 18038 deletions
+1 -1
View File
@@ -28,5 +28,5 @@ plugins/*
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
public
node_modules
+2
View File
@@ -13,6 +13,7 @@ client/coral-framework/graphql/introspection.json
.idea/
*.swp
*.DS_STORE
.prettierrc.json
coverage/
test/e2e/reports/
@@ -52,3 +53,4 @@ plugins/*
!plugins/talk-plugin-slack-notifications
**/node_modules/*
yarn-error.log
+7 -1
View File
@@ -1,5 +1,11 @@
{
"exec": "npm-run-all --parallel generate-introspection start:development",
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql"
"ext": "js,json,graphql,yml",
"watch": [
".",
"bin/cli",
"bin/cli-serve"
]
}
+3 -2
View File
@@ -1,5 +1,6 @@
{
"exceptions": [
"https://nodesecurity.io/advisories/531"
"https://nodesecurity.io/advisories/531",
"https://nodesecurity.io/advisories/532"
]
}
}
+1
View File
@@ -5,6 +5,7 @@ ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DEFAULT_LANG=en
ONBUILD ARG TALK_PLUGINS_JSON
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
# Bundle app source
ONBUILD COPY . /usr/src/app
+47 -17
View File
@@ -1,19 +1,38 @@
const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const uuid = require('uuid');
const merge = require('lodash/merge');
const helmet = require('helmet');
const plugins = require('./services/plugins');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const {HELMET_CONFIGURATION} = require('./config');
const {MOUNT_PATH} = require('./url');
const {applyLocals} = require('./services/locals');
const { HELMET_CONFIGURATION } = require('./config');
const { MOUNT_PATH } = require('./url');
const routes = require('./routes');
const debug = require('debug')('talk:app');
const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
const app = express();
// Request Identity Middleware
app.use((req, res, next) => {
req.id = uuid.v4();
next();
});
//==============================================================================
// PLUGIN PRE APPLICATION MIDDLEWARE
//==============================================================================
// Inject server route plugins.
plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
debug(`added plugin '${plugin.name}'`);
// Pass the app to the plugin to mount it's routes.
callback(app);
});
//==============================================================================
// APPLICATION WIDE MIDDLEWARE
//==============================================================================
@@ -23,25 +42,39 @@ if (process.env.NODE_ENV !== 'test') {
app.use(morgan('dev'));
}
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
const { Engine } = require('apollo-engine');
const engine = new Engine({
engineConfig: {
apiKey: APOLLO_ENGINE_KEY,
},
graphqlPort: PORT,
endpoint: `${MOUNT_PATH}api/v1/graph/ql`,
});
engine.start();
app.use(engine.expressMiddleware());
}
// Trust the first proxy in front of us, this will enable us to trust the fact
// that SSL was terminated correctly.
app.set('trust proxy', 1);
// Enable a suite of security good practices through helmet. We disable
// frameguard to allow crossdomain injection of the embed.
app.use(helmet(merge(HELMET_CONFIGURATION, {
frameguard: false,
})));
app.use(
helmet(
merge(HELMET_CONFIGURATION, {
frameguard: false,
})
)
);
// Compress the responses if appropriate.
app.use(compression());
// Parse the cookies on the request.
app.use(cookieParser());
// Parse the body json if it's there.
app.use(bodyParser.json());
//==============================================================================
// VIEW CONFIGURATION
//==============================================================================
@@ -53,9 +86,6 @@ app.set('view engine', 'ejs');
// ROUTES
//==============================================================================
// Add the locals to the app renderer.
applyLocals(app.locals);
debug(`mounting routes on the ${MOUNT_PATH} path`);
// Actually apply the routes.
+28 -25
View File
@@ -4,8 +4,9 @@
* Module dependencies.
*/
const program = require('./commander');
const {head, map} = require('lodash');
require('./util');
const program = require('commander');
const { head, map } = require('lodash');
const Matcher = require('did-you-mean');
program
@@ -25,33 +26,35 @@ program
.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);
const commands = map(program.commands, '_name');
const command = head(program.args);
if (!commands.includes(command)) {
const m = new Matcher(commands);
const similarCommands = m.list(command);
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(
`cli '${command}' is not a talk cli command. See 'cli --help'.`
);
if (similarCommands.length > 0) {
const sc = similarCommands.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
* signal. This is for the use case where we want to kill the process that is
* labled with the PID written out by the parent process.
*/
process.once('exit', () => {
if (
// /**
// * When this process 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
// * signal. This is for the use case where we want to kill the process that is
// * labeled with the PID written out by the parent process.
// */
// process.once('exit', () => {
// if (
// program.runningCommand &&
program.runningCommand.killed === false &&
program.runningCommand.exitCode === null
) {
program.runningCommand.kill('SIGINT');
}
});
// // program.runningCommand &&
// program.runningCommand.killed === false &&
// program.runningCommand.exitCode === null
// ) {
// program.runningCommand.kill('SIGINT');
// }
// });
+22 -26
View File
@@ -4,7 +4,8 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const parseDuration = require('ms');
const Table = require('cli-table');
const AssetModel = require('../models/asset');
@@ -12,34 +13,27 @@ const CommentModel = require('../models/comment');
const AssetsService = require('../services/assets');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('./util');
const inquirer = require('inquirer');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
/**
* Lists all the assets registered in the database.
*/
async function listAssets() {
try {
let assets = await AssetModel.find({}).sort({'created_at': 1});
let assets = await AssetModel.find({}).sort({ created_at: 1 });
let table = new Table({
head: [
'ID',
'Title',
'URL'
]
head: ['ID', 'Title', 'URL'],
});
assets.forEach((asset) => {
assets.forEach(asset => {
table.push([
asset.id,
asset.title ? asset.title : '',
asset.url ? asset.url : ''
asset.url ? asset.url : '',
]);
});
@@ -61,13 +55,13 @@ async function refreshAssets(ageString) {
$or: [
{
scraped: {
$lte: age
}
$lte: age,
},
},
{
scraped: null
}
]
scraped: null,
},
],
});
// Queue all the assets for scraping.
@@ -95,7 +89,6 @@ async function updateURL(assetID, assetURL) {
async function merge(srcID, dstID) {
try {
// Grab the assets...
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]);
if (!srcAsset || !dstAsset) {
@@ -103,21 +96,22 @@ async function merge(srcID, dstID) {
}
// Count the affected resources...
let srcCommentCount = await CommentModel.find({asset_id: srcID}).count();
let srcCommentCount = await CommentModel.find({ asset_id: srcID }).count();
console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`);
console.log(
`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`
);
let {confirm} = await inquirer.prompt([
let { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with merge',
default: false
}
default: false,
},
]);
if (confirm) {
// Perform the merge!
await AssetsService.merge(srcID, dstID);
} else {
@@ -152,7 +146,9 @@ program
program
.command('merge <srcID> <dstID>')
.description('merges two assets together by moving comments from src to dst and deleting the src asset')
.description(
'merges two assets together by moving comments from src to dst and deleting the src asset'
)
.action(merge);
program.parse(process.argv);
+46 -33
View File
@@ -4,33 +4,28 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const util = require('./util');
const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
/**
* Starts the job processor.
*/
function processJobs() {
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([() => kue.Task.shutdown()]);
// Start the scraper processor.
scraper.process();
// Start the mail processor.
mailer.process();
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => kue.Task.shutdown()
]);
}
/**
@@ -39,22 +34,15 @@ function processJobs() {
* @return {Promise}
*/
function removeJob(job) {
return new Promise((resolve, reject) => job.remove((err) => {
if (err) {
return reject(err);
}
return new Promise((resolve, reject) =>
job.remove(err => {
if (err) {
return reject(err);
}
return resolve(job);
}));
}
/**
* Removes the jobs passed in and returns a promise.
* @param {Array} jobs array of jobs
* @return {Promise}
*/
function removeJobs(jobs) {
return Promise.all(jobs.map(removeJob));
return resolve(job);
})
);
}
/**
@@ -63,7 +51,7 @@ function removeJobs(jobs) {
* @param {Number} limit limit of jobs to load
* @return {Promise}
*/
function rangeJobsByState(state = 'complete', limit) {
function rangeJobsByState(state, limit) {
return new Promise((resolve, reject) => {
kue.Job.rangeByState(state, 0, limit, 'asc', (err, jobs) => {
if (err) {
@@ -75,22 +63,47 @@ function rangeJobsByState(state = 'complete', limit) {
});
}
async function getJobBatch(n, includeStuck) {
let jobs = [];
jobs = await rangeJobsByState('complete', n);
if (includeStuck) {
jobs = jobs.concat(await rangeJobsByState('failed', n));
}
return jobs;
}
/**
* Cleans up the jobs that are in the queue.
*/
async function cleanupJobs(options) {
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([() => kue.Task.shutdown()]);
const n = 100;
try {
const joblists = await Promise.all([
rangeJobsByState('complete', n),
options.stuck ? rangeJobsByState('failed', n) : false
]);
// Connect to redis by establishing a queue.
kue.Task.connect();
await joblists.filter((jobs) => jobs).map(removeJobs);
let jobCount = 0;
let jobs = await getJobBatch(n, options.stuck);
while (jobs.length > 0) {
// Remove all the jobs.
await Promise.all(jobs.map(job => removeJob(job)));
jobCount += jobs.length;
// Get the next batch of jobs.
jobs = await getJobBatch(n, options.stuck);
}
util.shutdown();
console.log('Removed old jobs');
console.log(`Removed ${jobCount} jobs`);
} catch (err) {
console.error(err);
util.shutdown(1);
+12 -16
View File
@@ -4,20 +4,17 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const MigrationService = require('../services/migration');
// Register shutdown hooks.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function createMigration(name) {
try {
// Create the migration.
await MigrationService.create(name);
@@ -29,20 +26,20 @@ async function createMigration(name) {
}
async function runMigrations() {
try {
let {backedUp} = await inquirer.prompt([
let { backedUp } = await inquirer.prompt([
{
type: 'confirm',
name: 'backedUp',
message: 'Did you perform a database backup',
default: false
}
default: false,
},
]);
if (!backedUp) {
throw new Error('Please backup your databases prior to migrations occuring');
throw new Error(
'Please backup your databases prior to migrations occuring'
);
}
// Get the migrations to run.
@@ -50,21 +47,20 @@ async function runMigrations() {
console.log('Now going to run the following migrations:\n');
for (let {filename} of migrations) {
for (let { filename } of migrations) {
console.log(`\tmigrations/${filename}`);
}
let {confirm} = await inquirer.prompt([
let { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with migrations',
default: false
}
default: false,
},
]);
if (confirm) {
// Run the migrations.
await MigrationService.run(migrations);
} else {
+120 -79
View File
@@ -7,7 +7,8 @@
// Interface heavily inspired by the yarn package manager:
// https://yarnpkg.com/
const program = require('./commander');
require('./util');
const program = require('commander');
const inquirer = require('inquirer');
// Make things colorful!
@@ -20,11 +21,11 @@ const path = require('path');
const spawn = require('cross-spawn');
const semver = require('semver');
const resolve = require('resolve');
const {plugins, itteratePlugins, isInternal} = require('../plugins');
const { plugins, iteratePlugins, isInternal } = require('../plugins');
function existsInNodeModules(name) {
try {
resolve.sync(name, {basedir: dir});
resolve.sync(name, { basedir: dir });
return true;
} catch (e) {
@@ -38,13 +39,13 @@ function versionMatch(name, version) {
resolve.sync(name, {
basedir: dir,
packageFilter: (pkg) => {
packageFilter: pkg => {
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
matched = true;
}
return pkg;
}
},
});
return matched;
@@ -55,7 +56,7 @@ function versionMatch(name, version) {
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
function reconcilePackages({quiet = false, upgradeRemote = false}) {
function reconcilePackages({ quiet = false, upgradeRemote = false }) {
const fetchable = [];
const local = [];
const upgradable = [];
@@ -70,13 +71,14 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
}
for (let i in plugins) {
let section = itteratePlugins(plugins[i]);
let section = iteratePlugins(plugins[i]);
for (let j in section) {
let {name, version} = section[j];
let { name, version } = section[j];
let namespaced = name.charAt(0) === '@';
let dep = name.split('/')
let dep = name
.split('/')
.slice(0, namespaced ? 2 : 1)
.join('/');
@@ -90,7 +92,7 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log(` l ${name}`);
}
local.push({name, version});
local.push({ name, version });
continue;
}
@@ -98,9 +100,8 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
if (!quiet) {
console.log(` m ${name}`);
}
fetchable.push({name, version});
fetchable.push({ name, version });
} else if (!versionMatch(dep, version)) {
// A plugin was found, yet the current version does not match the
// current version installed. We should warn if upgradeRemote is
// not enabled that it is currently not supported.
@@ -114,14 +115,14 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log(` oe ${name} (package upgrade may be required)`);
upgradable.push({name, version});
upgradable.push({ name, version });
} else {
if (!quiet) {
console.log(` e ${name}`);
}
if (upgradeRemote) {
upgradable.push({name, version});
upgradable.push({ name, version });
}
}
}
@@ -131,33 +132,44 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log();
}
return {local, fetchable, upgradable};
return { local, fetchable, upgradable };
}
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow);
const {fetchable, upgradable} = reconcilePackages({upgradeRemote});
async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
console.log(
`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get(
'mag'
)} Reconciling plugins...`.yellow
);
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
console.log(
`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`
.yellow
);
if (fetchable.length > 0) {
console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
console.log(
`$ yarn add --ignore-scripts ${fetchable.map(
({ name, version }) => `${name}@${version}`.cyan
)}`
);
if (!dryRun) {
let args = [
'add',
'--ignore-scripts',
...fetchable.map(({name, version}) => `${name}@${version}`)
...fetchable.map(({ name, version }) => `${name}@${version}`),
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
stdio: ['ignore', 'pipe', 'inherit'],
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
throw new Error(
'Could not install external plugins, errors occured during install'
);
}
console.log(output.stdout.toString());
@@ -165,36 +177,45 @@ async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
}
if (upgradable.length > 0) {
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
console.log(
`$ yarn upgrade ${upgradable.map(
({ name, version }) => `${name}@${version}`.cyan
)}`
);
if (!dryRun) {
let args = [
'upgrade',
...upgradable.map(({name, version}) => `${name}@${version}`)
...upgradable.map(({ name, version }) => `${name}@${version}`),
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
stdio: ['ignore', 'pipe', 'inherit'],
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
throw new Error(
'Could not install external plugins, errors occured during install'
);
}
console.log(output.stdout.toString());
}
}
return {upgradable, fetchable};
return { upgradable, fetchable };
}
async function reconcileLocalPlugins({skipRemote, dryRun}) {
console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow);
const {local} = reconcilePackages({quiet: true});
async function reconcileLocalPlugins({ skipRemote, dryRun }) {
console.log(
`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get(
'pick'
)} Installing local plugin dependencies...\n`.yellow
);
const { local } = reconcilePackages({ quiet: true });
for (let i in local) {
let {name} = local[i];
let { name } = local[i];
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
continue;
@@ -209,11 +230,13 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit'],
cwd: wd
cwd: wd,
});
if (output.status) {
throw new Error('Could not install local plugin dependencies, errors occured during install');
throw new Error(
'Could not install local plugin dependencies, errors occured during install'
);
}
console.log(output.stdout.toString());
@@ -224,7 +247,12 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
// This traverses the local plugins and installs any dependencies listed there,
// this only is really needed for plugins that are installed via docker because
// core plugins will have their dependencies already included in core.
async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) {
async function reconcilePluginDeps({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
}) {
let startTime = new Date();
// We don't need to do anything if we skip everything....
@@ -234,14 +262,19 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({skipRemote, dryRun});
await reconcileLocalPlugins({ skipRemote, dryRun });
}
// Locate any external plugins and install them.
if (!skipRemote) {
let results = [];
try {
results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote});
results = await reconcileRemotePlugins({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
});
} catch (e) {
throw e;
}
@@ -261,7 +294,9 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote
} else if (results.fetchable.length === 0) {
message = `Upgraded ${results.upgradable.length} new plugins.`;
} else {
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`;
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
results.upgradable.length
} plugins.`;
}
console.log(`\n${status} ${message}`);
@@ -278,8 +313,7 @@ async function createSeedPlugin() {
function pluginNameExists(pluginName) {
const pluginNames = fs.readdirSync(pluginsDir);
return !!pluginNames
.filter((pn) => pn === pluginName).length;
return !!pluginNames.filter(pn => pn === pluginName).length;
}
let answers = await inquirer.prompt([
@@ -287,8 +321,7 @@ async function createSeedPlugin() {
type: 'input',
name: 'pluginName',
message: 'Plugin Name:',
validate: (input) => {
validate: input => {
if (pluginNameExists(input)) {
return 'Please, choose another name. That name already exists';
}
@@ -298,23 +331,23 @@ async function createSeedPlugin() {
}
return 'Plugin Name is required.';
}
},
},
{
type: 'confirm',
name: 'server',
message: 'Is this plugin extending the server capabilities?'
message: 'Is this plugin extending the server capabilities?',
},
{
type: 'confirm',
name: 'client',
message: 'Is this plugin extending the client capabilities?'
message: 'Is this plugin extending the client capabilities?',
},
{
type: 'confirm',
name: 'addPluginsJson',
message: 'Should we add it to the plugins.json?'
}
message: 'Should we add it to the plugins.json?',
},
]);
//==============================================================================
@@ -325,41 +358,41 @@ async function createSeedPlugin() {
const newPluginPath = path.join(pluginsDir, answers.pluginName);
if (fs.existsSync(seedPlugin)) {
if (answers.server && answers.client) {
// This is a server-side and client-side plugin!, let's copy the template
fs.copySync(seedPlugin, newPluginPath);
} else {
} else {
fs.copySync(seedPlugin, newPluginPath, {
filter: p => {
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/gim;
if (
rootRx.test(p) &&
(fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())
) {
return true;
}
fs.copySync(seedPlugin, newPluginPath, {filter: (p) => {
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/igm;
if (rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())) {
return true;
}
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
}});
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
},
});
}
// Let's add this to the plugins.json
if (answers.addPluginsJson) {
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
fs.readJson(pluginsJson)
.then((j) => {
fs
.readJson(pluginsJson)
.then(j => {
// This is a client-side plugin, let's push this.
if (answers.client) {
j.client.push(answers.pluginName);
@@ -376,14 +409,17 @@ async function createSeedPlugin() {
fs.writeFileSync(pluginsJson, output);
}
})
.catch((err) => {
.catch(err => {
console.error(err);
});
}
console.log(`✨ Yay! Plugin created! Find your plugin: ${answers.pluginName} in the ./plugins folder`);
console.log(
`✨ Yay! Plugin created! Find your plugin: ${
answers.pluginName
} in the ./plugins folder`
);
}
}
//==============================================================================
@@ -402,9 +438,14 @@ program
program
.command('reconcile')
.description('reconciles local plugin dependencies and downloads external plugins')
.description(
'reconciles local plugin dependencies and downloads external plugins'
)
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
.option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation')
.option(
'-d, --dry-run',
'does not actually change anything on the filesystem acts only as a simulation'
)
.option('--skip-local', 'skips the local dependancy reconciliation')
.option('--skip-remote', 'skips the remote plugin reconciliation')
.action(reconcilePluginDeps);
+10 -4
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const serve = require('../serve');
//==============================================================================
@@ -9,9 +10,14 @@ const serve = require('../serve');
program
.option('-j, --jobs', 'enable job processing on this thread')
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
.option(
'-w, --websockets',
'enable the websocket (subscriptions) handler on this thread'
)
.parse(process.argv);
// Start serving.
serve({jobs: program.jobs, websockets: program.websockets});
serve({ jobs: program.jobs, websockets: program.websockets }).catch(err => {
console.error(err);
util.shutdown(1);
});
+5 -5
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env node
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingsService = require('../services/settings');
const util = require('./util');
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
@@ -16,12 +16,12 @@ async function changeOrgName() {
try {
let settings = await SettingsService.retrieve();
let {organizationName} = await inquirer.prompt([
let { organizationName } = await inquirer.prompt([
{
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName
}
default: settings.organizationName,
},
]);
if (settings.organizationName !== organizationName) {
+41 -49
View File
@@ -4,7 +4,8 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingModel = require('../models/setting');
@@ -12,13 +13,10 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
const SettingsService = require('../services/settings');
const SetupService = require('../services/setup');
const UsersService = require('../services/users');
const util = require('./util');
const errors = require('../errors');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
//==============================================================================
// Setting up the program command line arguments.
@@ -34,19 +32,15 @@ program
//==============================================================================
const performSetup = async () => {
// Get the current settings, we are expecing an error here.
try {
// Try to get the settings.
await SettingsService.retrieve();
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
throw errors.ErrSettingsInit;
} catch (e) {
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (e !== errors.ErrSettingsNotInit) {
@@ -66,7 +60,9 @@ const performSetup = async () => {
// Create the base settings model.
let settings = new SettingModel();
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
console.log(
"\nWe'll ask you some questions in order to setup your installation of Talk.\n"
);
let answers = await inquirer.prompt([
{
@@ -74,31 +70,31 @@ const performSetup = async () => {
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
validate: input => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
},
{
type: 'list',
choices: MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
message: 'Select a moderation mode',
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
message: 'Should emails always be confirmed',
},
]);
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
Object.keys(answers).forEach(key => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
@@ -109,97 +105,93 @@ const performSetup = async () => {
type: 'confirm',
name: 'inputWhitelistedDomains',
default: true,
message: 'Would you like to specify a whitelisted domain'
message: 'Would you like to specify a whitelisted domain',
},
{
type: 'input',
name: 'whitelistedDomain',
message: 'Whitelisted Domain',
when: ({inputWhitelistedDomains}) => inputWhitelistedDomains,
validate: (input) => {
when: ({ inputWhitelistedDomains }) => inputWhitelistedDomains,
validate: input => {
if (input && input.length > 0) {
return true;
}
return 'Whitelisted Domain cannot be empty.';
}
}
},
},
]);
if (answers.inputWhitelistedDomains) {
settings.domains.whitelist = [answers.whitelistedDomain];
}
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
console.log("\nWe'll ask you some questions about your first admin user.\n");
let user = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: 'Username',
filter: (username) => {
return UsersService
.isValidUsername(username, false)
.catch((err) => {
throw err.message;
});
}
filter: username => {
return UsersService.isValidUsername(username, false).catch(err => {
throw err.message;
});
},
},
{
name: 'email',
message: 'Email',
format: 'email',
validate: (value) => {
validate: value => {
if (value && value.length >= 3) {
return true;
}
return 'Email is required';
}
},
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
filter: password => {
return UsersService.isValidPassword(password).catch(err => {
throw err.message;
});
},
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword, {password}) => {
filter: (confirmPassword, { password }) => {
if (password !== confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
return UsersService.isValidPassword(confirmPassword).catch(err => {
throw err.message;
});
},
},
]);
let {user: newUser} = await SetupService.setup({
let { user: newUser } = await SetupService.setup({
settings: settings.toObject(),
user: {
email: user.email,
username: user.username,
password: user.password
}
password: user.password,
},
});
console.log('Settings created.');
console.log(`User ${newUser.id} created.`);
console.log('\nTalk is now installed!');
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
console.log(
'\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'
);
};
// Start tthe setup process.
@@ -207,7 +199,7 @@ performSetup()
.then(() => {
util.shutdown();
})
.catch((e) => {
.catch(e => {
console.error(e);
util.shutdown(1);
});
+7 -19
View File
@@ -4,35 +4,25 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const mongoose = require('../services/mongoose');
const TokensService = require('../services/tokens');
const util = require('./util');
const Table = require('cli-table');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function listTokens(userID) {
try {
let tokens = await TokensService.list(userID);
let table = new Table({
head: [
'ID',
'Name',
'Status'
]
head: ['ID', 'Name', 'Status'],
});
tokens.forEach((token) => {
table.push([
token.id,
token.name,
token.active ? 'Active' : 'Revoked'
]);
tokens.forEach(token => {
table.push([token.id, token.name, token.active ? 'Active' : 'Revoked']);
});
console.log(table.toString());
@@ -46,7 +36,6 @@ async function listTokens(userID) {
async function revokeToken(tokenID) {
try {
await TokensService.revoke(null, tokenID);
console.log(`Revoked Token[${tokenID}]`);
@@ -60,8 +49,7 @@ async function revokeToken(tokenID) {
async function createToken(userID, tokenName) {
try {
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
+201 -413
View File
@@ -4,286 +4,183 @@
* Module dependencies.
*/
const program = require('./commander');
const inquirer = require('inquirer');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const USER_ROLES = require('../models/enum/user_roles');
const mongoose = require('../services/mongoose');
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const { graphql } = require('graphql');
const { stripIndent } = require('common-tags');
const Table = require('cli-table');
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
if (input && input.length >= len) {
return true;
}
// Make things colorful!
require('colors');
return msg;
};
// Register the autocomplete plugin.
inquirer.registerPrompt(
'autocomplete',
require('inquirer-autocomplete-prompt')
);
// Regeister the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
const schema = require('../graph/schema');
const Context = require('../graph/context');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const CommentModel = require('../models/comment');
const ActionModel = require('../models/action');
const USER_ROLES = require('../models/enum/user_roles');
const mongoose = require('../services/mongoose');
const databaseVerifications = require('./verifications/database');
function getUserCreateAnswers(options) {
if (options.flag_mode) {
let user = {
email: options.email,
password: options.password,
confirmPassword: options.password,
username: options.name,
roles: []
};
if (options.role && USER_ROLES.indexOf(options.role) > -1) {
user.roles = [options.role];
}
return Promise.resolve(user);
}
return inquirer.prompt([
{
name: 'email',
message: 'Email',
format: 'email',
validate: validateRequired('Email is required')
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'username',
message: 'Username',
filter: (username) => {
return UsersService
.isValidUsername(username)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'roles',
message: 'User Role',
type: 'checkbox',
choices: USER_ROLES
}
]);
}
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
/**
* Prompts for input and registers a user based on those.
* Deletes a user and cleans up their associated verifications.
*/
async function createUser(options) {
async function deleteUser(userID) {
try {
const answers = await getUserCreateAnswers(options);
if (answers.password !== answers.confirmPassword) {
throw new Error('Passwords do not match');
// Find the user we're removing.
const user = await UserModel.findOne({ id: userID });
if (!user) {
throw new Error(`user with id ${userID} not found`);
}
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
console.log(`Created user ${user.id}.`);
printUserAsTable(user);
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}.`);
});
}));
console.warn(stripIndent`
This will delete the above user.
This might take a long time if there is a lot of data, please confirm that
you want to continue.
`);
const { confirm } = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
message: 'Continue',
default: false,
});
if (!confirm) {
return util.shutdown();
}
console.warn("Removing user's actions");
// Remove all the user's actions.
await ActionModel.where({ user_id: user.id })
.setOptions({ multi: true })
.remove();
console.warn("Removing user's comments");
// Remove all the user's comments.
await CommentModel.where({ author_id: user.id })
.setOptions({ multi: true })
.remove();
console.warn('Updating the database indexes');
// Update the counts that might have changed.
for (const verification of databaseVerifications) {
await verification({ fix: true, limit: Infinity, batch: 1000 });
}
console.warn('Removing the user');
// Remove the user.
await user.remove();
util.shutdown();
} catch (err) {
console.error(err);
util.shutdown();
util.shutdown(1);
}
}
/**
* Deletes a user.
*/
function deleteUser(userID) {
UserModel
.findOneAndRemove({
id: userID
})
.then(() => {
console.log('Deleted user');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown();
});
}
function printUserAsTable(user) {
let table = new Table({});
/**
* Changes the password for a user.
*/
function passwd(userID) {
inquirer.prompt([
table.push(
{ ID: user.id.gray },
{ Username: user.username },
{ Emails: user.emails },
{ Tags: user.tags ? user.tags.map(({ tag: { name } }) => name) : '' },
{ Role: user.role },
{ Verified: user.hasVerifiedEmail },
{ Username: user.status.username.status },
{ Banned: user.banned },
{
name: 'password',
message: 'Password',
type: 'password',
validate: validateRequired('Password is required')
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
validate: validateRequired('Confirm Password is required')
Suspension: user.suspended
? `Until ${user.status.suspension.until}`
: false,
}
])
.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);
});
console.log(table.toString());
}
/**
* Updates the user from the options array.
* Searches for users based on their username and email address.
*/
function updateUser(userID, options) {
const updates = [];
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
let q = UserModel.update({
'id': userID,
'profiles.provider': 'local'
}, {
$set: {
'profiles.$.id': options.email
async function searchUsers() {
const ctx = Context.forSystem();
const searchQuery = `
query SearchUsers($value: String) {
users(query: {value: $value}) {
nodes {
id
username
role
profiles {
id
provider
}
}
}
});
}
`;
updates.push(q);
}
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
let q = UserModel.update({
'id': userID
}, {
$set: {
username: options.name
}
});
updates.push(q);
}
Promise
.all(updates.map((q) => q.exec()))
.then(() => {
console.log(`User ${userID} updated.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Lists all the users registered in the database.
*/
function listUsers() {
UsersService
.all()
.then((users) => {
let table = new Table({
head: [
'ID',
'Username',
'Profiles',
'Roles',
'Status',
'State'
]
});
users.forEach((user) => {
let state = user.disabled ? 'Disabled' : 'Enabled';
const profile = user.profiles.find(({provider}) => provider === 'local');
if (profile && profile.metadata && profile.metadata.confirmed_at) {
state += ', Verified';
} else {
state += ', Unverified';
try {
const answers = await inquirer.prompt({
type: 'autocomplete',
name: 'userID',
message: 'Search for a user',
source: async (answers, value) => {
if (value === null) {
value = '';
}
table.push([
user.id,
user.username,
user.profiles.map((p) => p.provider).join(', '),
user.roles.join(', '),
user.status,
state
]);
});
const { data, errors } = await graphql(schema, searchQuery, {}, ctx, {
value,
});
if (errors && errors.length > 0) {
throw errors[0];
}
console.log(table.toString());
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
if (data.users === null) {
return [];
}
/**
* Merges two users using the specified ID's.
* @param {String} dstUserID id of the user to which is the target of the merge
* @param {String} srcUserID id of the user to which is the source of the merge
*/
function mergeUsers(dstUserID, srcUserID) {
UsersService
.mergeUsers(dstUserID, srcUserID)
.then(() => {
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
return data.users.nodes.map(user => {
const emails = user.emails.join(', ');
return {
name: `${user.username} (${emails}) ${user.id.gray} - ${
user.role.gray
}`,
value: user.id,
};
});
},
});
const { userID } = answers;
const user = await UserModel.findOne({ id: userID });
printUserAsTable(user);
util.shutdown(0);
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
/**
@@ -291,127 +188,70 @@ function mergeUsers(dstUserID, srcUserID) {
* @param {String} userUD id of the user to add the role to
* @param {String} role the role to add
*/
function addRole(userID, role) {
async function setUserRole(userID) {
try {
const { role } = await inquirer.prompt([
{
name: 'role',
message: 'User Role',
type: 'list',
choices: USER_ROLES,
},
]);
if (USER_ROLES.indexOf(role) === -1) {
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
await UsersService.setRole(userID, role);
console.log(`Set User ${userID} to the ${role} role.`);
util.shutdown();
} catch (err) {
console.error(err);
util.shutdown(1);
return;
}
UsersService
.addRoleToUser(userID, role)
.then(() => {
console.log(`Added the ${role} role to User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Removes a role from a user
* @param {String} userUD id of the user to remove the role from
* @param {String} role the role to remove
*/
function removeRole(userID, role) {
if (USER_ROLES.indexOf(role) === -1) {
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
util.shutdown(1);
return;
}
UsersService
.removeRoleFromUser(userID, role)
.then(() => {
console.log(`Removed the ${role} role from User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Ban a user
* @param {String} userID id of the user to ban
*/
function ban(userID) {
UsersService
.setStatus(userID, 'BANNED')
.then(() => {
console.log(`Banned the User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Unban a user
* @param {String} userUD id of the user to remove the role from
*/
function unban(userID) {
UsersService
.setStatus(userID, 'ACTIVE')
.then(() => {
console.log(`Unban the User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Disable a given user.
* @param {String} userID the ID of a user to disable
*/
function disableUser(userID) {
UsersService
.disableUser(userID)
.then(() => {
console.log(`User ${userID} was disabled.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Enabled a given user.
* @param {String} userID the ID of a user to enable
*/
function enableUser(userID) {
UsersService
.enableUser(userID)
.then(() => {
console.log(`User ${userID} was enabled.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Verifies an email address for a user.
*
* @param userID the user's id
* @param email the user's email address to be verified
* @param email the user's email address to be verified, otherwise verifies the
* first email if there is one, if there are multiple, you get a
* prompt.
*/
async function verify(userID, email) {
async function verifyUserEmail(userID, email) {
try {
// Get the user.
const user = await UserModel.findOne({ id: userID });
if (!user) {
throw new Error(`user with ID ${userID} cannot be found`);
}
// Get all the user's email addresses.
const emails = user.emails;
if (emails.length === 0) {
throw new Error('user did not have any email addresses');
}
if (!email && emails.length === 1) {
// The email wasn't passed, and there is only one option.
email = emails[0];
} else if (!emails.includes(email)) {
// The email passed doesn't belong to this user.
throw new Error(`user does not have the email ${email}`);
} else if (emails.length > 1) {
// The email wasn't passed, and there is more than one choice.
const answers = await inquirer.prompt([
{
name: 'email',
message: 'Select Email to Verify',
type: 'list',
choices: emails,
},
]);
email = answers.email;
}
// Verify the email.
await UsersService.confirmEmail(userID, email);
console.log(`User ${userID} had their email ${email} verified.`);
util.shutdown();
@@ -425,77 +265,25 @@ async function verify(userID, email) {
// Setting up the program command line arguments.
//==============================================================================
program
.command('create')
.option('--email [email]', 'Email to use')
.option('--password [password]', 'Password to use')
.option('--name [name]', 'Name to use')
.option('--role [role]', 'Role to add')
.option('-f, --flag_mode', 'Source from flags instead of prompting')
.description('create a new user')
.action(createUser);
program
.command('delete <userID>')
.description('delete a user')
.action(deleteUser);
program
.command('passwd <userID>')
.description('change a password for a user')
.action(passwd);
program
.command('update <userID>')
.option('--email [email]', 'Email to use')
.option('--name [name]', 'Name to use')
.description('update a user')
.action(updateUser);
program
.command('list')
.description('list all the users in the database')
.action(listUsers);
.description('searches for a user based on their stored username and email')
.action(searchUsers);
program
.command('merge <dstUserID> <srcUserID>')
.description('merge srcUser into the dstUser')
.action(mergeUsers);
program
.command('addrole <userID> <role>')
.description('adds a role to a given user')
.action(addRole);
program
.command('removerole <userID> <role>')
.description('removes a role from a given user')
.action(removeRole);
program
.command('ban <userID>')
.description('ban a given user')
.action(ban);
program
.command('uban <userID>')
.description('unban a given user')
.action(unban);
program
.command('disable <userID>')
.description('disable a given user from logging in')
.action(disableUser);
program
.command('enable <userID>')
.description('enable a given user from logging in')
.action(enableUser);
.command('set-role <userID> <role>')
.description('sets the role on a user')
.action(setUserRole);
program
.command('verify <userID> <email>')
.description('verifies the given user\'s email address')
.action(verify);
.description("verifies the given user's email address")
.action(verifyUserEmail);
program.parse(process.argv);
+19 -10
View File
@@ -4,23 +4,24 @@
* Module dependencies.
*/
const program = require('./commander');
const mongoose = require('../services/mongoose');
const util = require('./util');
const program = require('commander');
const mongoose = require('../services/mongoose');
const databaseVerifications = require('./verifications/database');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function database({fix = false, limit = Infinity, batch = 1000}) {
async function database({ fix = false, limit = Infinity, batch = 1000 }) {
try {
for (const verification of databaseVerifications) {
await verification({fix, limit, batch});
await verification({ fix, limit, batch });
}
} catch (err) {
console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err);
console.error(
`Failed to process all the ${databaseVerifications.length} verifications`,
err
);
util.shutdown(1);
return;
}
@@ -36,8 +37,16 @@ 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)
.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);
-51
View File
@@ -1,51 +0,0 @@
const pkg = require('../package.json');
const dotenv = require('dotenv');
const fs = require('fs');
const program = require('commander');
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
const parseArgs = require('minimist')(process.argv.slice(2), {
alias: {
'c': 'config'
},
string: [
'config',
'pid'
],
default: {
'config': null,
'pid': null
}
});
/**
* If the config flag is present, then we have to load the configuration from
* the file specified. We will then load those values into the environment.
*/
if (parseArgs.config) {
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
Object.keys(envConfig).forEach((k) => {
process.env[k] = envConfig[k];
});
}
/**
* If the pid flag is present, then we have to create a pid file at the location
* specified.
*/
if (parseArgs.pid) {
const util = require('./util');
console.log('Wrote PID');
util.pid(parseArgs.pid);
}
module.exports = program
.version(pkg.version)
.option('-c, --config [path]', 'Specify the configuration file to load')
.option('--pid [path]', 'Specify a path to output the current PID to');
@@ -1,12 +1,12 @@
import React from 'react';
import {CoralLogo} from 'plugin-api/beta/client/components/ui';
import { CoralLogo } from 'plugin-api/beta/client/components/ui';
import styles from './MyPluginComponent.css';
class MyPluginComponent extends React.Component {
render() {
return (
<div className={styles.myPluginContainer}>
<CoralLogo className={styles.logo}/>
<CoralLogo className={styles.logo} />
<div className={styles.description}>
<h3>Plugin created by Talk CLI</h3>
+2 -3
View File
@@ -1,4 +1,3 @@
/**
This is a client index example file and it could look like this:
@@ -21,6 +20,6 @@ import MyPluginComponent from './components/MyPluginComponent';
export default {
slots: {
stream: [MyPluginComponent]
}
stream: [MyPluginComponent],
},
};
+21 -48
View File
@@ -1,7 +1,9 @@
const debug = require('debug')('talk:util');
const fs = require('fs');
// Setup the environment.
require('../services/env');
const util = module.exports = {};
const debug = require('debug')('talk:util');
const util = (module.exports = {});
/**
* Stores an array of functions that should be executed in the event that the
@@ -15,20 +17,22 @@ util.toshutdown = [];
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
*/
util.shutdown = (defaultCode = 0, signal = null) => {
if (signal) {
debug(`Reached ${signal} signal`);
}
debug(`${util.toshutdown.length} jobs now being called`);
Promise
.all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func))
Promise.all(
util.toshutdown
.map(func => (func ? func(signal) : null))
.filter(func => func)
)
.then(() => {
debug('Shutdown complete, now exiting');
process.exit(defaultCode);
})
.catch((err) => {
.catch(err => {
console.error(err);
process.exit(1);
@@ -41,54 +45,23 @@ util.shutdown = (defaultCode = 0, signal = null) => {
* @param {Array} jobs Array of promise capable shutdown functions that are
* executed.
*/
util.onshutdown = (jobs) => {
util.onshutdown = jobs => {
debug(`${jobs.length} jobs registered to be called during shutdown`);
// Add the new jobs to shutdown to the object reference.
util.toshutdown = util.toshutdown.concat(jobs);
};
/**
* Register a PID file to be maintained for the lifespan of the process.
* @param {String} path path to the PID file to create
*/
util.pid = (path) => {
if (!/\//.test(path)) {
if (!/\.pid/.test(path)) {
path += '.pid';
}
path = `/tmp/${path}`;
}
const pid = `${process.pid.toString()}\n`;
fs.writeFile(path, pid, (err) => {
if (err) {
console.error(`Can't write PID file: ${err}`);
throw err;
}
// Add the cleanup for the fs onto the shutdown.
util.onshutdown([
() => new Promise((resolve, reject) => {
// Remove the pid file.
fs.unlink(path, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
})
]);
});
};
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
// event that we have an external event. SIGUSR2 is called when the app is asked
// to be 'killed', same procedure here.
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
+194
View File
@@ -0,0 +1,194 @@
const UserModel = require('../../../models/user');
const CommentModel = require('../../../models/comment');
const ActionsService = require('../../../services/actions');
const { arrayJoinBy } = require('../../../graph/loaders/util');
const { get } = require('lodash');
const debug = require('debug')('talk:cli:verify');
const MODELS = [UserModel, CommentModel];
async function processBatch(Model, documents) {
// Get an array of all the document id's.
const documentIDs = documents.map(({ id }) => id);
// Store all the operations on this batch in this array that we'll return
// later.
const operations = [];
// Get the action summaries for this batch.
const totalActionSummaries = await ActionsService.getActionSummaries(
documentIDs
).then(arrayJoinBy(documentIDs, 'item_id'));
// Iterate over the documents.
for (let i = 0; i < documents.length; i++) {
const document = documents[i];
const actionSummaries = totalActionSummaries[i];
let ops = [];
for (const actionSummary of actionSummaries) {
if (actionSummary.group_id === null) {
continue;
}
// And we generate the group id.
const ACTION_TYPE = actionSummary.action_type.toLowerCase();
const GROUP_ID = 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 (
get(document, ['action_counts', ACTION_COUNT_FIELD]) !==
actionSummary.count
) {
// Batch updates for those changes.
ops.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
});
}
}
// Group all the action summaries together from all the different group
// ids.
const groupedActionSummaries = actionSummaries.reduce(
(acc, actionSummary) => {
// action_type is already snake cased (as it would have had to be when it
// was inserted in the database).
const ACTION_TYPE = 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 (get(document, ['action_counts', ACTION_COUNT_FIELD]) !== count) {
// Batch updates for those changes.
ops.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
});
}
}
// If this comment has action summaries that should be updated, then
// perform an update!
if (ops.length > 0) {
operations.push({
updateOne: {
filter: {
id: document.id,
},
update: {
$set: Object.assign({}, ...ops),
},
},
});
}
}
return operations;
}
module.exports = async ({ fix, batch }) => {
for (const Model of MODELS) {
const cursor = Model.collection
.find({})
.project({
id: 1,
action_counts: 1,
})
.sort({ created_at: 1 });
let operations = [];
let documents = [];
// While there are documents to process.
while (await cursor.hasNext()) {
// Load the document.
const document = await cursor.next();
// Push the document into the documents array.
documents.push(document);
// Check to see if the length of the documents array requires us to
// process it.
if (documents.length > batch) {
// Process this batch.
let batchOperations = await processBatch(Model, documents);
// Push the batch operations into the model operations.
operations.push(...batchOperations);
// Clear this batch contents.
documents = [];
}
}
// Check to see if there are any documents left over.
if (documents.length > 0) {
// Process this batch.
let batchOperations = await processBatch(Model, documents);
// Push the batch operations into the model operations.
operations.push(...batchOperations);
}
const OPERATIONS_LENGTH = operations.length;
console.log(
`action_counts.js: ${OPERATIONS_LENGTH} ${
Model.collection.name
} need their action counts fixed.`
);
// If fix was enabled, execute the batch writes.
if (OPERATIONS_LENGTH > 0) {
if (fix) {
debug(
`action_counts.js: fixing ${OPERATIONS_LENGTH} ${
Model.collection.name
}...`
);
while (operations.length) {
let result = await Model.collection.bulkWrite(
operations.splice(0, batch)
);
debug(
`action_counts.js: fixed batch of ${result.modifiedCount} ${
Model.collection.name
}.`
);
}
console.log(
`action_counts.js: applied all ${OPERATIONS_LENGTH} fixes to ${
Model.collection.name
}.`
);
} else {
console.warn(
'Skipping fixing, --fix was not enabled, pass --fix to fix these errors'
);
}
}
}
};
@@ -0,0 +1,140 @@
const CommentModel = require('../../../models/comment');
const { singleJoinBy } = require('../../../graph/loaders/util');
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)));
// Loop over the comments, with their action summaries.
for (let i = 0; i < comments.length; i++) {
let comment = comments[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,
});
}
// 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'
);
}
}
};
-193
View File
@@ -1,193 +0,0 @@
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');
}
}
};
+2 -4
View File
@@ -6,7 +6,5 @@
//
// async ({fix = false, batch = 1000}) => {}
//
// where their options are derrived.
module.exports = [
require('./comments'),
];
// where their options are derived.
module.exports = [require('./comment_replies'), require('./action_counts')];
+15 -3
View File
@@ -7,6 +7,8 @@ machine:
environment:
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
NODE_ENV: "test"
MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml"
MOCHA_REPORTER: "mocha-junit-reporter"
pre:
# TODO: use the following to add in support for MongoDB 3.4.
# # Upgrade the database version to 3.4.
@@ -17,6 +19,13 @@ machine:
# - sudo apt-get install -y mongodb-org
# - sudo service mongod restart
# Force sync the time.
- date
- sudo service ntp stop
- sudo ntpdate -s time.nist.gov
- sudo service ntp start
- date
# Install chromium for e2e and remove old google-chrome
- sudo rm -rf /opt/google/chrome
- sudo rm -f /usr/bin/google-chrome*
@@ -47,10 +56,11 @@ database:
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
- yarn test
# Run the end to end tests
- yarn e2e:ci
# Check dependancies using nsp.
- nsp check
- yarn e2e-ci
deployment:
release:
@@ -59,7 +69,9 @@ deployment:
- bash ./scripts/docker.sh deploy
latest:
branch: master
branch:
- master
- next
owner: coralproject
commands:
- bash ./scripts/docker.sh deploy
+17 -19
View File
@@ -1,46 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
import Configure from 'routes/Configure';
import Dashboard from 'routes/Dashboard';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community/containers/Community';
import {ModerationLayout, Moderation} from 'routes/Moderation';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
const routes = (
<div>
<Route exact path="/admin/install" component={Install}/>
<Route path='/admin' component={Layout}>
<IndexRedirect to='/admin/moderate' />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
<Route path='dashboard' component={Dashboard} />
<Route exact path="/admin/install" component={Install} />
<Route path="/admin" component={Layout}>
<IndexRedirect to="/admin/moderate" />
<Route path="configure" component={Configure} />
<Route path="stories" component={Stories} />
{/* Community Routes */}
<Route path='community'>
<Route path='flagged' components={Community}>
<Route path=':id' components={Community} />
<Route path="community">
<Route path="flagged" components={Community}>
<Route path=":id" components={Community} />
</Route>
<Route path='people' components={Community}>
<Route path=':id' components={Community} />
<Route path="people" components={Community}>
<Route path=":id" components={Community} />
</Route>
<IndexRedirect to='flagged' />
<IndexRedirect to="flagged" />
</Route>
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<Route path="moderate" component={ModerationLayout}>
<IndexRoute components={Moderation} />
<Route path=':tabOrId' components={Moderation} />
<Route path=":tabOrId" components={Moderation} />
<Route path=':tab' components={Moderation}>
<Route path=':id' components={Moderation} />
<Route path=":tab" components={Moderation}>
<Route path=":id" components={Moderation} />
</Route>
</Route>
</Route>
+43 -34
View File
@@ -7,26 +7,29 @@ import jwtDecode from 'jwt-decode';
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
dispatch({type: actions.LOGIN_REQUEST});
export const handleLogin = (email, password, recaptchaResponse) => (
dispatch,
_,
{ rest, client, storage }
) => {
dispatch({ type: actions.LOGIN_REQUEST });
const params = {
method: 'POST',
body: {
email,
password
}
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse
'X-Recaptcha-Response': recaptchaResponse,
};
}
return rest('/auth/local', params)
.then(({user, token}) => {
.then(({ user, token }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
@@ -38,19 +41,19 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch({
type: actions.LOGIN_FAILURE,
message: t('error.email_password')
message: t('error.email_password'),
});
}
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: t(`error.${error.translation_key}`),
@@ -69,27 +72,32 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
//==============================================================================
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
});
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
});
const forgotPasswordFailure = (error) => ({
const forgotPasswordFailure = error => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return rest('/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) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
@@ -99,24 +107,24 @@ export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
//==============================================================================
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST
type: actions.CHECK_LOGIN_REQUEST,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
isAdmin,
});
const checkLoginFailure = (error) => ({
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error
error,
});
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
export const checkLogin = () => (dispatch, _, { rest, client, storage }) => {
dispatch(checkLoginRequest());
return rest('/auth')
.then(({user}) => {
.then(({ user }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
@@ -127,9 +135,11 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
@@ -138,8 +148,8 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
// LOGOUT
//==============================================================================
export const logout = () => (dispatch, _, {rest, client, storage}) => {
return rest('/auth', {method: 'DELETE'}).then(() => {
export const logout = () => (dispatch, _, { rest, client, storage }) => {
return rest('/auth', { method: 'DELETE' }).then(() => {
if (storage) {
storage.removeItem('token');
}
@@ -147,7 +157,7 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
// Reset the websocket.
client.resetWebsocket();
dispatch({type: actions.LOGOUT});
dispatch({ type: actions.LOGOUT });
});
};
@@ -155,11 +165,10 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
export const handleAuthToken = token => (dispatch, _, { storage }) => {
if (storage) {
storage.setItem('exp', jwtDecode(token).exp);
storage.setItem('token', token);
}
dispatch({type: 'HANDLE_AUTH_TOKEN'});
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
};
@@ -1,7 +1,19 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
export const showBanUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_BAN_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideBanUserDialog = () => ({ type: HIDE_BAN_USER_DIALOG });
+23 -32
View File
@@ -7,73 +7,64 @@ import {
SORT_UPDATE,
SET_PAGE,
SET_SEARCH_VALUE,
SET_ROLE,
SET_COMMENTER_STATUS,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG
HIDE_REJECT_USERNAME_DIALOG,
} from '../constants/community';
import t from 'coral-framework/services/i18n';
export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => {
export const fetchUsers = (query = {}) => (dispatch, _, { rest }) => {
dispatch(requestFetchUsers());
rest(`/users?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>{
.then(({ result, page, count, limit, totalPages }) => {
dispatch({
type: FETCH_USERS_SUCCESS,
users: result,
page,
count,
limit,
totalPages
totalPages,
});
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_USERS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_USERS_FAILURE, error: errorMessage });
});
};
const requestFetchUsers = () => ({
type: FETCH_USERS_REQUEST
type: FETCH_USERS_REQUEST,
});
export const updateSorting = (sort) => ({
export const updateSorting = sort => ({
type: SORT_UPDATE,
sort
sort,
});
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
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, _, {rest}) => {
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
// Ban User Dialog
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
// Reject Username Dialog
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
export const showRejectUsernameDialog = user => ({
type: SHOW_REJECT_USERNAME_DIALOG,
user,
});
export const hideRejectUsernameDialog = () => ({
type: HIDE_REJECT_USERNAME_DIALOG,
});
+2 -2
View File
@@ -1,7 +1,7 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => (dispatch) => {
export const fetchConfig = () => dispatch => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({type: CONFIG_UPDATED, data});
dispatch({ type: CONFIG_UPDATED, data });
};
+5 -5
View File
@@ -1,13 +1,13 @@
import * as actions from 'constants/configure';
export const updatePending = ({updater, errorUpdater}) => {
return {type: actions.UPDATE_PENDING, updater, errorUpdater};
export const updatePending = ({ updater, errorUpdater }) => {
return { type: actions.UPDATE_PENDING, updater, errorUpdater };
};
export const clearPending = () => {
return {type: actions.CLEAR_PENDING};
return { type: actions.CLEAR_PENDING };
};
export const setActiveSection = (section) => {
return {type: actions.SET_ACTIVE_SECTION, section};
export const setActiveSection = section => {
return { type: actions.SET_ACTIVE_SECTION, section };
};
+77 -60
View File
@@ -3,17 +3,17 @@ import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import t from 'coral-framework/services/i18n';
export const nextStep = () => ({type: actions.NEXT_STEP});
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
export const nextStep = () => ({ type: actions.NEXT_STEP });
export const previousStep = () => ({ type: actions.PREVIOUS_STEP });
export const goToStep = step => ({ type: actions.GO_TO_STEP, step });
const installRequest = () => ({type: actions.INSTALL_REQUEST});
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
const installRequest = () => ({ type: actions.INSTALL_REQUEST });
const installSuccess = () => ({ type: actions.INSTALL_SUCCESS });
const installFailure = error => ({ type: actions.INSTALL_FAILURE, error });
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
const hasError = (error) => ({type: actions.HAS_ERROR, error});
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
const addError = (name, error) => ({ type: actions.ADD_ERROR, name, error });
const hasError = error => ({ type: actions.HAS_ERROR, error });
const clearErrors = () => ({ type: actions.CLEAR_ERRORS });
const validation = (formData, dispatch, next) => {
if (!(formData != null)) {
@@ -21,24 +21,21 @@ const validation = (formData, dispatch, next) => {
return;
}
const validKeys = Object.keys(formData)
.filter((name) => name !== 'domains');
const validKeys = Object.keys(formData).filter(name => name !== 'domains');
// Required Validation
const empty = validKeys
.filter((name) => {
const cond = !formData[name].length;
const empty = validKeys.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());
@@ -46,19 +43,17 @@ const validation = (formData, dispatch, next) => {
}
// RegExp Validation
const validation = validKeys
.filter((name) => {
const cond = !validate[name](formData[name]);
if (cond) {
const validation = validKeys.filter(name => {
const cond = !validate[name](formData[name]);
if (cond) {
// Adding Error
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (validation.length) {
dispatch(hasError());
@@ -67,20 +62,21 @@ const validation = (formData, dispatch, next) => {
// Confirm Validation
const prefixLength = 'confirm'.length;
const confirm = validKeys
.filter((name) => {
if (!name.startsWith('confirm')) {
return false;
}
const confirm = validKeys.filter(name => {
if (!name.startsWith('confirm')) {
return false;
}
// Check that 'confirmX' equals 'X'.
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
// Check that 'confirmX' equals 'X'.
const other =
name.substr(prefixLength, 1).toLowerCase() +
name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
if (confirm.length) {
dispatch(hasError());
@@ -105,41 +101,62 @@ export const submitUser = () => (dispatch, getState) => {
});
};
export const finishInstall = () => (dispatch, getState, {rest}) => {
export const finishInstall = () => (dispatch, getState, { rest }) => {
const data = getState().install.data;
dispatch(installRequest());
return rest('/setup', {method: 'POST', body: data})
return rest('/setup', { method: 'POST', body: data })
.then(() => {
dispatch(installSuccess());
dispatch(nextStep());
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(installFailure(errorMessage));
});
};
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
export const updateSettingsFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_SETTINGS,
name,
value,
});
export const updateUserFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_USER,
name,
value,
});
export const updatePermittedDomains = value => ({
type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS,
value,
});
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});
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) => async (dispatch, _, {rest}) => {
export const checkInstall = next => async (dispatch, _, { rest }) => {
dispatch(checkInstallRequest());
try {
const {installed} = await rest('/setup');
const { installed } = await rest('/setup');
dispatch(checkInstallSuccess(installed));
if (installed) {
next();
}
} catch (error) {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkInstallFailure(errorMessage));
}
};
+16 -12
View File
@@ -1,36 +1,40 @@
import * as actions from 'constants/moderation';
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
export const singleView = () => ({ type: actions.SINGLE_VIEW });
// hide shortcuts note
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
export const hideShortcutsNote = () => (dispatch, _, { storage }) => {
try {
if (storage) {
storage.setItem('coral:shortcutsNote', 'hide');
}
} catch (e) {
// above will fail in Safari private mode
}
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
};
export const setSortOrder = (order) => ({
export const setSortOrder = order => ({
type: actions.SET_SORT_ORDER,
order
order,
});
export const toggleStorySearch = (active) => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
export const toggleStorySearch = active => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH,
});
export const storySearchChange = (value) => ({
export const storySearchChange = value => ({
type: actions.STORY_SEARCH_CHANGE_VALUE,
value
value,
});
export const clearState = () => ({
type: actions.MODERATION_CLEAR_STATE
type: actions.MODERATION_CLEAR_STATE,
});
export const selectCommentId = id => ({
type: actions.MODERATION_SELECT_COMMENT,
id,
});
+27 -22
View File
@@ -10,7 +10,7 @@ import {
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE,
UPDATE_ASSETS
UPDATE_ASSETS,
} from '../constants/stories';
import t from 'coral-framework/services/i18n';
@@ -21,53 +21,58 @@ 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 = (query = {}) => (dispatch, _, {rest}) => {
dispatch({type: FETCH_ASSETS_REQUEST});
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
dispatch({ type: FETCH_ASSETS_REQUEST });
return rest(`/assets?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
.then(({ result, page, count, limit, totalPages }) =>
dispatch({
type: FETCH_ASSETS_SUCCESS,
assets: result,
page,
count,
limit,
totalPages,
}))
.catch((error) => {
})
)
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_ASSETS_FAILURE, error: errorMessage });
});
};
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch((error) => {
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage });
});
};
export const updateAssets = (assets) => (dispatch) => {
dispatch({type: UPDATE_ASSETS, assets});
export const updateAssets = assets => dispatch => {
dispatch({ type: UPDATE_ASSETS, assets });
};
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
export const setCriteria = (criteria) => ({
export const setCriteria = criteria => ({
type: SET_CRITERIA,
criteria,
});
@@ -1,7 +1,19 @@
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog.js';
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
export const showSuspendUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_SUSPEND_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideSuspendUserDialog = () => ({ type: HIDE_SUSPEND_USER_DIALOG });
+18 -9
View File
@@ -1,28 +1,37 @@
import * as actions from 'constants/userDetail';
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const viewUserDetail = userId => ({
type: actions.VIEW_USER_DETAIL,
userId,
});
export const hideUserDetail = () => ({ type: actions.HIDE_USER_DETAIL });
export const changeUserDetailStatuses = (tab) => {
export const changeTab = tab => {
let statuses = null;
if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
return { type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses };
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
export const clearUserDetailSelections = () => ({
type: actions.CLEAR_USER_DETAIL_SELECTIONS,
});
export const toggleSelectCommentInUserDetail = (id, active) => {
return {
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
id
type: active
? actions.SELECT_USER_DETAIL_COMMENT
: actions.UNSELECT_USER_DETAIL_COMMENT,
id,
};
};
export const toggleSelectAllCommentInUserDetail = (ids, active) => {
return {
type: active ? actions.SELECT_ALL_USER_DETAIL_COMMENT : actions.CLEAR_USER_DETAIL_SELECTIONS,
ids
type: active
? actions.SELECT_ALL_USER_DETAIL_COMMENT
: actions.CLEAR_USER_DETAIL_SELECTIONS,
ids,
};
};
+39 -21
View File
@@ -6,38 +6,56 @@ import t from 'coral-framework/services/i18n';
*/
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch, _, {rest}) => {
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch((error) => {
return (dispatch, _, { rest }) => {
dispatch({ type: userTypes.UPDATE_STATUS_REQUEST });
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);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.UPDATE_STATUS_FAILURE,
error: errorMessage,
});
});
};
};
// change status of a user
export const sendNotificationEmail = (userId, 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();
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
});
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();
dispatch({ type: userTypes.USER_EMAIL_FAILURE, error: errorMessage });
});
};
};
// let a user edit their username
export const enableUsernameEdit = (userId) => {
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/username-enable`, {method: 'POST'})
.catch((error) => {
export const enableUsernameEdit = userId => {
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();
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.USERNAME_ENABLE_FAILURE,
error: errorMessage,
});
}
);
};
};
@@ -0,0 +1,30 @@
.table {
flex-direction: column;
}
.table, .headerRow, .row {
display: flex;
}
.headerRowItem, .item {
flex: 1;
padding: 20px;
&:nth-child(2) {
flex: 2;
}
}
.headerRowItem {
color: #595959;
font-weight: bold;
}
.headerRow, .row {
border-bottom: 1px solid #e0e0e0;
}
.action {
color: black;
font-weight: bold;
}
@@ -0,0 +1,103 @@
import React from 'react';
import PropTypes from 'prop-types';
import { murmur3 } from 'murmurhash-js';
import styles from './AccountHistory.css';
import cn from 'classnames';
import flatten from 'lodash/flatten';
import orderBy from 'lodash/orderBy';
import moment from 'moment';
const buildUserHistory = (userState = {}) => {
return orderBy(
flatten(
Object.keys(userState.status)
.filter(k => k !== '__typename')
.map(k => userState.status[k].history)
),
'created_at',
'desc'
);
};
const buildActionResponse = (typename, until, status) => {
switch (typename) {
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed';
default:
return '-';
}
};
const getModerationValue = (userId, assignedBy = {}) => {
if (assignedBy && userId !== assignedBy.id) {
return assignedBy.username;
}
return 'SYSTEM';
};
class AccountHistory extends React.Component {
render() {
const { user } = this.props;
const userHistory = buildUserHistory(user.state);
return (
<div>
<div className={cn(styles.table, 'talk-admin-account-history')}>
<div
className={cn(
styles.headerRow,
'talk-admin-account-history-header-row'
)}
>
<div className={styles.headerRowItem}>Date</div>
<div className={styles.headerRowItem}>Action</div>
<div className={styles.headerRowItem}>Moderation</div>
</div>
{userHistory.map(
({ __typename, created_at, assigned_by, until, status }) => (
<div
className={cn(styles.row, 'talk-admin-account-history-row')}
key={`${__typename}_${murmur3(created_at)}`}
>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-date'
)}
>
{moment(new Date(created_at)).format('MMM DD, YYYY')}
</div>
<div
className={cn(
styles.item,
styles.action,
'talk-admin-account-history-row-status'
)}
>
{buildActionResponse(__typename, until, status)}
</div>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-assigned-by'
)}
>
{getModerationValue(user.id, assigned_by)}
</div>
</div>
)
)}
</div>
</div>
);
}
}
AccountHistory.propTypes = {
user: PropTypes.object.isRequired,
};
export default AccountHistory;
@@ -8,9 +8,6 @@
color: black;
> :global(.mdl-menu__container) {
margin-left: 10px;
> :global(.mdl-menu__outline) {
box-shadow: none;
}
}
}
@@ -18,12 +15,13 @@
box-shadow: none;
color: white;
background-color: #616161;
border-color: #616161;
}
.arrowIcon {
margin-left: 6px;
margin-right: 0;
vertical-align: middle;
vertical-align: middle;
margin-right: 0;
font-size: 14px;
}
@@ -33,8 +31,10 @@
}
.menuItem {
background-color: #2a2a2a;
color: white;
color: #2a2a2a;
background-color: white;
font-size: 0.95em;
&:first-child {
margin-bottom: 1px;
border-radius: 2px 2px 0px 0px;
@@ -43,7 +43,8 @@
border-radius: 0px 0px 2px 2px;
}
&:hover, &:active, &:focus {
background-color: #767676;
background-color: #e2e2e2;
border-color: #616161;
}
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
background-color: #262626;
@@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Icon} from 'coral-ui';
import {Menu} from 'react-mdl';
import { Button, Icon } from 'coral-ui';
import ClickOutside from 'coral-framework/components/ClickOutside';
import { Menu } from 'react-mdl';
import cn from 'classnames';
import {findDOMNode} from 'react-dom';
import { findDOMNode } from 'react-dom';
import styles from './ActionsMenu.css';
import t from 'coral-framework/services/i18n';
@@ -13,46 +14,60 @@ let count = 0;
class ActionsMenu extends React.Component {
id = `actions-dropdown-${count++}`;
menu = null;
state = {open: false};
state = { open: false };
timeout = null;
componentWillUnmount() {
clearTimeout(this.timeout);
}
handleRef = (ref) => {
handleRef = ref => {
this.menu = ref ? findDOMNode(ref).parentNode : null;
}
};
syncOpenState = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
this.setState({ open: this.menu.className.indexOf('is-visible') >= 0 });
}, 150);
};
render() {
const {className = ''} = this.props;
const { className = '', buttonClassNames = '', label = '' } = this.props;
return (
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
disabled={false}
id={this.id}
<ClickOutside onClickOutside={this.syncOpenState}>
<div
className={cn(styles.root, className)}
onBlur={this.syncOpenState}
onClick={this.syncOpenState}
icon={this.props.icon}
raised>
{t('modqueue.actions')}
<Icon
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
className={styles.arrowIcon}
/>
</Button>
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
{this.props.children}
</Menu>
</div>
onKeyUp={this.syncOpenState}
>
<Button
cStyle="actions"
className={cn(
styles.button,
{ [styles.buttonOpen]: this.state.open },
buttonClassNames
)}
disabled={false}
id={this.id}
onClick={this.syncOpenState}
icon={this.props.icon}
raised
>
{label ? label : t('modqueue.actions')}
<Icon
name={
this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'
}
className={styles.arrowIcon}
/>
</Button>
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
{this.props.children}
</Menu>
</div>
</ClickOutside>
);
}
}
@@ -61,6 +76,8 @@ ActionsMenu.propTypes = {
icon: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
label: PropTypes.string,
buttonClassNames: PropTypes.string,
};
export default ActionsMenu;
@@ -1,13 +1,18 @@
import React from 'react';
import cn from 'classnames';
import {MenuItem} from 'react-mdl';
import { MenuItem } from 'react-mdl';
import PropTypes from 'prop-types';
import styles from './ActionsMenu.css';
import camelCase from 'lodash/camelCase';
const ActionsMenuItem = (props) =>
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
const ActionsMenuItem = props => (
<MenuItem
className={cn(styles.menuItem, props.className, 'action-menu-item')}
{...props}
id={camelCase(props.children)}
/>
);
ActionsMenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.string,
+80 -54
View File
@@ -2,102 +2,128 @@ import React from 'react';
import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
import cn from 'classnames';
class AdminLogin extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {email: '', password: '', requestPassword: false};
this.state = { email: '', password: '', requestPassword: false };
}
handleSignIn = (e) => {
handleSignIn = e => {
e.preventDefault();
this.props.handleLogin(this.state.email, this.state.password);
}
};
onRecaptchaLoad = () => {
// do something?
}
};
onRecaptchaVerify = (recaptchaResponse) => {
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
}
onRecaptchaVerify = recaptchaResponse => {
this.props.handleLogin(
this.state.email,
this.state.password,
recaptchaResponse
);
};
handleRequestPassword = (e) => {
handleRequestPassword = e => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
};
render () {
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
render() {
const { errorMessage, loginMaxExceeded, recaptchaPublic } = this.props;
const signInForm = (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label='Email Address'
label="Email Address"
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
onChange={e => this.setState({ email: e.target.value })}
/>
<TextField
id="password"
label='Password'
label="Password"
value={this.state.password}
onChange={(e) => this.setState({password: e.target.value})}
type='password' />
<div style={{height: 10}}></div>
onChange={e => this.setState({ password: e.target.value })}
type="password"
/>
<div style={{ height: 10 }} />
<Button
className="talk-admin-login-sign-in-button"
type='submit'
cStyle='black'
type="submit"
cStyle="black"
full
onClick={this.handleSignIn}>Sign In</Button>
onClick={this.handleSignIn}
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
e.preventDefault();
this.setState({requestPassword: true});
}}>Request a new one.</a>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={e => {
e.preventDefault();
this.setState({ requestPassword: true });
}}
>
Request a new one.
</a>
</p>
{
loginMaxExceeded &&
{loginMaxExceeded && (
<Recaptcha
sitekey={recaptchaPublic}
render='explicit'
theme='dark'
render="explicit"
theme="dark"
onloadCallback={this.onRecaptchaLoad}
verifyCallback={this.onRecaptchaVerify} />
}
verifyCallback={this.onRecaptchaVerify}
/>
)}
</form>
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
const requestPasswordForm = this.props.passwordRequestSuccess ? (
<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>
}}
>
{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>
);
return (
<Layout fixedDrawer restricted={true}>
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
<h1 className={styles.loginHeader}>Team sign in</h1>
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
{ this.state.requestPassword ? requestPasswordForm : signInForm }
<p className={styles.loginCTA}>
Sign in to interact with your community.
</p>
{this.state.requestPassword ? requestPasswordForm : signInForm}
</div>
</Layout>
);
+1 -1
View File
@@ -5,7 +5,7 @@ import 'material-design-lite';
import AppRouter from '../AppRouter';
export default class App extends React.Component {
render () {
render() {
return (
<div>
<ToastContainer />
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick, className}) => {
const ApproveButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
@@ -28,4 +32,3 @@ ApproveButton.propTypes = {
};
export default ApproveButton;
@@ -1,153 +1,153 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 500px;
width: 400px;
top: 50%;
transform: translateY(-50%);
height: 184px;
padding: 20px;
border-radius: 4px;
}
h2 {
color: black;
font-size: 1.76em;
font-weight: 500;
margin: 0;
}
.header {
color: black;
font-size: 1.5em;
font-weight: 500;
margin: 0 0 8px 0;
}
h3 {
color: black;
font-size: 1.4em;
font-weight: 500;
margin: 0;
}
.subheader {
color: black;
font-size: 1.3em;
font-weight: 500;
margin: 0 0 8px 0;
}
.textField {
margin-top: 15px;
margin-top: 15px;
}
.textField label {
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
}
.textField input {
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
}
.footer {
margin: 20px auto 10px;
text-align: center;
margin: 20px auto 10px;
text-align: center;
}
.footer span {
display: block;
margin-bottom: 5px;
display: block;
margin-bottom: 5px;
}
.footer a {
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
}
.socialConnections {
margin-bottom: 20px;
margin-bottom: 20px;
}
.signInButton {
margin-top: 10px;
margin-top: 10px;
}
.close {
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
}
.close:hover {
color: #6b6b6b;
color: #6b6b6b;
}
input.error{
border: solid 2px #f44336;
border: solid 2px #f44336;
}
.errorMsg, .hint {
color: grey;
font-weight: 600;
padding: 3px 0 16px;
color: grey;
font-weight: 600;
padding: 3px 0 16px;
}
.alert {
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
.alert--success {
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
}
.alert--error {
background: #FFEBEE;
color: #B71C1C;
background: #FFEBEE;
color: #B71C1C;
}
.userBox a {
color: #2c69b6;
cursor: pointer;
margin: 0px;
color: #2c69b6;
cursor: pointer;
margin: 0px;
}
.attention {
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
}
.action {
margin-top: 15px;
margin-top: 15px;
}
.passwordRequestSuccess {
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
}
.passwordRequestFailure {
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
}
.cancel {
@@ -160,6 +160,20 @@ input.error{
}
.buttons {
margin: 20px;
text-align: center;
margin-top: 8px;
margin-bottom: 6px;
text-align: right;
}
.legend {
padding: 0;
font-weight: bold;
}
.messageInput {
border-radius: 3px;
width: 100%;
padding: 10px;
font-size: 14px;
box-sizing: border-box;
}
@@ -1,45 +1,130 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import { Dialog } from 'coral-ui';
import styles from './BanUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')}>
<span className={styles.close} onClick={onCancel}>×</span>
<div className={styles.header}>
<h2>{t('bandialog.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{t('bandialog.are_you_sure', username)}</h3>
<i>{info}</i>
</div>
<div className={styles.buttons}>
<Button
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
cStyle="cancel"
onClick={onCancel}
raised >
{t('bandialog.cancel')}
</Button>
<Button
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
{t('bandialog.yes_ban_user')}
</Button>
</div>
</Dialog>
);
const initialState = { step: 0, message: '' };
class BanUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
if (this.props.open && !next.open) {
this.setState(initialState);
}
}
handleMessageChange = e => {
const { value: message } = e;
this.setState({ message });
};
goToStep1 = () => {
this.setState({
step: 1,
message: t('bandialog.email_message_ban', this.props.username),
});
};
renderStep0() {
const { onCancel, username, info } = this.props;
return (
<section>
<h2 className={styles.header}>{t('bandialog.ban_user')}</h2>
<h3 className={styles.subheader}>
{t('bandialog.are_you_sure', username)}
</h3>
<p className={styles.description}>{info}</p>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised
>
{t('bandialog.cancel')}
</Button>
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={this.goToStep1}
raised
>
{t('bandialog.yes_ban_user')}
</Button>
</div>
</section>
);
}
renderStep1() {
const { onCancel, onPerform } = this.props;
const { message } = this.state;
return (
<section>
<h2 className={styles.header}>{t('bandialog.notify_ban_headline')}</h2>
<p className={styles.description}>
{t('bandialog.notify_ban_description')}
</p>
<fieldset>
<legend className={styles.legend}>
{t('bandialog.write_a_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised
>
{t('bandialog.cancel')}
</Button>
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised
>
{t('bandialog.send')}
</Button>
</div>
</section>
);
}
render() {
const { step } = this.state;
const { open, onCancel } = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')}
>
<span className={styles.close} onClick={onCancel}>
×
</span>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
</Dialog>
);
}
}
BanUserDialog.propTypes = {
open: PropTypes.bool,
@@ -1,15 +1,11 @@
import React from 'react';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import t from 'coral-framework/services/i18n';
import {withCopyToClipboard} from 'coral-framework/hocs';
import { withCopyToClipboard } from 'coral-framework/hocs';
class ButtonCopyToClipboard extends React.Component {
render () {
return (
<Button {...this.props} >
{t('common.copy')}
</Button>
);
render() {
return <Button {...this.props}>{t('common.copy')}</Button>;
}
}
@@ -1,10 +1,10 @@
import React from 'react';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
import { murmur3 } from 'murmurhash-js';
import { CSSTransitionGroup } from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
import PropTypes from 'prop-types';
const CommentBodyHighlighter = ({children, body}) => {
const CommentBodyHighlighter = ({ children, body }) => {
return (
<CSSTransitionGroup
component={'div'}
@@ -20,7 +20,9 @@ const CommentBodyHighlighter = ({children, body}) => {
transitionEnterTimeout={3600}
transitionLeaveTimeout={2800}
>
{React.cloneElement(React.Children.only(children), {key: murmur3(body)})}
{React.cloneElement(React.Children.only(children), {
key: murmur3(body),
})}
</CSSTransitionGroup>
);
};
@@ -1,5 +1,5 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
import memoize from 'lodash/memoize';
function escapeRegExp(string) {
@@ -9,18 +9,18 @@ function escapeRegExp(string) {
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map((phrase) =>
phrase.split(/\s+/)
.map((word) => escapeRegExp(word))
.map(phrase =>
phrase
.split(/\s+/)
.map(word => escapeRegExp(word))
.join('[\\s"?!.]+')
).join('|');
)
.join('|');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
} catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
@@ -39,10 +39,9 @@ const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map((token, i) =>
i % 3 === 2
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
: token
return tokens.map(
(token, i) =>
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
);
}
@@ -53,34 +52,26 @@ function markLinks(body) {
const content = [];
let index = 0;
if (matches) {
matches
.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
matches.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
export default ({suspectWords, bannedWords, body, ...rest}) => {
export default ({ suspectWords, bannedWords, body, ...rest }) => {
// First highlight links.
const content = markLinks(body)
.map((element, index) => {
const content = markLinks(body).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<div {...rest}>
{content}
</div>
);
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return <div {...rest}>{content}</div>;
};
@@ -5,7 +5,7 @@
.moreDetail {
position: absolute;
font-size: 12px;
font-size: 13px;
font-weight: 500;
color: black;
right: 16px;
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
@@ -7,25 +7,26 @@ import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
class CommentDetails extends Component {
state = {
showDetail: false
showDetail: false,
};
constructor () {
constructor() {
super();
this.state = {
showDetail: false
showDetail: false,
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
this.setState(state => ({
showDetail: !state.showDetail,
}));
}
this.props.clearHeightCache && this.props.clearHeightCache();
};
render() {
const {data, root, comment} = this.props;
const {showDetail} = this.state;
const { data, root, comment, clearHeightCache } = this.props;
const { showDetail } = this.state;
const queryData = {
root,
comment,
@@ -44,14 +45,18 @@ class CommentDetails extends Component {
<Slot
fill="adminCommentDetailArea"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
queryData={queryData}
/>}
{showDetail && (
<Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
)}
</div>
);
}
@@ -61,6 +66,7 @@ CommentDetails.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
clearHeightCache: PropTypes.func,
};
export default CommentDetails;
@@ -9,37 +9,67 @@ import styles from './CommentLabels.css';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
function isUserFlagged(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
return actions.some(
action => action.__typename === 'FlagAction' && action.user
);
}
function getUserFlaggedType(actions) {
return actions
.some((action) =>
return actions.some(
action =>
action.__typename === 'FlagAction' &&
action.user &&
action.user.roles.some((role) => staffRoles.includes(role))
) ? 'Staff' : 'User';
staffRoles.includes(action.user.role)
)
? 'Staff'
: 'User';
}
function hasSuspectedWords(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
return actions.some(
action =>
action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD'
);
}
function hasHistoryFlag(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
return actions.some(
action => action.__typename === 'FlagAction' && action.reason === 'TRUST'
);
}
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
const CommentLabels = ({
comment,
comment: { className, status, actions, hasParent },
}) => {
return (
<div className={cn(className, styles.root)}>
<div className={styles.coreLabels}>
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
{hasParent && (
<Label iconName="reply" className={styles.replyLabel}>
reply
</Label>
)}
{status === 'PREMOD' && (
<Label iconName="query_builder" className={styles.premodLabel}>
Pre-Mod
</Label>
)}
{isUserFlagged(actions) && (
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
)}
{hasSuspectedWords(actions) && (
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
)}
{hasHistoryFlag(actions) && (
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
)}
</div>
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
<Slot
className={styles.slot}
fill="adminCommentLabels"
queryData={{ comment }}
/>
</div>
);
};
@@ -4,7 +4,7 @@ import styles from './CountBadge.css';
import t from 'coral-framework/services/i18n';
const CountBadge = ({count}) => {
const CountBadge = ({ count }) => {
let number = count;
// shorten large counts to abbreviations
@@ -16,13 +16,11 @@ const CountBadge = ({count}) => {
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
}
return (
<span className={styles.count}>{number}</span>
);
return <span className={styles.count}>{number}</span>;
};
CountBadge.propTypes = {
count: PropTypes.number.isRequired
count: PropTypes.number.isRequired,
};
export default CountBadge;
+30 -36
View File
@@ -1,67 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Navigation, Drawer} from 'react-mdl';
import {IndexLink, Link} from 'react-router';
import { Navigation, Drawer } from 'react-mdl';
import { IndexLink, Link } from 'react-router';
import styles from './Drawer.css';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import { can } from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({handleLogout, auth = {}}) => (
const CoralDrawer = ({ handleLogout, auth = {} }) => (
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<div>
<Navigation className={styles.nav}>
<IndexLink
className={cn('talk-admin-nav-dashboard', styles.navLink)}
to="/admin/dashboard"
activeClassName={styles.active}>
{t('configure.dashboard')}
</IndexLink>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<Link
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</Link>
)
}
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
</IndexLink>
)}
<Link
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.community')}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') &&
(
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
<a onClick={handleLogout}>Sign Out</a>
<span>{`v${process.env.VERSION}`}</span>
</Navigation>
</div> : null }
</div>
) : null}
</Drawer>
);
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool, // hide app elements from a logged out user
auth: PropTypes.object
auth: PropTypes.object,
};
export default CoralDrawer;
@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Card} from 'coral-ui';
import { Card } from 'coral-ui';
const EmptyCard = (props) => (
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
const EmptyCard = props => (
<Card style={{ textAlign: 'center', maxWidth: 400, margin: '0 auto' }}>
{props.children}
</Card>
);
EmptyCard.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
};
export default EmptyCard;
@@ -1,11 +1,11 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './FullLoading.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
export const FullLoading = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Loading</h1>
<CoralLogo />
</div>
@@ -1,7 +1,7 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
export default ({text, children}) => {
export default ({ text, children }) => {
const hasLinks = !!matchLinks(text);
if (!hasLinks) {
@@ -1,23 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import styles from './LoadMore.css';
import cn from 'classnames';
const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
const LoadMore = ({ loadMore, showLoadMore, className = '', ...rest }) => (
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
{
showLoadMore && <Button
className={styles.loadMore}
onClick={loadMore}>
{showLoadMore && (
<Button className={styles.loadMore} onClick={loadMore}>
Load More
</Button>
}
</div>;
)}
</div>
);
LoadMore.propTypes = {
className: PropTypes.string,
loadMore: PropTypes.func.isRequired,
showLoadMore: PropTypes.bool.isRequired
showLoadMore: PropTypes.bool.isRequired,
};
export default LoadMore;
+5 -3
View File
@@ -1,11 +1,13 @@
import React from 'react';
import {Button, Icon} from 'react-mdl';
import { Button, Icon } from 'react-mdl';
import styles from './Modal.css';
export default ({open, children, onClose}) => (
export default ({ open, children, onClose }) => (
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
<div className={styles.inner}>
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
<Button className={styles.close} onClick={onClose}>
<Icon name="close" />
</Button>
{children}
</div>
</div>
@@ -28,7 +28,7 @@
padding: 15px;
box-sizing: border-box;
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
z-index: 10;
z-index: 4;
.ctaHeader {
font-size: 16px;
@@ -5,52 +5,75 @@ import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
export default class ModerationKeysModal extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired,
queueCount: PropTypes.number.isRequired
}
queueCount: PropTypes.number.isRequired,
};
buildShortcuts = () => {
return [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
j: 'modqueue.next_comment',
k: 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
t: 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
s: 'modqueue.singleview',
'?': 'modqueue.thismenu',
},
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
d: 'modqueue.approve',
f: 'modqueue.reject',
},
},
];
}
};
render () {
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
render() {
const {
open,
onClose,
hideShortcutsNote,
shortcutsNoteVisible,
} = this.props;
return (
<div>
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
<div
className={styles.callToAction}
style={{
display: shortcutsNoteVisible === 'show' ? 'block' : 'none',
}}
>
<div onClick={hideShortcutsNote} className={styles.closeButton}>
×
</div>
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
<p><strong>{t('modqueue.try_these')}:</strong></p>
<p>
<strong>{t('modqueue.try_these')}:</strong>
</p>
<ul>
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
<li>
<span>{t('modqueue.approve')}</span>{' '}
<span className={styles.smallKey}>d</span>
</li>
<li>
<span>{t('modqueue.reject')}</span>{' '}
<span className={styles.smallKey}>f</span>
</li>
</ul>
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
<p>
<span>{t('modqueue.view_more_shortcuts')}</span>{' '}
<span className={styles.smallKey}>{t('modqueue.shift_key')}</span> +{' '}
<span className={styles.smallKey}>/</span>
</p>
</div>
<Modal open={open} onClose={onClose}>
<h3>{t('modqueue.shortcuts')}</h3>
@@ -63,9 +86,11 @@ export default class ModerationKeysModal extends React.Component {
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map((key) => (
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key}tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td className={styles.shortcut}>
<span className={styles.key}>{key}</span>
</td>
<td>{t(shortcut.shortcuts[key])}</td>
</tr>
))}
@@ -1,13 +1,16 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './NotFound.css';
export const NotFound = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Page Not Found</h1>
<p>The communicorn feels your pain.</p>
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
<img
src="https://coralproject.net/images/communicorn.jpg"
alt="Communicorn"
/>
</div>
</Layout>
);
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick, className}) => {
const RejectButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
@@ -28,4 +32,3 @@ RejectButton.propTypes = {
};
export default RejectButton;
@@ -1,25 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import { Dialog } from 'coral-ui';
import { RadioGroup, Radio } from 'react-mdl';
import styles from './SuspendUserDialog.css';
import cn from 'classnames';
import Button from 'coral-ui/components/Button';
import t, {timeago} from 'coral-framework/services/i18n';
import {dateAdd} from 'coral-framework/utils';
import t, { timeago } from 'coral-framework/services/i18n';
import { dateAdd } from 'coral-framework/utils';
const initialState = {step: 0, duration: '3'};
const initialState = { step: 0, duration: '3' };
function durationsToDate(hours) {
// Add 1 minute more to help `timeago.js` to display the correct duration.
return dateAdd(new Date(), 'minute', hours * 60 + 1);
}
class SuspendUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -28,13 +26,13 @@ class SuspendUserDialog extends React.Component {
}
}
handleDurationChange = (event) => {
this.setState({duration: event.target.value});
}
handleDurationChange = event => {
this.setState({ duration: event.target.value });
};
handleMessageChange = (event) => {
this.setState({message: event.target.value});
}
handleMessageChange = event => {
this.setState({ message: event.target.value });
};
goToStep1 = () => {
this.setState({
@@ -43,13 +41,12 @@ class SuspendUserDialog extends React.Component {
'suspenduser.email_message_suspend',
this.props.username,
this.props.organizationName,
timeago(durationsToDate(this.state.duration)),
timeago(durationsToDate(this.state.duration))
),
});
}
};
handlePerform = () => {
this.props.onPerform({
message: this.state.message,
@@ -59,36 +56,49 @@ class SuspendUserDialog extends React.Component {
};
renderStep0() {
const {onCancel, username} = this.props;
const {duration} = this.state;
const { onCancel, username } = this.props;
const { duration } = this.state;
return (
<section>
<h1 className={styles.header}>
{t('suspenduser.title_suspend')}
</h1>
<section className="talk-admin-suspend-user-dialog-step-0">
<h1 className={styles.header}>{t('suspenduser.title_suspend')}</h1>
<p className={styles.description}>
{t('suspenduser.description_suspend', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
<legend className={styles.legend}>
{t('suspenduser.select_duration')}
</legend>
<RadioGroup
name='status filter'
name="status filter"
value={duration}
childContainer='div'
childContainer="div"
onChange={this.handleDurationChange}
className={styles.radioGroup}
>
<Radio value='1'>{t('suspenduser.one_hour')}</Radio>
<Radio value='3'>{t('suspenduser.hours', 3)}</Radio>
<Radio value='24'>{t('suspenduser.hours', 24)}</Radio>
<Radio value='168'>{t('suspenduser.days', 7)}</Radio>
<Radio value="1">{t('suspenduser.one_hour')}</Radio>
<Radio value="3">{t('suspenduser.hours', 3)}</Radio>
<Radio value="24">{t('suspenduser.hours', 24)}</Radio>
<Radio value="168">{t('suspenduser.days', 7)}</Radio>
</RadioGroup>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
<Button
cStyle="black"
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-confirm'
)}
onClick={this.goToStep1}
raised
>
{t('suspenduser.suspend_user')}
</Button>
</div>
@@ -97,31 +107,40 @@ class SuspendUserDialog extends React.Component {
}
renderStep1() {
const {onCancel, username} = this.props;
const {message} = this.state;
const { message } = this.state;
const { onCancel, username } = this.props;
return (
<section>
<h1 className={styles.header}>
{t('suspenduser.title_notify')}
</h1>
<section className="talk-admin-suspend-user-dialog-step-1">
<h1 className={styles.header}>{t('suspenduser.title_notify')}</h1>
<p className={styles.description}>
{t('suspenduser.description_notify', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
<legend className={styles.legend}>
{t('suspenduser.write_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange} />
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button
cStyle="black"
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-send'
)}
onClick={this.handlePerform}
disabled={this.state.message.length === 0}
raised
@@ -134,8 +153,8 @@ class SuspendUserDialog extends React.Component {
}
render() {
const {open, onCancel} = this.props;
const {step} = this.state;
const { open, onCancel } = this.props;
const { step } = this.state;
return (
<Dialog
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
@@ -143,7 +162,13 @@ class SuspendUserDialog extends React.Component {
open={open}
>
<div className={styles.close}>
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
<button
aria-label="Close"
onClick={onCancel}
className={styles.closeButton}
>
×
</button>
</div>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
@@ -5,8 +5,9 @@ import AutosizeInput from 'react-input-autosize';
import PropTypes from 'prop-types';
import cn from 'classnames';
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
const autosizingRenderInput = ({ onChange, value, addTag: _, ...other }) => (
<AutosizeInput type="text" onChange={onChange} value={value} {...other} />
);
autosizingRenderInput.propTypes = {
onChange: PropTypes.func,
@@ -14,17 +15,16 @@ autosizingRenderInput.propTypes = {
addTag: PropTypes.func,
};
const TagsInputComponent = ({className = '', ...props}) => {
const TagsInputComponent = ({ className = '', ...props }) => {
return (
<TagsInput
addOnBlur={true}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
pasteSplit={data => data.split(',').map(d => d.trim())}
className={cn(styles.root, 'tags-input', className)}
focusedClassName={styles.rootFocus}
renderInput={autosizingRenderInput}
{...props}
tagProps={{
className: styles.tag,
classNameRemove: styles.tagRemove,
@@ -207,7 +207,7 @@
.toastify__body {
color: white;
overflow-x: scroll;
overflow-x: auto;
font-size: 15px;
font-weight: 400;
}
@@ -1,6 +1,6 @@
import './ToastContainer.css';
import {defaultProps} from 'recompose';
import {ToastContainer} from 'react-toastify';
import { defaultProps } from 'recompose';
import { ToastContainer } from 'react-toastify';
export default defaultProps({
autoClose: 5000,
@@ -94,73 +94,52 @@
width: calc(100% - 90px);
}
.commentStatuses {
.tabBar {
padding: 0 0 0 10px;
margin: 0;
align-self: center;
list-style: none;
box-sizing: border-box;
li {
display: inline-block;
margin-right: 10px;
cursor: pointer;
padding: 0 10px;
}
border: none;
}
.active {
.tab {
display: inline-block;
margin-right: 10px;
cursor: pointer;
padding: 0 10px;
}
.tabButton {
border: none;
background: white;
border-radius: 0;
font-size: 1em;
padding: 5px;
cursor: pointer;
}
.tabButtonActive {
font-weight: bold;
border-bottom: 3px solid #F36451;
}
.bulkActionGroup {
height: 52px;
background-color: #efefef;
padding: 0 0 0 10px;
display: flex;
i {
margin-right: 0;
}
.bulkAction {
display: inline-block;
width: 48px;
height: 48px;
transform: scale(.7);
min-width: 0;
}
.bulkAction:last-child {
margin-left: -10px;
}
.username {
display: inline-block;
vertical-align: middle;
}
.selectedCommentsInfo {
align-self: center;
font-weight: 500;
margin-left: 15px;
.actionsMenu {
display: inline-block;
}
.loadMore>button {
background-color: #696969;
&:hover {
background-color: #404040;
color: white;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
color: white;
}
.toggleAll {
padding: 0 10px 0 0;
align-self: center;
.actionsMenuBanned {
background-color: #E45241;
border-color: #E45241;
color: white;
}
.commentList {
clear: both;
}
.bulkActionHeader {
display: flex;
justify-content: space-between;
height: 52px;
&.selected {
background-color: #efefef;
}
}
+262 -126
View File
@@ -1,94 +1,88 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import Comment from '../containers/UserDetailComment';
import capitalize from 'lodash/capitalize';
import { getErrorMessages } from 'coral-framework/utils';
import styles from './UserDetail.css';
import {Icon, Drawer, Spinner} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import AccountHistory from './AccountHistory';
import { Slot } from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import {
getReliability,
isSuspended,
isBanned,
} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import LoadMore from '../components/LoadMore';
import cn from 'classnames';
import capitalize from 'lodash/capitalize';
import {getReliability} from 'coral-framework/utils/user';
import ApproveButton from './ApproveButton';
import RejectButton from './RejectButton';
import {getErrorMessages} from 'coral-framework/utils';
import {
Icon,
Drawer,
Spinner,
TabBar,
Tab,
TabContent,
TabPane,
} from 'coral-ui';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
export default class UserDetail extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeStatus: PropTypes.func.isRequired,
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
notify: PropTypes.func.isRequired
}
rejectThenReload = async (info) => {
class UserDetail extends React.Component {
rejectThenReload = async info => {
try {
await this.props.rejectComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
acceptThenReload = async (info) => {
acceptThenReload = async info => {
try {
await this.props.acceptComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkAcceptThenReload = async () => {
try {
await this.props.bulkAccept();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkRejectThenReload = async () => {
try {
await this.props.bulkReject();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
showAll = () => {
this.props.changeStatus('all');
}
changeTab = tab => {
this.props.changeTab(tab);
};
showRejected = () => {
this.props.changeStatus('rejected');
}
showSuspenUserDialog = () =>
this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () =>
this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
renderLoading() {
return (
@@ -100,36 +94,108 @@ export default class UserDetail extends React.Component {
);
}
getActionMenuLabel() {
const { root: { user } } = this.props;
if (isBanned(user)) {
return 'Banned';
} else if (isSuspended(user)) {
return 'Suspended';
}
return '';
}
renderLoaded() {
const {
data,
root,
root: {
user,
totalComments,
rejectedComments,
comments: {nodes, hasNextPage}
},
root: { me, user, totalComments, rejectedComments },
activeTab,
selectedCommentIds,
toggleSelect,
hideUserDetail,
viewUserDetail,
loadMore,
toggleSelectAll
toggleSelectAll,
unbanUser,
unsuspendUser,
modal,
} = this.props;
let rejectedPercent = (rejectedComments / totalComments) * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
// if totalComments is 0, you're dividing by zero
let rejectedPercent = rejectedComments / totalComments * 100;
// if totalComments is 0, you're dividing by zero, which is naughty
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
rejectedPercent = 0;
}
const banned = isBanned(user);
const suspended = isSuspended(user);
return (
<ClickOutside onClickOutside={hideUserDetail}>
<Drawer onClose={hideUserDetail}>
<h3>{user.username}</h3>
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer
className="talk-admin-user-detail-drawer"
onClose={hideUserDetail}
>
<h3
className={cn(styles.username, 'talk-admin-user-detail-username')}
>
{user.username}
</h3>
{user.id && (
<ActionsMenu
icon="person"
className={cn(
styles.actionsMenu,
'talk-admin-user-detail-actions-menu'
)}
buttonClassNames={cn(
{
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel()}
>
{suspended ? (
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
Remove Suspension
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}
>
Suspend User
</ActionsMenuItem>
)}
{banned ? (
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
Remove Ban
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}
>
Ban User
</ActionsMenuItem>
)}
</ActionsMenu>
)}
{(banned || suspended) && (
<UserInfoTooltip
user={user}
banned={banned}
suspended={suspended}
/>
)}
<div>
<ul className={styles.userDetailList}>
@@ -139,13 +205,18 @@ export default class UserDetail extends React.Component {
{new Date(user.created_at).toLocaleString()}
</li>
{user.profiles.map(({id}) =>
{user.profiles.map(({ id }) => (
<li key={id}>
<Icon name="email" />
<span className={styles.userDetailItem}>Email:</span>
{id} <ButtonCopyToClipboard className={styles.copyButton} icon="content_copy" copyText={id} />
{id}{' '}
<ButtonCopyToClipboard
className={styles.copyButton}
icon="content_copy"
copyText={id}
/>
</li>
)}
))}
</ul>
<ul className={styles.stats}>
@@ -161,7 +232,12 @@ export default class UserDetail extends React.Component {
</li>
<li className={styles.stat}>
<span className={styles.statItem}>Reports</span>
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
<span
className={cn(
styles.statReportResult,
styles[getReliability(user.reliable.flagger)]
)}
>
{capitalize(getReliability(user.reliable.flagger))}
</span>
</li>
@@ -171,67 +247,98 @@ export default class UserDetail extends React.Component {
<Slot
fill="userProfile"
data={this.props.data}
queryData={{root, user}}
queryData={{ root, user }}
/>
<hr />
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
{
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}>
<ApproveButton
onClick={this.bulkAcceptThenReload}
minimal
/>
<RejectButton
onClick={this.bulkRejectThenReload}
minimal
/>
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
</div>
)
}
<div className={styles.toggleAll}>
<input
type='checkbox'
id='toogleAll'
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
onChange={(e) => {
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
}} />
<label htmlFor='toogleAll'>Select all</label>
</div>
</div>
<div className={styles.commentList}>
{
nodes.map((comment) => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>;
})
}
</div>
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
<TabBar
onTabClick={this.changeTab}
activeTab={activeTab}
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
aria-controls="talk-admin-user-detail-content"
tabClassNames={{
button: styles.tabButton,
buttonActive: styles.tabButtonActive,
}}
>
<Tab
tabId={'all'}
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-all-tab'
)}
>
All
</Tab>
<Tab
tabId={'rejected'}
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
>
Rejected
</Tab>
<Tab
tabId={'history'}
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-history-tab'
)}
>
Account History
</Tab>
</TabBar>
<TabContent
activeTab={activeTab}
className="talk-admin-user-detail-content"
>
<TabPane
tabId={'all'}
className={'talk-admin-user-detail-all-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selectedCommentIds={selectedCommentIds}
toggleSelectAll={toggleSelectAll}
bulkAcceptThenReload={this.bulkAcceptThenReload}
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane
tabId={'rejected'}
className={'talk-admin-user-detail-rejected-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selectedCommentIds={selectedCommentIds}
toggleSelectAll={toggleSelectAll}
bulkAcceptThenReload={this.bulkAcceptThenReload}
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane
tabId={'history'}
className={'talk-admin-user-detail-history-tab-pane'}
>
<AccountHistory user={user} />
</TabPane>
</TabContent>
</Drawer>
</ClickOutside>
);
@@ -244,3 +351,32 @@ export default class UserDetail extends React.Component {
return this.renderLoaded();
}
}
UserDetail.propTypes = {
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeTab: PropTypes.func.isRequired,
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
notify: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
modal: PropTypes.bool,
};
export default UserDetail;
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import { Link } from 'react-router';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
@@ -13,19 +13,18 @@ import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
import t, { timeago } from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () =>
this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({ commentId: this.props.comment.id });
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
reject = () =>
this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({ commentId: this.props.comment.id });
render() {
const {
@@ -34,30 +33,35 @@ class UserDetailComment extends React.Component {
toggleSelect,
className,
data,
root: {settings: {wordlist: {banned, suspect}}},
root: { settings: { wordlist: { banned, suspect } } },
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
})}
>
<div className={styles.container}>
<div className={styles.header}>
<input
className={styles.bulkSelectInput}
type='checkbox'
type="checkbox"
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
onChange={e => toggleSelect(e.target.value, e.target.checked)}
/>
<span className={styles.created}>
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{comment.editing && comment.editing.edited ? (
<span>
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
</span>
) : null}
<div className={styles.labels}>
<CommentLabels comment={comment} />
@@ -65,7 +69,11 @@ class UserDetailComment extends React.Component {
</div>
<div className={styles.story}>
Story: {comment.asset.title}
{<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
{
<Link to={`/admin/moderate/${comment.asset.id}`}>
{t('modqueue.moderate')}
</Link>
}
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
@@ -74,8 +82,7 @@ class UserDetailComment extends React.Component {
suspectWords={suspect}
bannedWords={banned}
body={comment.body}
/>
{' '}
/>{' '}
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -106,17 +113,15 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails
data={data}
root={root}
comment={comment}
/>
<CommentDetails data={data} root={root} comment={comment} />
</li>
);
}
}
UserDetailComment.propTypes = {
selected: PropTypes.bool,
data: PropTypes.object,
user: PropTypes.object.isRequired,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
@@ -140,7 +145,7 @@ UserDetailComment.propTypes = {
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
id: PropTypes.string,
}),
}),
};
@@ -0,0 +1,56 @@
.commentList {
clear: both;
}
.loadMore > button {
background-color: #696969;
&:hover {
background-color: #404040;
color: white;
}
}
.bulkActionHeader {
display: flex;
justify-content: space-between;
height: 52px;
justify-content: flex-end;
&.selected {
background-color: #efefef;
}
}
.bulkActionGroup {
height: 52px;
background-color: #efefef;
padding: 0 0 0 10px;
display: flex;
flex: 1;
i {
margin-right: 0;
}
.bulkAction {
display: inline-block;
width: 48px;
height: 48px;
transform: scale(.7);
min-width: 0;
}
.bulkAction:last-child {
margin-left: -10px;
}
}
.selectedCommentsInfo {
align-self: center;
font-weight: 500;
margin-left: 15px;
}
.toggleAll {
padding-right: 14px;
align-self: center;
}
@@ -0,0 +1,106 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './UserDetailCommentList.css';
import LoadMore from '../components/LoadMore';
import Comment from '../containers/UserDetailComment';
import RejectButton from './RejectButton';
import ApproveButton from './ApproveButton';
const UserDetailCommentList = props => {
const {
data,
root,
root: { user, comments: { nodes, hasNextPage } },
acceptComment,
rejectComment,
selectedCommentIds,
toggleSelect,
viewUserDetail,
loadMore,
toggleSelectAll,
bulkAcceptThenReload,
bulkRejectThenReload,
} = props;
return (
<div
className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}
>
<div
className={
selectedCommentIds.length > 0
? cn(styles.bulkActionHeader, styles.selected)
: styles.bulkActionHeader
}
>
{selectedCommentIds.length > 0 && (
<div className={styles.bulkActionGroup}>
<ApproveButton onClick={bulkAcceptThenReload} minimal />
<RejectButton onClick={bulkRejectThenReload} minimal />
<span className={styles.selectedCommentsInfo}>
{' '}
{selectedCommentIds.length} comments selected
</span>
</div>
)}
<div className={styles.toggleAll}>
<input
type="checkbox"
id="toogleAll"
checked={
selectedCommentIds.length > 0 &&
selectedCommentIds.length === nodes.length
}
onChange={e => {
toggleSelectAll(
nodes.map(comment => comment.id),
e.target.checked
);
}}
/>
<label htmlFor="toogleAll">Select all</label>
</div>
</div>
{nodes.map(comment => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return (
<Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
acceptComment={acceptComment}
rejectComment={rejectComment}
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>
);
})}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
</div>
);
};
UserDetailCommentList.propTypes = {
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
toggleSelect: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
bulkAcceptThenReload: PropTypes.func.isRequired,
bulkRejectThenReload: PropTypes.func.isRequired,
};
export default UserDetailCommentList;
@@ -0,0 +1,69 @@
.userInfo {
position: relative;
display: inline-block;
}
.icon {
font-size: 16px;
color: #616161;
-ms-user-select:none;
-moz-user-select: none;
-webkit-user-select: none;
-webkit-touch-callout:none;
user-select: none;
-webkit-tap-highlight-color:rgba(0,0,0,0);
}
.icon:hover {
cursor: pointer;
}
.menu {
background-color: white;
border: solid 1px #999;
border-radius: 3px;
padding: 10px;
position: absolute;
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
z-index: 10;
top: 32px;
right: 0px;
width: 140px;
text-align: left;
color: #616161;
}
.menu::before{
content: '';
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 0px;
top: -20px;
transform: rotate(180deg);
}
.menu::after{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 0px;
top: -19px;
transform: rotate(180deg);
}
.descriptionList {
padding: 0;
margin: 0;
list-style: none;
}
.strongItem {
margin-right: 3px;
}
.descriptionItem {
font-size: 0.9em;
}
@@ -0,0 +1,177 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { Icon } from 'coral-ui';
import styles from './UserInfoTooltip.css';
import ClickOutside from 'coral-framework/components/ClickOutside';
import moment from 'moment';
const initialState = { menuVisible: false };
class UserInfoTooltip extends React.Component {
state = initialState;
toogleMenu = () => {
this.setState({ menuVisible: !this.state.menuVisible });
};
hideMenu = () => {
this.setState({ menuVisible: false });
};
getLastHistoryItem = (user, status = 'banned') => {
const userHistory = user.state.status[status].history;
return userHistory[userHistory.length - 1];
};
render() {
const { menuVisible } = this.state;
const { user, banned, suspended } = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.userInfo, 'talk-admin-user-info-tooltip')}>
<span
onClick={this.toogleMenu}
className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}
>
<Icon name="info_outline" />
</span>
{menuVisible && (
<div
className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}
>
{banned && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-banned'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Banned On</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'banned').assigned_by
.username
}
</span>
</li>
</ul>
</div>
)}
{suspended && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-suspended'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Suspension</strong>
<span />
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'suspension')
.assigned_by.username
}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Start</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(
user,
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>End</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
</div>
)}
</div>
)}
</div>
</ClickOutside>
);
}
}
UserInfoTooltip.propTypes = {
user: PropTypes.object,
banned: PropTypes.bool,
suspended: PropTypes.bool,
};
export default UserInfoTooltip;
+74 -67
View File
@@ -1,105 +1,112 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import { Navigation, Header, IconButton, MenuItem, Menu } from 'react-mdl';
import { Link, IndexLink } from 'react-router';
import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import {Logo} from './Logo';
import {can} from 'coral-framework/services/perms';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import Indicator from './Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
root
root,
}) => {
return (
<div className={styles.headerWrapper}>
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
<div>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id='dashboardNav'
className={cn('talk-admin-nav-dashboard', styles.navLink)}
to="/admin/dashboard"
activeClassName={styles.active}>
{t('configure.dashboard')}
id="moderateNav"
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && (
<Indicator />
)}
</IndexLink>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<Link
id='moderateNav'
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
</Link>
)
}
<Link
id='storiesNav'
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
)}
<Link
id="storiesNav"
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
id='communityNav'
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
<Link
id="communityNav"
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}
>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') && (
<Link
id='configureNav'
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
</Navigation>
:
null
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
id="configureNav"
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
</Navigation>
) : null}
<div className={styles.rightPanel}>
<ul>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<IconButton
name="settings"
id="menu-settings"
className="talk-admin-header-settings-button"
/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem onClick={() => showShortcuts(true)}>
{t('configure.shortcuts')}
</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
View latest version
<a
href="https://github.com/coralproject/talk/releases"
target="_blank"
rel="noopener noreferrer"
>
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
<a
href="https://support.coralproject.net"
target="_blank"
rel="noopener noreferrer"
>
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
<li>{`v${process.env.VERSION}`}</li>
</ul>
</div>
</div>
@@ -112,7 +119,7 @@ CoralHeader.propTypes = {
auth: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired
root: PropTypes.object.isRequired,
};
export default CoralHeader;
@@ -1,7 +1,6 @@
import React from 'react';
import styles from './Indicator.css';
const Indicator = () =>
<span className={styles.indicator}></span>;
const Indicator = () => <span className={styles.indicator} />;
export default Indicator;
+4 -10
View File
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Layout as LayoutMDL} from 'react-mdl';
import { Layout as LayoutMDL } from 'react-mdl';
import Header from '../../containers/Header';
import Drawer from '../Drawer';
import styles from './Layout.css';
@@ -18,14 +18,8 @@ const Layout = ({
showShortcuts={toggleShortcutModal}
auth={auth}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
auth={auth}
/>
<div className={styles.layout}>
{children}
</div>
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
<div className={styles.layout}>{children}</div>
</LayoutMDL>
);
@@ -34,7 +28,7 @@ Layout.propTypes = {
auth: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
restricted: PropTypes.bool, // hide elements from a user that's logged out
};
export default Layout;
+3 -3
View File
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './Logo.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
import PropTypes from 'prop-types';
export const Logo = ({className = ''}) => (
export const Logo = ({ className = '' }) => (
<div className={`${styles.logo} ${className}`}>
<h1>
<CoralLogo className={styles.base} />
@@ -13,5 +13,5 @@ export const Logo = ({className = ''}) => (
);
Logo.propTypes = {
className: PropTypes.string
className: PropTypes.string,
};
+2 -1
View File
@@ -1 +1,2 @@
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS =
'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
+4 -2
View File
@@ -2,8 +2,10 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST =
'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS =
'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
@@ -6,8 +6,6 @@ export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`;
export const SORT_UPDATE = `${prefix}_SORT_UPDATE`;
export const SET_PAGE = `${prefix}_SET_PAGE`;
export const SET_ROLE = `${prefix}_SET_ROLE`;
export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`;
export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`;
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`;
+2 -1
View File
@@ -10,7 +10,8 @@ export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS =
'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
@@ -6,3 +6,4 @@ export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
export const MODERATION_SELECT_COMMENT = 'MODERATION_SELECT_COMMENT';
@@ -1,6 +1,6 @@
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
export const CHANGE_TAB_USER_DETAIL = 'CHANGE_TAB_USER_DETAIL';
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
@@ -1,29 +1,39 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import BanUserDialog from '../components/BanUserDialog';
import {hideBanUserDialog} from '../actions/banUserDialog';
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {compose} from 'react-apollo';
import { hideBanUserDialog } from '../actions/banUserDialog';
import {
withBanUser,
withSetCommentStatus,
} from 'coral-framework/graphql/mutations';
import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
import {getErrorMessages} from 'coral-framework/utils';
import {notify} from 'coral-framework/actions/notification';
import { getErrorMessages } from 'coral-framework/utils';
import { notify } from 'coral-framework/actions/notification';
class BanUserDialogContainer extends Component {
banUser = async () => {
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog, notify} = this.props;
const {
userId,
commentId,
commentStatus,
banUser,
setCommentStatus,
hideBanUserDialog,
notify,
} = this.props;
try {
await setUserStatus({userId, status: 'BANNED'});
await banUser({ id: userId, message: '' });
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
await setCommentStatus({ commentId, status: 'REJECTED' });
}
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
}
};
getInfo() {
let note = t('bandialog.note_ban_user');
@@ -46,7 +56,17 @@ class BanUserDialogContainer extends Component {
}
}
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
BanUserDialogContainer.propTypes = {
banUser: PropTypes.func.isRequired,
hideBanUserDialog: PropTypes.func,
open: PropTypes.bool,
username: PropTypes.string,
commentStatus: PropTypes.string,
};
const mapStateToProps = ({
banUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -54,18 +74,18 @@ const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, com
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideBanUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideBanUserDialog,
notify,
},
dispatch
),
});
export default compose(
withSetUserStatus,
withBanUser,
withSetCommentStatus,
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps)
)(BanUserDialogContainer);
@@ -1,12 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentDetails from '../components/CommentDetails';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import withFragments from 'coral-framework/hocs/withFragments';
const slots = [
'adminCommentDetailArea',
'adminCommentMoreDetails',
];
const slots = ['adminCommentDetailArea', 'adminCommentMoreDetails'];
export default withFragments({
root: gql`
@@ -20,5 +17,5 @@ export default withFragments({
__typename
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentDetails);
@@ -1,11 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentLabels from '../components/CommentLabels';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
const slots = [
'adminCommentLabels',
];
const slots = ['adminCommentLabels'];
export default withFragments({
root: gql`
@@ -25,10 +23,10 @@ export default withFragments({
}
user {
id
roles
role
}
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentLabels);
+22 -20
View File
@@ -1,24 +1,26 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/ui/Header';
export default withQuery(gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: {
statuses: [PREMOD]
})
reportedCount: commentCount(query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
action_type: FLAG
})
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
statuses: [PENDING]
})
}
`, {
options: {
pollInterval: 5000
export default withQuery(
gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: { statuses: [PREMOD] })
reportedCount: commentCount(
query: { statuses: [NONE, PREMOD, SYSTEM_WITHHELD], action_type: FLAG }
)
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
}
})(Header);
`,
{
options: {
pollInterval: 10000,
},
}
)(Header);
+35 -24
View File
@@ -1,35 +1,39 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/ui/Layout';
import {fetchConfig} from '../actions/config';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import {FullLoading} from '../components/FullLoading';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
import {
checkLogin,
handleLogin,
requestPasswordReset,
logout,
} from '../actions/auth';
import { can } from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
import PropTypes from 'prop-types';
class LayoutContainer extends React.Component {
componentWillMount() {
const {checkLogin, fetchConfig} = this.props;
const { checkLogin, fetchConfig } = this.props;
checkLogin();
fetchConfig();
}
render() {
const {
user,
loggedIn,
loadingUser,
loginError,
loginMaxExceeded,
passwordRequestSuccess
passwordRequestSuccess,
} = this.props.auth;
const {
@@ -61,7 +65,8 @@ class LayoutContainer extends React.Component {
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
auth={this.props.auth} >
auth={this.props.auth}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
@@ -71,7 +76,10 @@ class LayoutContainer extends React.Component {
} else if (loggedIn) {
return (
<Layout {...this.props}>
<p>This page is for team use only. Please contact an administrator if you want to join this team.</p>
<p>
This page is for team use only. Please contact an administrator if
you want to join this team.
</p>
</Layout>
);
}
@@ -89,22 +97,25 @@ LayoutContainer.propTypes = {
toggleShortcutModal: PropTypes.func,
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
checkLogin: PropTypes.func,
fetchConfig: PropTypes.func
fetchConfig: PropTypes.func,
};
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
auth: state.auth,
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -1,33 +1,43 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
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 t, {timeago} from 'coral-framework/services/i18n';
import { hideSuspendUserDialog } from '../actions/suspendUserDialog';
import {
withSetCommentStatus,
withSuspendUser,
} from 'coral-framework/graphql/mutations';
import { compose, gql } from 'react-apollo';
import t, { timeago } from 'coral-framework/services/i18n';
import withQuery from 'coral-framework/hocs/withQuery';
import {getErrorMessages} from 'coral-framework/utils';
import { getErrorMessages } from 'coral-framework/utils';
import get from 'lodash/get';
import {notify} from 'coral-framework/actions/notification';
import { notify } from 'coral-framework/actions/notification';
class SuspendUserDialogContainer extends Component {
suspendUser = async ({message, until}) => {
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
suspendUser = async ({ message, until }) => {
const {
userId,
username,
commentStatus,
commentId,
hideSuspendUserDialog,
setCommentStatus,
suspendUser,
notify,
} = this.props;
hideSuspendUserDialog();
try {
await suspendUser({id: userId, message, until});
await suspendUser({ id: userId, message, until });
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until)),
t('suspenduser.notify_suspend_until', username, timeago(until))
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
await setCommentStatus({ commentId, status: 'REJECTED' });
}
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
};
@@ -53,14 +63,16 @@ SuspendUserDialogContainer.propTypes = {
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
settings {
organizationName
__typename
settings {
organizationName
}
}
`);
const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
const mapStateToProps = ({
suspendUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -68,19 +80,19 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideSuspendUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideSuspendUserDialog,
notify,
},
dispatch
),
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps),
withSuspendUser,
withSetCommentStatus,
withOrganizationName,
withOrganizationName
)(SuspendUserDialogContainer);
+164 -76
View File
@@ -1,25 +1,35 @@
import React from 'react';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import PropTypes from 'prop-types';
import { compose, gql } from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
getDefinitionName,
getSlotFragmentSpreads,
} from 'coral-framework/utils';
import {
viewUserDetail,
hideUserDetail,
changeUserDetailStatuses,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
toggleSelectAllCommentInUserDetail
toggleSelectAllCommentInUserDetail,
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import {notify} from 'coral-framework/actions/notification';
import { notify } from 'coral-framework/actions/notification';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
nodes {
...${getDefinitionName(UserDetailComment.fragments.comment)}
}
@@ -30,38 +40,36 @@ const commentConnectionFragment = gql`
${UserDetailComment.fragments.comment}
`;
const slots = [
'userProfile',
];
const slots = ['userProfile'];
class UserDetailContainer extends React.Component {
isLoadingMore = false;
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = async (status) => {
const changes = this.props.selectedCommentIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
bulkSetCommentStatus = async status => {
const changes = this.props.selectedCommentIds.map(commentId => {
return this.props.setCommentStatus({ commentId, status });
});
await Promise.all(changes);
this.props.clearUserDetailSelections(); // un-select everything
}
};
bulkReject = () => {
return this.bulkSetCommentStatus('REJECTED');
}
};
bulkAccept = () => {
return this.bulkSetCommentStatus('ACCEPTED');
}
};
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
acceptComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'ACCEPTED' });
};
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
rejectComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'REJECTED' });
};
loadMore = () => {
if (this.isLoadingMore) {
@@ -75,24 +83,25 @@ class UserDetailContainer extends React.Component {
author_id: this.props.data.variables.author_id,
statuses: this.props.data.variables.statuses,
};
this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
return update(prev, {
comments: {
nodes: {$push: comments.nodes},
hasNextPage: {$set: comments.hasNextPage},
startCursor: {$set: comments.startCursor},
endCursor: {$set: comments.endCursor},
},
});
}
})
this.props.data
.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, { fetchMoreResult: { comments } }) => {
return update(prev, {
comments: {
nodes: { $push: comments.nodes },
hasNextPage: { $set: comments.hasNextPage },
startCursor: { $set: comments.startCursor },
endCursor: { $set: comments.endCursor },
},
});
},
})
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
.catch(err => {
this.isLoadingMore = false;
throw err;
});
@@ -104,37 +113,66 @@ class UserDetailContainer extends React.Component {
}
}
render () {
render() {
if (!this.props.userId) {
return null;
}
const loading = this.props.data.loading;
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeStatus={this.props.changeUserDetailStatuses}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props} />;
return (
<UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeTab={this.props.changeTab}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props}
/>
);
}
}
UserDetailContainer.propTypes = {
changeTab: PropTypes.func,
toggleSelectCommentInUserDetail: PropTypes.func,
toggleSelectAllCommentInUserDetail: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
setCommentStatus: PropTypes.func,
clearUserDetailSelections: PropTypes.func,
selectedCommentIds: PropTypes.array,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
};
const LOAD_MORE_QUERY = gql`
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
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_UserDetail_CommentConnection
}
}
${commentConnectionFragment}
`;
export const withUserDetailQuery = withQuery(gql`
export const withUserDetailQuery = withQuery(
gql`
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
id
@@ -147,52 +185,102 @@ export const withUserDetailQuery = withQuery(gql`
reliable {
flagger
}
state {
status {
suspension {
until
history {
until
created_at
assigned_by {
id
username
}
}
}
banned {
status
history {
status
assigned_by {
id
username
}
created_at
}
}
username {
status
history {
status
assigned_by {
id
username
}
created_at
}
}
}
}
${getSlotFragmentSpreads(slots, 'user')}
}
me {
id
}
totalComments: commentCount(query: {author_id: $author_id, statuses: []})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
comments: comments(query: {
author_id: $author_id,
statuses: $statuses
}) {
...CoralAdmin_Moderation_CommentConnection
...CoralAdmin_UserDetail_CommentConnection
}
...${getDefinitionName(UserDetailComment.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
}
${UserDetailComment.fragments.root}
${commentConnectionFragment}
`, {
options: ({userId, statuses}) => {
return {
variables: {author_id: userId, statuses},
fetchPolicy: 'network-only',
};
},
skip: (ownProps) => !ownProps.userId,
});
`,
{
options: ({ userId, statuses }) => {
return {
variables: { author_id: userId, statuses },
fetchPolicy: 'network-only',
};
},
skip: ownProps => !ownProps.userId,
}
);
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
userId: state.userDetail.userId,
selectedCommentIds: state.userDetail.selectedCommentIds,
statuses: state.userDetail.statuses,
activeTab: state.userDetail.activeTab,
modal: state.ui.modal,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify
}, dispatch)
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
showBanUserDialog,
showSuspendUserDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify,
},
dispatch
),
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUserDetailQuery,
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser
)(UserDetailContainer);
@@ -1,7 +1,7 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import UserDetailComment from '../components/UserDetailComment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getDefinitionName} from 'coral-framework/utils';
import { getDefinitionName } from 'coral-framework/utils';
import CommentLabels from './CommentLabels';
import CommentDetails from './CommentDetails';
@@ -35,10 +35,13 @@ export default withFragments({
editing {
edited
}
status_history {
type
}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
`,
})(UserDetailComment);
+174 -27
View File
@@ -1,55 +1,202 @@
import update from 'immutability-helper';
import mapValues from 'lodash/mapValues';
import { mapLeaves } from 'coral-framework/utils';
import { gql } from 'react-apollo';
// Map nested object leaves. Array objects are considered leaves.
function mapLeaves(o, mapper) {
return mapValues(o, (val) => {
if (typeof val === 'object' && !Array.isArray(val)) {
return mapLeaves(val, mapper);
const userStatusFragment = gql`
fragment Talk_UpdateUserStatus on User {
state {
status {
banned {
status
}
suspension {
until
}
}
}
return mapper(val);
});
}
}
`;
const userRoleFragment = gql`
fragment Talk_UpdateUserRole on User {
role
}
`;
export default {
mutations: {
SetUserStatus: ({variables: {status, userId}}) => ({
SetUserRole: ({ variables: { id, role } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userRoleFragment,
id: fragmentId,
});
const updated = update(data, {
role: {
$set: role,
},
});
proxy.writeFragment({
fragment: userRoleFragment,
id: fragmentId,
data: updated,
});
},
}),
SuspendUser: ({ variables: { input: { id, until } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
suspension: {
until: { $set: until },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
UnsuspendUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
suspension: {
until: { $set: null },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
BanUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
banned: {
status: { $set: true },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
UnbanUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
banned: {
status: { $set: false },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
SetUserBanStatus: ({ variables: { status, id } }) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
if (status !== 'APPROVED') {
TalkAdmin_Community: prev => {
if (!status) {
return prev;
}
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
});
return updated;
}
}
},
},
}),
RejectUsername: ({variables: {input: {id: userId}}}) => ({
ApproveUsername: ({ variables: { id } }) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
TalkAdmin_Community: prev => {
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
flaggedUsers: {
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
});
return updated;
}
}
},
},
}),
UpdateSettings: ({variables: {input}}) => ({
RejectUsername: ({ variables: { id: userId } }) => ({
updateQueries: {
TalkAdmin_Configure: (prev) => {
TalkAdmin_Community: prev => {
const updated = update(prev, {
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
flaggedUsers: {
nodes: {
$apply: nodes => nodes.filter(node => node.id !== userId),
},
},
});
return updated;
}
}
},
},
}),
UpdateSettings: ({ variables: { input } }) => ({
updateQueries: {
TalkAdmin_Configure: prev => {
const updated = update(prev, {
settings: mapLeaves(input, leaf => ({ $set: leaf })),
});
return updated;
},
},
}),
},
};
+15 -9
View File
@@ -1,20 +1,20 @@
import React from 'react';
import {render} from 'react-dom';
import { render } from 'react-dom';
import smoothscroll from 'smoothscroll-polyfill';
import TalkProvider from 'coral-framework/components/TalkProvider';
import {createContext} from 'coral-framework/services/bootstrap';
import { createContext } from 'coral-framework/services/bootstrap';
import reducers from './reducers';
import App from './components/App';
import 'react-mdl/extra/material.js';
import graphqlExtension from './graphql';
import pluginsConfig from 'pluginsConfig';
import {toast} from 'react-toastify';
import {createNotificationService} from './services/notification';
import {hideShortcutsNote} from './actions/moderation';
import { toast } from 'react-toastify';
import { createNotificationService } from './services/notification';
import { hideShortcutsNote } from './actions/moderation';
smoothscroll.polyfill();
function init({store, storage}) {
function init({ store, storage }) {
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
store.dispatch(hideShortcutsNote());
}
@@ -22,13 +22,19 @@ function init({store, storage}) {
async function main() {
const notification = createNotificationService(toast);
const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init});
const context = await createContext({
reducers,
graphqlExtension,
pluginsConfig,
notification,
init,
});
render(
<TalkProvider {...context}>
<App />
</TalkProvider>
, document.querySelector('#root')
</TalkProvider>,
document.querySelector('#root')
);
}
+53 -52
View File
@@ -5,60 +5,61 @@ const initialState = {
user: null,
loginError: null,
loginMaxExceeded: false,
passwordRequestSuccess: null
passwordRequestSuccess: null,
};
export default function auth (state = initialState, action) {
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_REQUEST:
return {
...state,
loadingUser: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
loggedIn: false,
loadingUser: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
loggedIn: true,
loadingUser: false,
user: action.user,
};
case actions.LOGOUT:
return initialState;
case actions.LOGIN_SUCCESS:
return {
...state,
loginMaxExceeded: false,
loginError: null,
};
case actions.LOGIN_FAILURE:
return {
...state,
loginError: action.message,
};
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return {
...state,
passwordRequestSuccess: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestSuccess: 'If you have a registered account, a password reset link was sent to that email.',
};
case actions.LOGIN_MAXIMUM_EXCEEDED:
return {
...state,
loginMaxExceeded: true,
loginError: action.message,
};
default :
return state;
case actions.CHECK_LOGIN_REQUEST:
return {
...state,
loadingUser: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
loggedIn: false,
loadingUser: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
loggedIn: true,
loadingUser: false,
user: action.user,
};
case actions.LOGOUT:
return initialState;
case actions.LOGIN_SUCCESS:
return {
...state,
loginMaxExceeded: false,
loginError: null,
};
case actions.LOGIN_FAILURE:
return {
...state,
loginError: action.message,
};
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return {
...state,
passwordRequestSuccess: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestSuccess:
'If you have a registered account, a password reset link was sent to that email.',
};
case actions.LOGIN_MAXIMUM_EXCEEDED:
return {
...state,
loginMaxExceeded: true,
loginError: action.message,
};
default:
return state;
}
}
@@ -1,4 +1,7 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
const initialState = {
open: false,
@@ -10,21 +13,21 @@ const initialState = {
export default function banUserDialog(state = initialState, action) {
switch (action.type) {
case SHOW_BAN_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
case SHOW_BAN_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
}
}
+67 -90
View File
@@ -5,12 +5,10 @@ import {
SORT_UPDATE,
SET_PAGE,
SET_SEARCH_VALUE,
SET_ROLE,
SET_COMMENTER_STATUS,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG
HIDE_REJECT_USERNAME_DIALOG,
} from '../constants/community';
const initialState = {
@@ -25,96 +23,75 @@ const initialState = {
pagePeople: 0,
user: {},
banDialog: false,
rejectUsernameDialog: false
rejectUsernameDialog: false,
};
export default function community (state = initialState, action) {
export default function community(state = initialState, action) {
switch (action.type) {
case FETCH_USERS_REQUEST :
return {
...state,
isFetchingPeople: true,
};
case FETCH_USERS_FAILURE :
return {
...state,
isFetchingPeople: false,
errorPeople: action.error,
};
case FETCH_USERS_SUCCESS : {
case FETCH_USERS_REQUEST:
return {
...state,
isFetchingPeople: true,
};
case FETCH_USERS_FAILURE:
return {
...state,
isFetchingPeople: false,
errorPeople: action.error,
};
case FETCH_USERS_SUCCESS: {
const {users, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
return {
...state,
isFetchingPeople: false,
errorPeople: '',
pagePeople: page,
countPeople: count,
limitPeople: limit,
totalPagesPeople: totalPages,
...rest,
users, // Sets to normal array
};
}
case SET_PAGE:
return {
...state,
pagePeople: action.page,
};
case SET_ROLE : {
const commenters = state.users;
const idx = commenters.findIndex((el) => el.id === action.id);
commenters[idx].roles[0] = action.role;
return {
...state,
users: commenters.map((id) => id),
};
}
case SET_COMMENTER_STATUS: {
const commenters = state.users;
const idx = commenters.findIndex((el) => el.id === action.id);
commenters[idx].status = action.status;
return {
...state,
users: commenters.map((id) => id),
};
}
case SORT_UPDATE :
return {
...state,
fieldPeople: action.sort.field,
ascPeople: !state.ascPeople,
};
case HIDE_BANUSER_DIALOG:
return {
...state,
banDialog: false,
};
case SHOW_BANUSER_DIALOG:
return {
...state,
user: action.user,
banDialog: true,
};
case HIDE_REJECT_USERNAME_DIALOG:
return {
...state,
rejectUsernameDialog: false,
};
case SHOW_REJECT_USERNAME_DIALOG:
return {
...state,
user: action.user,
rejectUsernameDialog: true
};
case SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
default :
return state;
return {
...state,
isFetchingPeople: false,
errorPeople: '',
pagePeople: page,
countPeople: count,
limitPeople: limit,
totalPagesPeople: totalPages,
...rest,
users, // Sets to normal array
};
}
case SET_PAGE:
return {
...state,
pagePeople: action.page,
};
case SORT_UPDATE:
return {
...state,
fieldPeople: action.sort.field,
ascPeople: !state.ascPeople,
};
case HIDE_BANUSER_DIALOG:
return {
...state,
banDialog: false,
};
case SHOW_BANUSER_DIALOG:
return {
...state,
user: action.user,
banDialog: true,
};
case HIDE_REJECT_USERNAME_DIALOG:
return {
...state,
rejectUsernameDialog: false,
};
case SHOW_REJECT_USERNAME_DIALOG:
return {
...state,
user: action.user,
rejectUsernameDialog: true,
};
case SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
default:
return state;
}
}
+9 -9
View File
@@ -1,17 +1,17 @@
import * as actions from '../actions/config';
const initialState = {
data: {}
data: {},
};
export default function config (state = initialState, action) {
export default function config(state = initialState, action) {
switch (action.type) {
case actions.CONFIG_UPDATED:
return {
...state,
data: action.data,
};
default:
return state;
case actions.CONFIG_UPDATED:
return {
...state,
data: action.data,
};
default:
return state;
}
}

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