Compare commits

...
459 Commits
Author SHA1 Message Date
Kim Gardner 950efba3fe Merge pull request #1378 from coralproject/suspension-fix
Suspension Button Fix
2018-02-16 18:27:48 -05:00
Wyatt Johnson af592e326f Merge branch 'master' into suspension-fix 2018-02-16 16:18:02 -07:00
Wyatt Johnson c2aa2077f4 Show suspension status on button 2018-02-16 16:14:49 -07:00
Kim Gardner 26476f42f7 Merge pull request #1377 from coralproject/template-fixes
Template bugs
2018-02-16 18:06:28 -05:00
Wyatt Johnson 3434fb4fed Merge branch 'master' into template-fixes 2018-02-16 15:45:14 -07:00
Wyatt Johnson a6bc29af9a Merge pull request #1368 from coralproject/snyk-fix-946937f7
[Snyk Update] New fixes for 4 vulnerable dependency paths
2018-02-16 15:34:54 -07:00
Wyatt Johnson d86ff8418d fixed changes to api 2018-02-16 15:26:33 -07:00
Kim Gardner e71bcbe58d Yarn install 2018-02-16 17:14:54 -05:00
Kim Gardner 77c838ea22 Merge branch 'master' into snyk-fix-946937f7 2018-02-16 17:03:50 -05:00
Kim Gardner 39313a20fa Merge pull request #1374 from coralproject/spam-fixes
Fixes for Spam Plugin
2018-02-16 16:51:37 -05:00
Wyatt Johnson ad4427286d Merge branch 'master' into snyk-fix-946937f7 2018-02-16 14:39:32 -07:00
Wyatt Johnson dc82db6eda fixed template bugs 2018-02-16 14:38:57 -07:00
Wyatt Johnson 4bb2b3d731 fixes to spam plugin 2018-02-16 14:34:04 -07:00
Kim Gardner d812a0f696 Merge pull request #1371 from coralproject/fix-auth-proptypes
Fix proptypes errors in auth
2018-02-16 10:58:22 -05:00
Chi Vinh Le ba11eb233a Fix proptypes errors in auth 2018-02-16 12:31:37 +01:00
Wyatt Johnson e76d109f93 Merge branch 'master' into snyk-fix-946937f7 2018-02-15 14:23:28 -07:00
Kim Gardner 78db4077e1 Merge pull request #1369 from coralproject/typo-breaks-akismet
Translation typo is breaking Akismet
2018-02-15 16:07:00 -05:00
Kim Gardner 0519b55707 Translation typo is breaking Akismet 2018-02-15 15:55:51 -05:00
Kim Gardner 5602bf222c Merge pull request #1365 from coralproject/no-limit
Configurable Limits
2018-02-15 15:12:28 -05:00
Kim Gardner bfd6cfbaa4 Merge branch 'master' into snyk-fix-946937f7 2018-02-14 21:03:45 -05:00
Kim Gardner 9ec7027f3c Merge branch 'master' into no-limit 2018-02-14 21:03:17 -05:00
Kim Gardner 4acb6aeb6c Merge pull request #1361 from coralproject/auth-refactor
Auth refactor
2018-02-14 21:01:21 -05:00
snyk-bot c3bccaf4a8 fix: package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/npm:hoek:20180212
- https://snyk.io/vuln/npm:hoek:20180212

Latest report for coralproject/talk:
https://snyk.io/test/github/coralproject/talk
2018-02-15 01:19:30 +00:00
Kim Gardner e162a97919 Remove Facebook Auth from default Talk plugins 2018-02-14 18:31:12 -05:00
Kim Gardner f63a7915ea Update Facebook Auth and Recaptcha Docs 2018-02-14 18:24:16 -05:00
Wyatt Johnson 18710e2345 added tests 2018-02-14 15:58:15 -07:00
Kim Gardner ff29fa04e5 Merge branch 'master' into auth-refactor 2018-02-14 17:55:06 -05:00
Kim Gardner 7a9023744a Merge pull request #1363 from coralproject/router-cleanup
Router Cleanup
2018-02-14 17:54:45 -05:00
Kim Gardner b1c03973e6 Merge branch 'master' into router-cleanup 2018-02-14 17:39:39 -05:00
Kiwi 9e4759cbfd Merge branch 'master' into auth-refactor 2018-02-14 20:14:22 +01:00
Kiwi 94c98f000c Merge branch 'master' into no-limit 2018-02-14 20:11:39 +01:00
Kim Gardner 0b2cc29b4a Merge pull request #1367 from coralproject/akismet-docs-patch
Akismet Docs
2018-02-14 13:35:27 -05:00
Wyatt Johnson 0efa419b35 Update 04-03-additional-plugins.md 2018-02-14 11:26:43 -07:00
Wyatt Johnson c2154b6efa Merge branch 'master' into router-cleanup 2018-02-14 10:44:43 -07:00
Kim Gardner cfd52144f4 Merge pull request #1366 from coralproject/bump-version-4-2-0
Update version to 4.2.0
2018-02-14 12:43:00 -05:00
Kim Gardner 239377dd7f Update version to 4.2.0 2018-02-14 12:25:40 -05:00
Kiwi c46b29aed9 Merge branch 'master' into router-cleanup 2018-02-14 16:06:59 +01:00
Chi Vinh Le 43e02e1574 Apply suggestions 2018-02-14 14:59:00 +01:00
Wyatt Johnson ffd88e0ef0 Merge branch 'master' into no-limit 2018-02-13 18:32:02 -07:00
Wyatt Johnson 9820e575b2 patch 2018-02-13 18:29:07 -07:00
Wyatt Johnson 1e329c8967 code cleanup 2018-02-13 17:45:26 -07:00
Wyatt Johnson 73034a20f0 added support for configuring limiting, action optims 2018-02-13 17:24:57 -07:00
Wyatt Johnson 8f7171b54e fixed old default.css 2018-02-13 13:59:20 -07:00
Chi Vinh Le b6e4581ba2 Merge branch 'auth-refactor' of ssh://github.com/coralproject/talk into auth-refactor 2018-02-13 21:27:13 +01:00
Chi Vinh Le 78444a0bb4 Remove empty reducers 2018-02-13 21:25:48 +01:00
Chi Vinh Le 4d7e64e1ec Fix css 2018-02-13 21:20:08 +01:00
Kiwi b8e340c471 Merge branch 'master' into auth-refactor 2018-02-13 21:19:50 +01:00
Chi Vinh Le 39c1fcf83a remove before close 2018-02-13 21:17:41 +01:00
Chi Vinh Le 4b2d9fc93a Add formatting to Slot 2018-02-13 21:17:24 +01:00
Wyatt Johnson f56b8ca3a4 Merge pull request #1364 from coralproject/pym-patch
Patched Pym.js
2018-02-13 12:52:47 -07:00
Wyatt Johnson 0a324a2268 template cleanup 2018-02-13 12:46:55 -07:00
Wyatt Johnson 5f5f0ffa14 patched pym 2018-02-13 12:32:57 -07:00
Wyatt Johnson fc27e02140 removed old unreverted code 2018-02-13 12:28:42 -07:00
Wyatt Johnson f40f209395 cleaned up routes 2018-02-13 12:12:03 -07:00
Wyatt Johnson e924ed9189 reverted path change, cleaned embed stream 2018-02-13 11:35:20 -07:00
Wyatt Johnson 4053a25239 moved login route from /login to /embed/login 2018-02-13 11:35:20 -07:00
Kim Gardner de094ee948 Merge branch 'master' into auth-refactor 2018-02-13 13:07:42 -05:00
Kim Gardner aecae3efbe Merge pull request #1362 from sanoma/master
Added/updated missing Dutch translations
2018-02-13 12:23:36 -05:00
Bart Stroeken 67ade3a323 Merge pull request #3 from sanoma/feature/fix_translations
Added/updated dutch translations
2018-02-13 17:26:54 +01:00
Bart Stroeken 6d2e74eeec capitalfix 2018-02-13 17:22:36 +01:00
Chi Vinh Le c7ee8307e6 Merge branch 'master' into auth-refactor 2018-02-13 16:33:17 +01:00
Bart Stroeken eaeb7ea0da email spellingfix 2018-02-13 16:10:28 +01:00
Bart Stroeken 9e28be8246 Fixed review result 2018-02-13 16:09:25 +01:00
Chi Vinh Le 431bbd4f03 Fix some comments 2018-02-13 16:03:06 +01:00
Chi Vinh Le 0f69df26f6 Use more selectors 2018-02-13 16:00:03 +01:00
Chi Vinh Le c140a62822 Fix e2e 2018-02-13 15:53:12 +01:00
Chi Vinh Le cad25eedd1 CSS cleanup 2018-02-13 15:46:49 +01:00
Chi Vinh Le 87af05c0d9 Fix translations 2018-02-13 15:33:50 +01:00
Chi Vinh Le c54a8bdc4f Remove old code 2018-02-13 15:00:53 +01:00
Bart Stroeken 40963e236f Added/updated dutch translations 2018-02-13 13:24:52 +01:00
Chi Vinh Le d857585c7f Implement facebook auth client 2018-02-13 12:26:55 +01:00
Chi Vinh Le 3999b806c7 Use plugin-api 2018-02-13 12:24:29 +01:00
Chi Vinh Le 6c1ed826d0 Fix External 2018-02-13 11:48:12 +01:00
Chi Vinh Le 4224f18ba7 Add BareButton 2018-02-13 11:42:25 +01:00
Chi Vinh Le 6feaa94fba Listen for auth changes 2018-02-13 11:34:30 +01:00
Chi Vinh Le d639596047 Add External Slot 2018-02-13 11:26:37 +01:00
Chi Vinh Le f5409b11bf Use plugin-api 2018-02-13 11:14:12 +01:00
Wyatt Johnson a0118751c7 Merge pull request #1355 from leeeandroo/lock-install-bug
Lock install issue
2018-02-12 19:39:03 -07:00
Wyatt Johnson 6860b2b03f Merge branch 'master' into lock-install-bug 2018-02-12 19:18:39 -07:00
Wyatt Johnson ec8cfdf5f7 fixed logic 2018-02-12 19:17:58 -07:00
Chi Vinh Le 142c5b32f1 More refactoring 2018-02-13 00:54:15 +01:00
Kim Gardner 2bca30d3c1 Merge pull request #1356 from hellp/l10n-de
German translation
2018-02-12 18:38:49 -05:00
Kim Gardner 3dde285776 Merge branch 'master' into l10n-de 2018-02-12 18:30:02 -05:00
Kim Gardner 1304669416 Merge pull request #1359 from coralproject/fallback-lang
Fallback English Translations
2018-02-12 18:29:41 -05:00
Wyatt Johnson 9703bc8645 lint 2018-02-12 15:58:26 -07:00
Kim Gardner 85d67dba19 Merge branch 'master' into fallback-lang 2018-02-12 17:52:52 -05:00
Wyatt Johnson ffc880c33b duplicated code 2018-02-12 15:51:47 -07:00
Wyatt Johnson ae881df7f3 added fallback en for missing translations 2018-02-12 15:50:36 -07:00
Kim Gardner 33dd2e5c0a Merge branch 'master' into l10n-de 2018-02-12 17:46:38 -05:00
Kim Gardner dbe471fa96 Merge pull request #1358 from coralproject/add-suspend-msg-translations
Add translation keys for suspend error message
2018-02-12 17:45:49 -05:00
Kim Gardner afe411515d Add suspend error message 2018-02-12 17:26:40 -05:00
Kim Gardner a214969527 Merge pull request #1353 from coralproject/add-missing-translation-keys
Add missing translation keys
2018-02-12 17:22:59 -05:00
Kim Gardner bd16ff863d Merge branch 'master' into add-missing-translation-keys 2018-02-12 15:36:30 -05:00
Kim Gardner c0b0627d6a Merge pull request #1354 from facuesr/user-suspended-post-error-msg
Informative msg when the user is suspended and tries to post a message
2018-02-12 15:35:55 -05:00
Fabian Neumann 13a52e8d64 Revert "Change button 'Freigegeben' to 'Akzeptiert' as a style-workaround."
This reverts commit f515e22217.
2018-02-12 14:41:31 +01:00
Fabian Neumann 968dd14720 German translations for user_detail 2018-02-12 14:07:27 +01:00
Fabian Neumann 065af06709 Add translations for "admin_sidebar" 2018-02-12 13:55:21 +01:00
Fabian Neumann b2ce217a41 Translate "Zen Mode" more directly. 2018-02-12 13:25:55 +01:00
Fabian Neumann e93e4756bf Add DE translation for string added in 4.1.0 2018-02-12 13:25:23 +01:00
Fabian Neumann 349c972f8b Fix some copy-n-paste caused "E-Mail" keys. 2018-02-12 13:18:00 +01:00
Fabian Neumann f515e22217 Change button 'Freigegeben' to 'Akzeptiert' as a style-workaround.
'Freigegeben' is too long for the current button style to fit it.
While 'Akzeptiert' is a (slighly) worse translations, it will work
until (maybe) Talk PR 1322 is accepted.
2018-02-12 13:18:00 +01:00
Fabian Neumann 90c99727cb Change tab "Vormoderation" to "Unmoderiert".
This better reflects the status in which the comments in this lane are.
2018-02-12 13:18:00 +01:00
Fabian Neumann 54ce58c48f German translations for plugins. Plus typos and wording changes in core. 2018-02-12 13:18:00 +01:00
Fabian Neumann 175b677afd Add German translations for Talk core. 2018-02-12 13:18:00 +01:00
Fabian Neumann 39b34d624a Register DE locales in i18n framework. 2018-02-12 13:18:00 +01:00
Leandro e9a781607a prettier change 2018-02-12 12:09:28 +01:00
Leandro 05a6af49c4 fix switch syntax and not throw error when install lock=TRUE 2018-02-12 11:56:47 +01:00
Facu 4b2aa04ef6 error msg when suspenden user try to post 2018-02-11 23:37:33 -03:00
Kim Gardner 5481491bf2 Fix typo 2018-02-09 18:28:12 -05:00
Kim Gardner eef55f33fc Respect plugin 2018-02-09 18:17:33 -05:00
Kim Gardner 2ad5219e59 Sorting plugins 2018-02-09 18:16:46 -05:00
Kim Gardner e08024a136 Subscriber plugin 2018-02-09 18:11:15 -05:00
Kim Gardner 9f465ee7ce Toxic comments plugin 2018-02-09 18:10:29 -05:00
Kim Gardner 29a64d540b Viewing options plugin 2018-02-09 18:08:56 -05:00
Kim Gardner 749189f0e5 Akismet plugin 2018-02-09 18:07:59 -05:00
Kim Gardner 41413a9e91 Auth plugin 2018-02-09 18:06:25 -05:00
Kim Gardner e65c71cae7 Author menu plugin 2018-02-09 18:04:43 -05:00
Kim Gardner c7775cfdfd Featured comments plugin 2018-02-09 18:03:42 -05:00
Kim Gardner 78473cb21f Flag details plugin 2018-02-09 18:02:28 -05:00
Kim Gardner 4a3bdb0633 Ignore user plugin 2018-02-09 18:01:39 -05:00
Kim Gardner fdafa1ca42 Like plugin 2018-02-09 17:59:01 -05:00
Kim Gardner 243f846f3b Love plugin 2018-02-09 17:58:08 -05:00
Kim Gardner 8f72ff7ca2 Member since plugin 2018-02-09 17:56:41 -05:00
Kim Gardner adf0b65fde Moderation actions plugin 2018-02-09 17:55:34 -05:00
Kim Gardner 3bb0cb9168 Off-topic plugin 2018-02-09 17:53:36 -05:00
Kim Gardner 670084427d Update Chinese 2018-02-09 17:23:54 -05:00
Chi Vinh Le 4b6a3b5b9d More refactoring 2018-02-09 23:18:21 +01:00
Kim Gardner c8727e0f41 Update Portuguese 2018-02-09 17:10:32 -05:00
Kim Gardner 705c54f397 Update Dutch 2018-02-09 16:42:54 -05:00
Chi Vinh Le f4fc8915e3 Refactor talk-plugin-auth part 1 2018-02-09 20:12:02 +01:00
Kim Gardner 1b2929af29 Add some missing Danish 2018-02-09 11:47:06 -05:00
Chi Vinh Le 1774f98519 Refactor Stream container passing props 2018-02-09 00:19:22 +01:00
Chi Vinh Le 4883bb17b9 Finish refactor embed stream auth 2018-02-09 00:08:31 +01:00
Kim Gardner 2f71e1bf8e Update French 2018-02-08 17:49:43 -05:00
Kim Gardner 408a082f2d Update Spanish 2018-02-08 17:12:16 -05:00
Chi Vinh Le 37dd6ae044 Auth refactor part 3 2018-02-08 23:06:01 +01:00
Kim Gardner 2e0a4df2be Update Danish 2018-02-08 14:28:39 -05:00
Kim Gardner 6965b33968 Update Danish 2018-02-08 14:28:27 -05:00
Kim Gardner d2235f9918 Merge pull request #1338 from leeeandroo/admin-user-details
Admin user details translations and wording
2018-02-08 12:50:00 -05:00
Kim Gardner 5ebb99740b Merge branch 'master' into admin-user-details 2018-02-08 12:07:12 -05:00
Kim Gardner 2978d6138c Merge pull request #1349 from coralproject/akismet
Akismet
2018-02-08 12:07:02 -05:00
Kim Gardner 4dbd9e7df1 Merge branch 'master' into akismet 2018-02-08 11:57:49 -05:00
Kim Gardner 686096bfae Merge pull request #1352 from coralproject/change-comment-not-found-text
Update stream.comment_not_found text
2018-02-08 11:57:31 -05:00
Kim Gardner ddb80a95c1 Merge branch 'master' into akismet 2018-02-08 11:38:20 -05:00
Kim Gardner 18cc8b9389 Update stream.comment_not_found text 2018-02-08 11:28:21 -05:00
Kim Gardner 22467ac123 Merge branch 'master' into admin-user-details 2018-02-08 11:26:11 -05:00
Chi Vinh Le d526a404ae Finish admin auth refactor 2018-02-07 22:17:52 +01:00
Chi Vinh Le aaad7aa86f Reimplement recaptcha 2018-02-07 22:10:05 +01:00
Wyatt Johnson 74a2ed4a41 introduce no limit 2018-02-07 13:29:59 -07:00
Chi Vinh Le bad0363628 Save static config to redux 2018-02-07 21:08:37 +01:00
Chi Vinh Le ffbcd93c3f Adapt to master changes 2018-02-07 19:04:35 +01:00
Wyatt Johnson 2407cb9594 Merge pull request #1351 from mattmontgomery/patch-1
Provides username for subscription fields
2018-02-07 10:47:32 -07:00
Chi Vinh Le 61bc372640 Merge branch 'master' into auth-refactor 2018-02-07 18:42:26 +01:00
Chi Vinh Le ee3d576a49 Auth Refactor part 2 2018-02-07 18:42:03 +01:00
Wyatt Johnson 3c04e6c7ec Merge branch 'master' into patch-1 2018-02-07 10:40:14 -07:00
Wyatt Johnson 52491d81a9 Merge pull request #1346 from mattmontgomery/patch-2
Adds logout for non-admin users
2018-02-07 10:40:05 -07:00
Wyatt Johnson 77723e3fb3 Merge branch 'master' into patch-2 2018-02-07 10:32:34 -07:00
Leandro bba14f502a Merge branch 'admin-user-details' of https://github.com/leeeandroo/talk into admin-user-details 2018-02-07 09:14:28 +01:00
Leandro 44913864eb remove identification changes on UserDetails 2018-02-07 09:12:39 +01:00
Matt Montgomery 3c66401546 Providers username for subscription fields 2018-02-06 16:29:25 -07:00
Wyatt Johnson 2e6a7c0559 Merge pull request #1335 from coralproject/keep-drafts
Keep comment drafts
2018-02-06 14:30:27 -07:00
Wyatt Johnson e7fc33c667 Merge branch 'master' into keep-drafts 2018-02-06 14:21:53 -07:00
Wyatt Johnson 1cc484b61e Merge branch 'master' into akismet 2018-02-06 14:06:03 -07:00
Wyatt Johnson 17fefcb4bd adjusted docs 2018-02-06 13:47:57 -07:00
Wyatt Johnson f4d520fbaa adjusted translations 2018-02-06 13:44:28 -07:00
Chi Vinh Le f8e0556904 Auth Refactor part 1 2018-02-06 20:30:31 +01:00
Kim Gardner a5c08129ce Merge branch 'master' into admin-user-details 2018-02-06 14:20:26 -05:00
Kim Gardner 7c859ad4c3 Merge branch 'master' into patch-2 2018-02-06 14:20:00 -05:00
Kim Gardner 2b2a762623 Merge pull request #1348 from coralproject/password-attempt-typo
Correct password attempt comment
2018-02-06 14:13:07 -05:00
Kim Gardner b9cfddc5c0 Merge branch 'master' into keep-drafts 2018-02-06 14:06:57 -05:00
Wyatt Johnson 7dca937de2 Merge branch 'master' into password-attempt-typo 2018-02-06 10:37:05 -07:00
Wyatt Johnson 6cb3fc7be8 added docs for akismet 2018-02-06 10:23:26 -07:00
Kim Gardner 95211e6cb9 Merge pull request #1345 from mattmontgomery/patch-1
Use translate-able `sign_out`
2018-02-06 12:23:07 -05:00
Wyatt Johnson db322d192f added note 2018-02-06 09:58:02 -07:00
Wyatt Johnson bf98050d33 Merge branch 'master' into akismet 2018-02-06 09:49:13 -07:00
Kim Gardner caa588f11d Merge branch 'master' into keep-drafts 2018-02-06 11:36:31 -05:00
Kim Gardner 764ba4ab36 Correct password attempt comment 2018-02-06 11:21:41 -05:00
Matt Montgomery 9c139eb703 Fixes prettier/prettier linting error 2018-02-05 17:46:10 -07:00
Matt Montgomery c289ed149f Adds logout for non-admin users 2018-02-05 17:17:51 -07:00
Matt Montgomery 05d80a0549 Use translate-able sign_out 2018-02-05 17:03:44 -07:00
Kim Gardner 8bef21274d Merge branch 'master' into admin-user-details 2018-02-05 11:31:28 -05:00
Kim Gardner 869fb9d029 Merge pull request #1344 from coralproject/rem-semi
Remove semicolon
2018-02-05 11:30:19 -05:00
Chi Vinh Le cb077a87dc Remove semicolon 2018-02-05 17:22:13 +01:00
Kim Gardner 6d662e4469 Merge branch 'master' into admin-user-details 2018-02-05 10:59:42 -05:00
Wyatt Johnson 901c96a67a Merge pull request #1343 from mattmontgomery/patch-2
Changes node requirement notation
2018-02-03 13:10:22 -07:00
Matt Montgomery 6b5a690c4f Changes node requirement notation
This changes the notation for the `node` requirement. Solves #1342
2018-02-02 17:04:01 -07:00
Wyatt Johnson 9debb0d48d Merge branch 'master' into akismet 2018-02-02 15:01:06 -07:00
Wyatt Johnson 484ae2539b added akismet support 2018-02-02 15:00:47 -07:00
Kim Gardner 1dfce22b89 Merge pull request #1341 from coralproject/fix-zen-mode
Fix zen mode
2018-02-02 10:00:46 -05:00
Kiwi e36098369b Merge branch 'master' into fix-zen-mode 2018-02-02 15:53:13 +01:00
Chi Vinh Le aec0251c4f Fix zen mode 2018-02-02 15:50:43 +01:00
Kim Gardner c9613337d4 Merge pull request #1339 from coralproject/fix-flag-issues
Fix flag issues
2018-02-02 09:27:28 -05:00
Chi Vinh Le ac67b0484b Better fix 2018-02-02 12:12:58 +01:00
Chi Vinh Le edac21a4f4 Fix Flag Menu not working 2018-02-02 11:52:42 +01:00
Leandro f75924e683 translate ViewOptions componenet 2018-02-02 10:34:46 +01:00
Leandro 0966aa9463 insert new admin keys about user details 2018-02-02 10:25:08 +01:00
Leandro 30cd16fd56 make user details texts translatable and change email to identification 2018-02-02 10:22:16 +01:00
Kim Gardner 86f4ae4b6d Merge pull request #1336 from icco/patch-2
Change text wrapping of Stories table
2018-02-01 13:56:31 -05:00
Nat Welch 219fcdddea Change text wrapping of Stories table 2018-02-01 13:26:34 -05:00
Chi Vinh Le 4464183b42 Linting 2018-02-01 18:25:35 +01:00
Kiwi ed8d9e985c Merge branch 'master' into keep-drafts 2018-02-01 18:19:30 +01:00
Chi Vinh Le 840fef84db Fix e2e 2018-02-01 18:17:35 +01:00
Wyatt Johnson 6c9411d173 Merge pull request #1333 from coralproject/remove-auth-data
Cleanly remove auth data from storage
2018-02-01 10:12:48 -07:00
Chi Vinh Le 6afb300d86 Unlisten 2018-02-01 17:55:53 +01:00
Kiwi 894141619c Merge branch 'master' into keep-drafts 2018-02-01 17:50:06 +01:00
Chi Vinh Le 7e135cef47 Add a little comment 2018-02-01 17:46:06 +01:00
Chi Vinh Le 34726e2c4e Implement DraftArea 2018-02-01 17:29:53 +01:00
Wyatt Johnson 1f6c29cee6 Merge branch 'master' into remove-auth-data 2018-02-01 09:21:53 -07:00
Wyatt Johnson 2234acc28e Merge pull request #1334 from leeeandroo/missing-path
Insert BASE_PATH on public assets
2018-02-01 09:21:38 -07:00
Wyatt Johnson 13e6645f26 Merge branch 'master' into remove-auth-data 2018-02-01 09:15:54 -07:00
Wyatt Johnson 09983e412d Merge branch 'master' into missing-path 2018-02-01 09:14:16 -07:00
Wyatt Johnson 3563f7e2ae Merge pull request #1332 from coralproject/moderator-configure
Moderator Configuration
2018-02-01 09:13:59 -07:00
Chi Vinh Le dc5a3e43a6 If we are not in iFrame just use localStorage 2018-02-01 13:53:15 +01:00
Chi Vinh Le 2e59f64738 Pass sessionStorage to context 2018-02-01 13:34:03 +01:00
Chi Vinh Le 91e08cb117 Rename storage to localStorage in context 2018-02-01 13:28:28 +01:00
Leandro c4b6cb54e6 Insert BASE_PATH on public assets 2018-02-01 13:27:17 +01:00
Chi Vinh Le 118301c2b4 Implement a session storage 2018-02-01 12:25:51 +01:00
Chi Vinh Le d6ddd9c195 Clearly remove auth data from storage 2018-02-01 12:13:33 +01:00
Wyatt Johnson 596c214f1c allowed moderators to configure asset 2018-01-31 13:41:07 -07:00
Wyatt Johnson 9b7f1dc43e Merge pull request #1331 from coralproject/public-path
Dynamic Public Path
2018-01-31 12:33:30 -07:00
Wyatt Johnson c4cddefcff abstracted config to service 2018-01-31 11:25:43 -07:00
Wyatt Johnson 062ce58cf8 ensure dynamic imports use proper path/url 2018-01-31 10:57:01 -07:00
Wyatt Johnson 8b6a993587 Merge pull request #1329 from coralproject/docs
Docs update
2018-01-30 15:40:43 -07:00
Wyatt Johnson 26bd01cbda updated docs for 4.1 release 2018-01-30 15:29:36 -07:00
Kim Gardner f492040a57 Merge pull request #1328 from coralproject/user-perms
Username Permissions
2018-01-30 15:49:00 -05:00
Kim Gardner d7fd8ac714 Merge branch 'master' into user-perms 2018-01-30 13:58:42 -05:00
Kim Gardner 3e273a76f1 Merge pull request #1327 from coralproject/bump-version
Update version to 4.1.0 and update LICENSE
2018-01-30 13:58:29 -05:00
Kim Gardner 7504dc6dfe Merge branch 'master' into bump-version 2018-01-30 13:28:08 -05:00
Wyatt Johnson 0f9387dbb9 Merge branch 'master' into user-perms 2018-01-30 11:03:42 -07:00
Wyatt Johnson ec54376a53 blocked user actions in username change process 2018-01-30 11:00:48 -07:00
Kim Gardner 0793784f5a Merge pull request #1324 from coralproject/notify-on-edit
Refactor + Fix notify on edit bug
2018-01-30 12:58:47 -05:00
Kiwi 00a7b11039 Merge branch 'master' into notify-on-edit 2018-01-30 18:30:42 +01:00
Kiwi 5138e79424 Merge pull request #1325 from coralproject/fix-gender
Fix gender translation
2018-01-30 18:30:25 +01:00
Kim Gardner 00eff653cb Update version to 4.1.0 and update LICENSE 2018-01-30 12:21:10 -05:00
Chi Vinh Le a59464e45d Merge branch 'notify-on-edit' of ssh://github.com/coralproject/talk into notify-on-edit 2018-01-30 18:20:59 +01:00
Chi Vinh Le feeaa8ee8b Better todo descr 2018-01-30 18:19:31 +01:00
Kiwi 1c4b8a0a67 Merge branch 'master' into notify-on-edit 2018-01-30 18:10:47 +01:00
Kim Gardner 8ad5675916 Merge branch 'master' into fix-gender 2018-01-30 12:09:06 -05:00
Kim Gardner 7a7d45cf91 Merge pull request #1326 from coralproject/fix-ie11-sideactions
Add IE CSS fixes
2018-01-30 12:08:58 -05:00
Kiwi e77e62236c Merge branch 'master' into fix-ie11-sideactions 2018-01-30 17:57:04 +01:00
Kiwi 8e142a5ecb Merge branch 'master' into fix-gender 2018-01-30 17:56:31 +01:00
Chi Vinh Le 3d57122a65 Add IE CSS fixes 2018-01-30 17:56:15 +01:00
Chi Vinh Le 665d8c1ef0 Fix gender translation 2018-01-30 17:42:30 +01:00
Kiwi bd2c4d4bfc Merge branch 'master' into notify-on-edit 2018-01-30 17:37:15 +01:00
Chi Vinh Le 81e68c8ef4 Fix edited comment not showing notifications 2018-01-30 17:34:18 +01:00
Chi Vinh Le cb5a41f119 Refactor 2018-01-30 17:18:09 +01:00
Kim Gardner 082287387a Merge pull request #1321 from coralproject/username-flow-fix
Changed Username Moderation Flow
2018-01-30 11:14:33 -05:00
Kim Gardner 9143bd2b31 Merge branch 'master' into username-flow-fix 2018-01-30 10:31:58 -05:00
Kim Gardner 2def894237 Merge pull request #1322 from hellp/approve-reject-button-style
Make Approve/Reject button styles more robust for longer labels
2018-01-30 10:31:27 -05:00
Kim Gardner 7dd7f9ca54 Merge branch 'master' into approve-reject-button-style 2018-01-30 10:14:22 -05:00
Chi Vinh Le b33cde03c3 Removes some weird characters 2018-01-30 15:59:02 +01:00
Chi Vinh Le d80cd6f0a2 More Refactor 2018-01-30 15:49:28 +01:00
Chi Vinh Le ebb85d17ce Refactor 2018-01-30 15:13:14 +01:00
Fabian Neumann efe84f3f98 Make Approve/Reject button styles more robust for long texts.
For other languages than English the 'Approve' text did not fit
into the button. This commit makes the CSS a bit more flexible by
removing some very pixel-specfic styles.
2018-01-30 14:50:35 +01:00
Wyatt Johnson 335349963c adjusted flow for changed usernames, e2e 2018-01-29 17:46:03 -07:00
Kim Gardner 495e3b009f Merge branch 'master' into username-flow-fix 2018-01-29 19:21:08 -05:00
Kim Gardner 2d82f4c217 Merge pull request #1319 from coralproject/cleanup
User Deletion Cleanup
2018-01-29 19:20:53 -05:00
Kim Gardner 6a70077398 Merge branch 'master' into cleanup 2018-01-29 19:11:18 -05:00
Wyatt Johnson d25ca0ef6f more test criteria 2018-01-29 15:36:59 -07:00
Wyatt Johnson 985fc05a47 only users that have approved usernames may comment 2018-01-29 15:29:55 -07:00
Wyatt Johnson 73cbe4ab4b Merge pull request #1315 from coralproject/better-change-detection
Better handle comment edits
2018-01-29 14:54:00 -07:00
Wyatt Johnson 8d656b51a0 introduced count adjusters for deleting a user 2018-01-29 13:56:09 -07:00
Chi Vinh Le 0a889fd1b3 More Refactoring 2018-01-29 21:26:44 +01:00
Kim Gardner c18c43e5c8 Merge branch 'master' into better-change-detection 2018-01-29 15:01:45 -05:00
Kim Gardner c22f8d0dc0 Merge pull request #1317 from coralproject/edit-history
Cleanup to Edit/Premod logic
2018-01-29 14:57:27 -05:00
Chi Vinh Le 0799e9d8ee More refactor 2018-01-29 20:52:44 +01:00
Kim Gardner 9645ccb6cd Merge branch 'master' into better-change-detection 2018-01-29 14:40:48 -05:00
Kim Gardner be51704b1f Merge branch 'master' into edit-history 2018-01-29 14:40:44 -05:00
Kim Gardner 7e2d414f13 Merge pull request #1312 from coralproject/zen-mode
Rename "single comment view" to "zen mode"
2018-01-29 14:40:03 -05:00
Kiwi d2e87bcbda Merge branch 'master' into zen-mode 2018-01-29 20:32:08 +01:00
Kim Gardner 9d168c6af8 Merge pull request #1316 from coralproject/fix-hidden-moderator-commens
Fix moderator comments not showing up in premod
2018-01-29 14:17:35 -05:00
Kim Gardner 3a67768c51 Merge branch 'master' into fix-hidden-moderator-commens 2018-01-29 14:09:48 -05:00
Kim Gardner e3ccb8560b Merge pull request #1318 from coralproject/nsp
Removed NSP from CI
2018-01-29 14:09:36 -05:00
Chi Vinh Le c355e10453 Refactor 2018-01-29 19:21:42 +01:00
Wyatt Johnson e20e55b589 removed outdated logic 2018-01-29 10:35:10 -07:00
Wyatt Johnson c13ea699d9 turned off nsp on CI 2018-01-29 10:07:42 -07:00
Wyatt Johnson 873932b6a6 Merge branch 'master' into cleanup 2018-01-29 10:02:52 -07:00
Wyatt Johnson bc42539fc4 cleanup 2018-01-29 10:02:38 -07:00
Chi Vinh Le 5f608e9bc6 Fix moderator comments not showing up 2018-01-29 17:40:38 +01:00
Chi Vinh Le 2bfa99fa1a Remove unused proptype 2018-01-29 14:52:26 +01:00
Chi Vinh Le c12fe526b8 Better detect edits 2018-01-29 14:52:09 +01:00
Chi Vinh Le fc3bc0a542 Merge branch 'master' into better-change-detection 2018-01-29 14:21:02 +01:00
Chi Vinh Le 78019396e2 Use same created_at 2018-01-29 13:47:46 +01:00
Kim Gardner 7d31487275 Merge branch 'master' into zen-mode 2018-01-26 15:31:22 -05:00
Kim Gardner f5c3f7e9bc Merge pull request #1313 from coralproject/activity-dot-subscription
Port Activity dot polling to subscriptions
2018-01-26 15:31:01 -05:00
Kim Gardner 9e127d2300 Merge branch 'master' into activity-dot-subscription 2018-01-26 15:13:30 -05:00
Wyatt Johnson 172bc22af5 Merge branch 'master' into cleanup 2018-01-26 12:58:36 -07:00
Wyatt Johnson 4be9e74ed9 removed unused code 2018-01-26 12:58:16 -07:00
Wyatt Johnson f4d134d0e2 Merge pull request #1311 from coralproject/migration-updates
Migration updates
2018-01-26 12:57:08 -07:00
Wyatt Johnson 752825db68 Merge branch 'master' into migration-updates 2018-01-26 10:51:58 -07:00
Chi Vinh Le 9a36473c8f Merge branch 'activity-dot-subscription' of ssh://github.com/coralproject/talk into activity-dot-subscription 2018-01-26 17:30:16 +01:00
Chi Vinh Le 3fa2af975c Bug fixes 2018-01-26 17:28:21 +01:00
Wyatt Johnson 57a77a7edf enhanced flag migration 2018-01-25 19:21:00 -07:00
Wyatt Johnson 6caae2df97 applied migration performance improvements 2018-01-25 18:17:04 -07:00
Kiwi c00e534984 Merge branch 'master' into activity-dot-subscription 2018-01-26 01:49:04 +01:00
Chi Vinh Le b638b783fe Refactor 2018-01-26 01:42:46 +01:00
Kim Gardner de18ab1e15 Merge pull request #1310 from coralproject/plugin-cli
Plugin CLI Fix
2018-01-25 18:44:53 -05:00
Wyatt Johnson 75751285fe fixed default setup 2018-01-25 16:18:05 -07:00
Wyatt Johnson 04390c5acd migration rewrite and removed verifications 2018-01-25 16:12:26 -07:00
Chi Vinh Le ce25cfb159 Reconstruct previous comment state more accurately 2018-01-25 22:03:37 +01:00
Wyatt Johnson e8f73ddb87 changed copy 2018-01-25 13:20:16 -07:00
Wyatt Johnson e6f796e99c expanded on migration support 2018-01-25 13:07:29 -07:00
Kim Gardner 0507e750b9 Update translations 2018-01-25 15:00:34 -05:00
Kim Gardner fe4cddda60 Copy change 2018-01-25 14:51:15 -05:00
Kim Gardner 63347dc0eb Change s to z for zen mode shortcut and update copy and docs 2018-01-25 14:50:06 -05:00
Chi Vinh Le 426025f8c2 Subscribe to edited + bug fixes 2018-01-25 20:49:57 +01:00
Chi Vinh Le 6fd4d1576e Correct logic for disabling activity subscriptions 2018-01-25 20:08:57 +01:00
Chi Vinh Le 26ca3622c2 Integrate featured into activity indicator subscription 2018-01-25 20:01:25 +01:00
Wyatt Johnson ef7693afa5 fixed error in migration 2018-01-25 11:25:32 -07:00
Wyatt Johnson de9383e657 more extreme error exiting for rejected promise that are not handled 2018-01-25 10:17:26 -07:00
Wyatt Johnson 1e517469c3 Fail on plugin installation fail 2018-01-25 10:16:14 -07:00
Chi Vinh Le 66934cbb82 Implement indicator subscriptions for moderation 2018-01-25 16:45:19 +01:00
Chi Vinh Le 2246b16cf4 Add some comments 2018-01-24 23:34:53 +01:00
Chi Vinh Le 1fc07e8649 Implement activity tracking for flagged accounts 2018-01-24 23:31:31 +01:00
Wyatt Johnson 4e9b19c7f6 performance related improvements to migraitons 2018-01-24 15:00:30 -07:00
Wyatt Johnson a8f794ef76 Merge pull request #1306 from coralproject/error-notifications
Error handling improvements
2018-01-24 12:34:11 -07:00
Kim Gardner 6825b4c999 Merge branch 'master' into error-notifications 2018-01-24 14:22:52 -05:00
Kim Gardner 54fb334288 Merge pull request #1308 from coralproject/flagged-user-padding
Add padding to flagged user
2018-01-24 14:22:39 -05:00
Kim Gardner 798732cbcf Merge branch 'master' into flagged-user-padding 2018-01-24 13:54:34 -05:00
Kim Gardner e18d92944a Merge pull request #1309 from hellp/wrong-premod-text
Fix wrong text on "premod if contains links" setting
2018-01-24 13:54:04 -05:00
Kim Gardner f0498fb3ba Merge branch 'master' into flagged-user-padding 2018-01-24 13:45:56 -05:00
Kim Gardner 0f938b19a3 Merge branch 'master' into wrong-premod-text 2018-01-24 13:45:08 -05:00
Wyatt Johnson bcd18ccb32 Merge branch 'master' into error-notifications 2018-01-24 10:31:43 -07:00
Wyatt Johnson c6dd182f56 Merge pull request #1303 from coralproject/moderation-refactor
Moderation refactor
2018-01-24 10:31:31 -07:00
Kiwi cae974d0df Merge branch 'master' into moderation-refactor 2018-01-24 18:19:09 +01:00
Kiwi c41320e155 Merge branch 'master' into error-notifications 2018-01-24 18:18:52 +01:00
Kiwi 66ca985405 Merge branch 'master' into flagged-user-padding 2018-01-24 18:18:40 +01:00
Wyatt Johnson a366c6efa4 Merge pull request #1305 from coralproject/auth-callback
Auth Callback Regression
2018-01-24 10:18:03 -07:00
Wyatt Johnson 366f40a6e4 applied fix to template 2018-01-24 10:09:33 -07:00
Fabian Neumann 9da61b5991 Fix wrong text on "premod if contains links" setting. 2018-01-24 16:58:58 +01:00
Chi Vinh Le 9feeaf4dca Add padding to flagged user 2018-01-24 15:35:34 +01:00
Kiwi 7f9e09461c Merge branch 'master' into error-notifications 2018-01-23 20:41:17 +01:00
Chi Vinh Le d15694041a Don't require notify property 2018-01-23 20:27:46 +01:00
Chi Vinh Le 59cdc9fd5d Port plugins 2018-01-23 20:06:55 +01:00
Chi Vinh Le b196bb379a Comments 2018-01-23 19:52:04 +01:00
Chi Vinh Le c7242f5d98 Improve error handling of report feature 2018-01-23 19:47:11 +01:00
Chi Vinh Le ace6cbb158 Error reporting for the rest 2018-01-23 19:41:00 +01:00
Chi Vinh Le 0523faee2b withQuery and withMutation support for notifyOnError (default: true) 2018-01-23 19:05:08 +01:00
Wyatt Johnson e4834027b0 Auth Callback Regression 2018-01-23 11:03:53 -07:00
Chi Vinh Le 303e2e84c5 Notify on error (embed stream -> profile) 2018-01-23 17:37:52 +01:00
Chi Vinh Le 04fd056cab Notify on errors (embed stream - configure) 2018-01-23 17:36:07 +01:00
Chi Vinh Le 74b545a8d9 Refetch configure and notify on error 2018-01-23 16:58:11 +01:00
Chi Vinh Le 957fd34168 Show error message on data error 2018-01-23 16:53:03 +01:00
Chi Vinh Le dd836ad28e Notify on error (Community > Reported Username) 2018-01-23 16:45:32 +01:00
Chi Vinh Le 196ae5e29c Notify on error (Communit > People) 2018-01-23 16:41:44 +01:00
Chi Vinh Le f4e4c1d810 Notify on error (UserDetail) 2018-01-23 16:27:02 +01:00
Wyatt Johnson f3469f3dc4 Merge branch 'master' into moderation-refactor 2018-01-22 16:34:56 -07:00
Wyatt Johnson d101989ebd Merge pull request #1298 from coralproject/live-reported
Implement Live Updates for Reported Usernames
2018-01-22 16:30:46 -07:00
Wyatt Johnson b36c70acc1 replaced pre-git with pre-commit 2018-01-22 16:16:04 -07:00
Wyatt Johnson 25359ee88d Moderation Refactor 2018-01-22 16:11:13 -07:00
Chi Vinh Le 6a06cb4684 Show notification when encountering an error in modqueue 2018-01-22 22:42:17 +01:00
Kiwi 12652d59c4 Merge branch 'master' into live-reported 2018-01-22 20:01:12 +01:00
Wyatt Johnson 8e8458781d Merge pull request #1302 from coralproject/plugin-directory
Plugin Working Directory
2018-01-22 11:50:45 -07:00
Kiwi 852b21e264 Merge branch 'master' into plugin-directory 2018-01-22 19:42:05 +01:00
Kim Gardner 81d049e533 Merge branch 'master' into live-reported 2018-01-22 13:33:15 -05:00
Kim Gardner 3fe8a8fb8d Merge pull request #1301 from coralproject/fix-report-bugs
Fixes for report dialog
2018-01-22 13:33:01 -05:00
Kim Gardner 0f6cc20478 Update error message 2018-01-22 13:08:28 -05:00
Wyatt Johnson 26c109e8ae Removed cwd in place of __dirname 2018-01-22 11:06:49 -07:00
Chi Vinh Le 1e34af6aa3 Fixes for report dialog 2018-01-22 18:20:36 +01:00
Kiwi 60c62e4f1a Merge branch 'master' into live-reported 2018-01-22 17:46:49 +01:00
Kim Gardner 30dddf387c Merge pull request #1299 from coralproject/fix-modqueue-counts
Fix modqueue count bugs
2018-01-22 11:31:42 -05:00
Kiwi 0528106fdf Merge branch 'master' into live-reported 2018-01-22 17:16:08 +01:00
Kim Gardner bdd207524c Merge branch 'master' into fix-modqueue-counts 2018-01-22 11:09:40 -05:00
Kim Gardner 4be19fd028 Merge pull request #1300 from coralproject/fix-false-spinner
Don't show spinner on fetchMore
2018-01-22 11:08:59 -05:00
Kim Gardner 66202d1fd6 Merge branch 'master' into fix-false-spinner 2018-01-22 10:51:49 -05:00
Kim Gardner d98a76272c Merge pull request #1280 from sanoma/master
Dutch translation
2018-01-22 10:51:00 -05:00
Chi Vinh Le 15d0ecbe51 Don't show spinner on fetchMore 2018-01-22 14:34:30 +01:00
Tibo Beijen ca653fd5ff Added missing translations, added since start of nl translations. 2018-01-22 14:13:44 +01:00
Tibo Beijen 468a1683ce Small lintin fix. 2018-01-22 11:22:12 +01:00
Tibo Beijen d827ebef72 Merge branch 'master' into sanoma-master 2018-01-22 09:29:22 +01:00
Kiwi 65588f234a Merge branch 'master' into live-reported 2018-01-19 22:07:20 +01:00
Kiwi bb2ea77008 Merge branch 'master' into fix-modqueue-counts 2018-01-19 21:52:13 +01:00
Chi Vinh Le 392c5511eb Fix modqueue counts 2018-01-19 21:15:44 +01:00
Wyatt Johnson a9be049bf6 Merge pull request #1296 from coralproject/user-json
Auth Callback Rewrite
2018-01-19 11:27:19 -07:00
Chi Vinh Le bf752a50ca Don't decrease count twice 2018-01-19 18:19:34 +01:00
Wyatt Johnson a402817cc9 Merge branch 'master' into user-json 2018-01-19 10:12:36 -07:00
Chi Vinh Le 669eb360df Merge branch 'live-reported' of ssh://github.com/coralproject/talk into live-reported 2018-01-19 17:04:49 +01:00
Chi Vinh Le 25dd979a37 Naming 2018-01-19 16:06:25 +01:00
Kiwi e653a5f9c0 Merge branch 'master' into live-reported 2018-01-19 16:04:43 +01:00
Chi Vinh Le 5d26f23b7b Prevent simulaneouse mutations (of the same one) 2018-01-19 15:51:51 +01:00
Chi Vinh Le e2a361276b show notification on errors 2018-01-19 15:33:34 +01:00
Chi Vinh Le 319bca3ba8 Refactor indicator 2018-01-19 14:55:53 +01:00
daan944 02ca59638a Merge branch 'master' into master 2018-01-19 11:29:47 +01:00
Wyatt Johnson b2cf5efed6 Merge pull request #1297 from coralproject/webpack-performance
Webpack Build Performance Improvements
2018-01-18 18:03:52 -07:00
Wyatt Johnson 7258c3dd08 added performance updates to webpack build 2018-01-18 17:19:19 -07:00
Wyatt Johnson 4bbd561a1b fix to cli 2018-01-18 16:54:27 -07:00
Wyatt Johnson 49d20db26f replaced homegrown with package 2018-01-18 14:22:19 -07:00
Wyatt Johnson 14d035adb1 Merge branch 'master' into user-json 2018-01-18 14:16:49 -07:00
Wyatt Johnson 844ce5dbfe fixed json parse error 2018-01-18 14:15:33 -07:00
Wyatt Johnson 8f1d9f051e added encoding for the user data as json with proper encoding/escaping 2018-01-18 13:58:45 -07:00
Chi Vinh Le a111439541 Only increme once 2018-01-18 20:31:51 +01:00
Chi Vinh Le 72331699b5 Refactor 2018-01-18 20:28:12 +01:00
Chi Vinh Le 544ef081dc Add previous username to notification 2018-01-18 20:09:47 +01:00
Chi Vinh Le ddf3816f5a Better notifications 2018-01-18 20:00:31 +01:00
Chi Vinh Le 7d022ed380 Refactor Community 2018-01-18 19:10:32 +01:00
Chi Vinh Le 40539bf3b7 Set limit to 10 when searching for people 2018-01-18 18:35:29 +01:00
Chi Vinh Le c1a26d2e71 Split community queries 2018-01-18 18:32:49 +01:00
Kim Gardner 6f55189658 Merge branch 'master' into master 2018-01-18 12:08:07 -05:00
Kim Gardner 3c9b4bd20e Merge pull request #1294 from coralproject/close-asset-now
closeAsset Mutation
2018-01-18 12:07:47 -05:00
Chi Vinh Le 2a6543bcea don't query username count twice 2018-01-18 17:43:46 +01:00
Wyatt Johnson 716a61aa5c Merge branch 'master' into master 2018-01-18 09:38:42 -07:00
Wyatt Johnson 127f0c1e20 Merge branch 'master' into close-asset-now 2018-01-18 09:38:24 -07:00
Wyatt Johnson fa83ba9556 Merge pull request #1292 from coralproject/locale-local-storage
Locale local storage
2018-01-18 09:38:12 -07:00
Kim Gardner 317541a63b Merge branch 'master' into close-asset-now 2018-01-18 11:27:12 -05:00
Kim Gardner fd5eb7d23d Merge branch 'master' into locale-local-storage 2018-01-18 11:27:04 -05:00
Kim Gardner 06f308c182 Merge pull request #1290 from coralproject/asset-rewrite
Asset Rewriting
2018-01-18 11:26:02 -05:00
Kim Gardner 2345daf783 Merge branch 'master' into locale-local-storage 2018-01-18 11:21:20 -05:00
Kim Gardner df2bce4d53 Merge branch 'master' into asset-rewrite 2018-01-18 11:19:23 -05:00
Kim Gardner c99411b8fd Merge pull request #1288 from coralproject/reply-on-hidden-comment
Replies to hidden/deleted comments
2018-01-18 11:18:53 -05:00
Kim Gardner 022a57479d Merge branch 'master' into reply-on-hidden-comment 2018-01-18 11:02:08 -05:00
Kim Gardner 56001d7780 Merge pull request #1284 from coralproject/action-index
Action index
2018-01-18 11:01:43 -05:00
Kim Gardner a010302242 Merge branch 'master' into reply-on-hidden-comment 2018-01-18 11:01:36 -05:00
Kim Gardner f306d54924 Merge branch 'master' into action-index 2018-01-18 10:53:45 -05:00
Kim Gardner c1ff7e9c84 Merge branch 'master' into asset-rewrite 2018-01-18 10:53:09 -05:00
Kim Gardner 3255b8d23b Merge branch 'master' into close-asset-now 2018-01-18 10:52:37 -05:00
Kim Gardner 8ea3ab9c02 Merge pull request #1283 from coralproject/formatting
Adding format to admin comment - CommentFormatter
2018-01-18 10:51:50 -05:00
Kim Gardner ec9a57bede Merge branch 'master' into formatting 2018-01-18 10:42:56 -05:00
Chi Vinh Le 5f3da9a09a Ignore own updates 2018-01-18 16:08:46 +01:00
daan944 dbfd0fd25e Merge branch 'master' into master 2018-01-18 14:13:57 +01:00
Wyatt Johnson ac62cbf348 added new closeAsset mutation and support in UI 2018-01-17 16:04:32 -07:00
Wyatt Johnson c612902779 fixed styles 2018-01-17 13:41:02 -07:00
Wyatt Johnson 3938b7883f introduced some locale fixes 2018-01-17 13:24:54 -07:00
Chi Vinh Le cc320cf80a Remove leftover console 2018-01-17 18:03:59 +01:00
Chi Vinh Le 3c52d58a6e Cleanup Dangling 2018-01-17 18:01:42 +01:00
Chi Vinh Le f282aa47c9 Implement dangling 2018-01-17 17:31:48 +01:00
Kim Gardner 426d7dbfec Merge branch 'master' into formatting 2018-01-17 10:06:47 -05:00
Chi Vinh Le ee78f08598 Implement live update for reported users 2018-01-17 15:20:38 +01:00
Chi Vinh Le 519480c0e4 Fix counts not decreasing on mutations 2018-01-17 15:12:17 +01:00
Wyatt Johnson 45d6800143 added dry mode 2018-01-16 17:10:59 -07:00
Wyatt Johnson e3cdad1f2a added asset re-write 2018-01-16 16:47:05 -07:00
Wyatt Johnson 66cfd20f7b Merge branch 'master' into master 2018-01-16 16:00:37 -07:00
Wyatt Johnson af40a85071 Merge branch 'master' into master 2018-01-16 15:58:58 -07:00
Wyatt Johnson b11a730373 Merge branch 'master' into reply-on-hidden-comment 2018-01-16 14:13:13 -07:00
Wyatt Johnson b709cb485e refactored publicCreate 2018-01-16 14:11:24 -07:00
Wyatt Johnson 45d4c79c26 added reply bug 2018-01-16 12:30:38 -07:00
Chi Vinh Le 6aece16fd0 Add userFlagged subscription 2018-01-16 11:31:44 +01:00
okbel 2684ac4933 missing brackets 2018-01-15 13:14:24 -03:00
okbel 1f0e9da5d0 Merge branch 'formatting' of github.com:coralproject/talk into formatting
* 'formatting' of github.com:coralproject/talk:
  prettier
  End of line
  More syncs
  Fix deselect bug of action menu in ff
2018-01-15 13:00:07 -03:00
okbel bfa2f6edb9 Updates 2018-01-15 12:59:39 -03:00
Kiwi ea7998257c Merge branch 'master' into action-index 2018-01-15 16:02:53 +01:00
Wyatt Johnson 25c178b267 Merge branch 'master' into formatting 2018-01-12 17:23:35 -07:00
Wyatt Johnson d69b1f98c8 Merge branch 'master' into action-index 2018-01-12 15:50:02 -07:00
okbel 77d83e04d5 Wrong name 2018-01-12 15:53:32 -03:00
okbel 86c4d70daf Adding changes to the comments on the User Drawer Detail 2018-01-12 15:51:49 -03:00
okbel e256b0da8a Adding format to admin comment - CommentFormatter 2018-01-12 15:47:09 -03:00
Tibo Beijen bf77ddc49b Merge branch 'master' into master 2018-01-12 13:54:31 +01:00
daan944 ebe6341b01 Merge pull request #1 from sanoma/feature/MDN-10616_translation_additions
Feature/mdn 10616 translation additions
2018-01-12 13:45:20 +01:00
d.luijten 34a0e314e8 After discussion: changed status to shorter “Pre-moderatie” 2018-01-12 13:44:29 +01:00
d.luijten 951f16f0a1 Improved email message: ‘je gebruiker’ is always referred to as ‘je account’. Also, simplified ‘Diens inhoud’ to ‘de inhoud van je gebruikersnaam’. 2018-01-12 11:32:48 +01:00
d.luijten 988b39f19d Fixed call-to-action: “Kom in gesprek!” sounds a bit weird to me. 2018-01-12 11:20:29 +01:00
d.luijten 9333b407bb Fixed consistency: Community guidelines are used both with and without capital C. Chose to go for the version without capital C, since it’s not a title. 2018-01-12 11:19:21 +01:00
d.luijten 8867fd54f3 Fixed consistency: “Voor-mod” is never used. “Pre-moderatie” would be acceptable, but “Wachtend op moderatie” sounds more like the correct status message. 2018-01-12 11:14:51 +01:00
d.luijten 56bd3041d6 Fixed consistency: a “bio” has been both named “biografie” and “bio”. Chose “biografie” for now. 2018-01-12 11:11:09 +01:00
d.luijten a53bb6650a Fixed SI units. 2018-01-12 11:10:30 +01:00
d.luijten 0a99ae50b1 Fixed consistency: “Afgewezen wachtrij” implies that this name is used more frequently, but it isn’t. 2018-01-12 11:10:06 +01:00
Tibo Beijen 6bad3fdc2f Dotted some sentences. 2018-01-12 09:09:21 +01:00
Bart Stroeken e5c3474455 dutch translation was not added yet without adding it to i18n.js - and refined a few translations. 2018-01-11 11:56:30 +01:00
Bart Stroeken 1f2caada3f Finished drafting translations. Let's spin it up for a test run 2018-01-11 09:04:45 +01:00
Wyatt Johnson 66f068359f Added index for item_id on Actions. 2018-01-04 14:14:27 -07:00
416 changed files with 11235 additions and 6022 deletions
+24 -22
View File
@@ -1,32 +1,34 @@
**/*.html
dist
docs
**/*.html
node_modules
plugins/*
!plugins/talk-plugin-facebook-auth
public
!plugins/talk-plugin-akismet
!plugins/talk-plugin-auth
!plugins/talk-plugin-respect
!plugins/talk-plugin-offtopic
!plugins/talk-plugin-like
!plugins/talk-plugin-mod
!plugins/talk-plugin-love
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-sort-newest
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-sort-most-replied
!plugins/talk-plugin-flag-details
!plugins/talk-plugin-ignore-user
!plugins/talk-plugin-like
!plugins/talk-plugin-love
!plugins/talk-plugin-member-since
!plugins/talk-plugin-mod
!plugins/talk-plugin-moderation-actions
!plugins/talk-plugin-offtopic
!plugins/talk-plugin-permalink
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-respect
!plugins/talk-plugin-sort-most-liked
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-replied
!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
!plugins/talk-plugin-moderation-actions
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-sort-newest
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
public
node_modules
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-viewing-options
+1
View File
@@ -23,6 +23,7 @@ browserstack.err
plugins.json
plugins/*
!plugins/talk-plugin-akismet
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-auth
!plugins/talk-plugin-respect
-1
View File
@@ -1,6 +1,5 @@
{
"exec": "npm-run-all --parallel generate-introspection start:development",
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql,yml",
"watch": [
+3
View File
@@ -1,6 +1,9 @@
FROM coralproject/talk:latest
# Setup the build arguments
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DEFAULT_LANG=en
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2017 Mozilla Foundation
Copyright 2018 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
-4
View File
@@ -5,7 +5,6 @@ const uuid = require('uuid');
const merge = require('lodash/merge');
const helmet = require('helmet');
const plugins = require('./services/plugins');
const compression = require('compression');
const { HELMET_CONFIGURATION } = require('./config');
const { MOUNT_PATH } = require('./url');
const routes = require('./routes');
@@ -72,9 +71,6 @@ app.use(
)
);
// Compress the responses if appropriate.
app.use(compression());
//==============================================================================
// VIEW CONFIGURATION
//==============================================================================
-18
View File
@@ -18,7 +18,6 @@ program
.command('token', 'work with the access tokens')
.command('users', 'work with the application auth')
.command('migration', 'provides utilities for migrating the database')
.command('verify', 'provides utilities for performing data verification')
.command(
'plugins',
'provides utilities for interacting with the plugin system'
@@ -41,20 +40,3 @@ if (!commands.includes(command)) {
}
process.exit(1);
}
// /**
// * 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');
// }
// });
+73
View File
@@ -14,6 +14,7 @@ const AssetsService = require('../services/assets');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const inquirer = require('inquirer');
const { URL } = require('url');
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
@@ -125,6 +126,70 @@ async function merge(srcID, dstID) {
}
}
async function rewrite(search, replace, options) {
try {
search = new RegExp(search);
const assets = await AssetModel.find({
url: { $regex: search },
});
if (assets.length === 0) {
console.log(`No assets found with the pattern: ${search}`);
return util.shutdown(0);
}
let opts = [];
assets.forEach(({ id, url: oldURL }) => {
// Replace the url.
const newURL = oldURL.replace(search, replace);
// Try to validate that the new url is valid.
try {
new URL(newURL);
} catch (err) {
throw new Error(
`Rewrite would have replaced the valid URL ${oldURL} with an invalid one ${newURL}`
);
}
opts.push({
find: { id },
updateOne: { $set: { url: newURL } },
id,
oldURL,
newURL,
});
});
if (opts.length > 0) {
if (options.dryRun) {
const table = new Table({ head: ['ID', 'Old URL', 'New URL'] });
opts.forEach(({ id, oldURL, newURL }) => {
table.push([id, oldURL, newURL]);
});
console.log(table.toString());
} else {
const bulk = AssetModel.collection.initializeUnorderedBulkOp();
opts.forEach(({ find, updateOne, oldURL, newURL }) => {
// If the url was updated with the operation, then queue up the update op.
if (newURL !== oldURL) {
bulk.find(find).updateOne(updateOne);
}
});
await bulk.execute();
console.log(`${opts.length} assets had their url's updated`);
}
}
util.shutdown(0);
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
@@ -151,6 +216,14 @@ program
)
.action(merge);
program
.command('rewrite <search> <replace>')
.option('-d, --dry-run', 'enables dry run of the replacement')
.description(
"rewrites asset url's using the provided regex replacement pattern"
)
.action(rewrite);
program.parse(process.argv);
// If there is no command listed, output help.
+59 -27
View File
@@ -5,6 +5,7 @@
*/
const util = require('./util');
const _ = require('lodash');
const program = require('commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
@@ -25,46 +26,60 @@ async function createMigration(name) {
}
}
async function runMigrations() {
async function runMigrations(options) {
const { yes, queryBatchSize, updateBatchSize } = options;
try {
let { backedUp } = await inquirer.prompt([
{
type: 'confirm',
name: 'backedUp',
message: 'Did you perform a database backup',
default: false,
},
]);
if (!yes) {
const { backedUp } = await inquirer.prompt([
{
type: 'confirm',
name: 'backedUp',
message: 'Did you perform a database backup',
default: false,
},
]);
if (!backedUp) {
throw new Error(
'Please backup your databases prior to migrations occuring'
);
if (!backedUp) {
throw new Error(
'Please backup your databases prior to migrations occuring'
);
}
}
// Get the migrations to run.
let migrations = await MigrationService.listPending();
const migrations = await MigrationService.listPending();
console.log('Now going to run the following migrations:\n');
for (let { filename } of migrations) {
for (const { filename } of migrations) {
console.log(`\tmigrations/${filename}`);
}
let { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with migrations',
default: false,
},
]);
if (!yes) {
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with migrations',
default: false,
},
]);
if (confirm) {
// Run the migrations.
await MigrationService.run(migrations);
if (confirm) {
// Run the migrations.
await MigrationService.run(migrations, {
queryBatchSize,
updateBatchSize,
});
} else {
console.warn('Skipping migrations');
}
} else {
console.warn('Skipping migrations');
// Run the migrations.
await MigrationService.run(migrations, {
queryBatchSize,
updateBatchSize,
});
}
util.shutdown();
@@ -83,8 +98,25 @@ program
.description('creates a new migration')
.action(createMigration);
// Bypasses issue that defaults + coercion doesn't work well together.
// Ref: https://github.com/tj/commander.js/issues/400#issuecomment-310860869
const parse10 = _.ary(_.partialRight(parseInt, 10), 1);
program
.command('run')
.option(
'-q, --query-batch-size <n>',
'change the size of queried documents that are batched at a time',
parse10,
10000
)
.option(
'-u, --update-batch-size <n>',
'change the size of documents that are batched before the update is sent',
parse10,
20000
)
.option('-y, --yes', 'will answer yes to all questions')
.description('runs all pending migrations')
.action(runMigrations);
+45 -43
View File
@@ -15,9 +15,9 @@ const inquirer = require('inquirer');
require('colors');
const emoji = require('node-emoji');
const dir = process.cwd();
const fs = require('fs-extra');
const path = require('path');
const dir = path.resolve(__dirname, '..');
const spawn = require('cross-spawn');
const semver = require('semver');
const resolve = require('resolve');
@@ -235,7 +235,7 @@ async function reconcileLocalPlugins({ skipRemote, dryRun }) {
if (output.status) {
throw new Error(
'Could not install local plugin dependencies, errors occured during install'
'Could not install local plugin dependencies, errors occurred during install'
);
}
@@ -253,59 +253,61 @@ async function reconcilePluginDeps({
dryRun,
upgradeRemote,
}) {
let startTime = new Date();
try {
let startTime = new Date();
// We don't need to do anything if we skip everything....
if (skipLocal && skipRemote) {
return;
}
// We don't need to do anything if we skip everything....
if (skipLocal && skipRemote) {
return;
}
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({ skipRemote, dryRun });
}
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({ skipRemote, dryRun });
}
// Locate any external plugins and install them.
if (!skipRemote) {
let results = [];
try {
results = await reconcileRemotePlugins({
// Locate any external plugins and install them.
if (!skipRemote) {
const results = await reconcileRemotePlugins({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
});
} catch (e) {
throw e;
let status;
if (dryRun) {
status = '[dry-run] success'.green;
} else {
status = 'success'.green;
}
let message;
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
message = 'Already up-to-date.';
} else if (results.upgradable.length === 0) {
message = `Fetched ${results.fetchable.length} new plugins.`;
} 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.`;
}
console.log(`\n${status} ${message}`);
}
let status;
if (dryRun) {
status = '[dry-run] success'.green;
} else {
status = 'success'.green;
}
let endTime = new Date();
let message;
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
message = 'Already up-to-date.';
} else if (results.upgradable.length === 0) {
message = `Fetched ${results.fetchable.length} new plugins.`;
} 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.`;
}
console.log(`\n${status} ${message}`);
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
2
);
console.log(`✨ Done in ${totalTime}s.`);
} catch (err) {
console.error(err);
process.exit(1);
}
let endTime = new Date();
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
console.log(`✨ Done in ${totalTime}s.`);
}
async function createSeedPlugin() {
+8 -1
View File
@@ -13,6 +13,7 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
const SettingsService = require('../services/settings');
const SetupService = require('../services/setup');
const UsersService = require('../services/users');
const MigrationService = require('../services/migration');
const errors = require('../errors');
// Register the shutdown criteria.
@@ -51,6 +52,12 @@ const performSetup = async () => {
if (program.defaults) {
await SettingsService.init();
// Get the migrations to run.
let migrations = await MigrationService.listPending();
// Perform all migrations.
await MigrationService.run(migrations);
console.log('Settings created.');
console.log('\nTalk is now installed!');
@@ -194,7 +201,7 @@ const performSetup = async () => {
);
};
// Start tthe setup process.
// Start the setup process.
performSetup()
.then(() => {
util.shutdown();
+60 -9
View File
@@ -8,6 +8,7 @@ const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const { graphql } = require('graphql');
const helpers = require('../services/migration/helpers');
const { stripIndent } = require('common-tags');
const Table = require('cli-table');
@@ -28,11 +29,23 @@ 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');
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
/**
* transforms a specific action to a removal action on the target model.
*/
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
query: { id: item_id },
update: {
$inc: {
[`action_counts.${action_type.toLowerCase()}`]: -1,
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
},
},
});
/**
* Deletes a user and cleans up their associated verifications.
*/
@@ -63,8 +76,27 @@ async function deleteUser(userID) {
return util.shutdown();
}
const { transformSingleWithCursor } = helpers({
queryBatchSize: 10000,
updateBatchSize: 10000,
});
console.warn("Removing user's actions");
// Remove all actions against comments.
await transformSingleWithCursor(
ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
actionDecrTransformer,
CommentModel
);
// Remove all actions against users.
await transformSingleWithCursor(
ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }),
actionDecrTransformer,
UserModel
);
// Remove all the user's actions.
await ActionModel.where({ user_id: user.id })
.setOptions({ multi: true })
@@ -72,18 +104,34 @@ async function deleteUser(userID) {
console.warn("Removing user's comments");
// Removes all the user's reply counts on each of the comments that they
// have commented on.
await transformSingleWithCursor(
CommentModel.collection.aggregate([
{ $match: { author_id: user.id } },
{
$group: {
_id: '$parent_id',
count: { $sum: 1 },
},
},
]),
({ _id: parent_id, count }) => ({
query: { id: parent_id },
update: {
$inc: {
reply_count: -1 * count,
},
},
}),
CommentModel
);
// 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.
@@ -161,7 +209,10 @@ async function searchUsers() {
}
return data.users.nodes.map(user => {
const emails = user.emails.join(', ');
const emails = user.profiles
.filter(({ provider }) => provider === 'local')
.map(({ id }) => id)
.join(', ');
return {
name: `${user.username} (${emails}) ${user.id.gray} - ${
user.role.gray
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
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()]);
async function database({ fix = false, limit = Infinity, batch = 1000 }) {
try {
for (const verification of databaseVerifications) {
await verification({ fix, limit, batch });
}
} catch (err) {
console.error(
`Failed to process all the ${databaseVerifications.length} verifications`,
err
);
util.shutdown(1);
return;
}
util.shutdown();
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('db')
.description('verifies the database integrity')
.option('-f, --fix', 'fix the problems found with database inconsistencies')
.option(
'-l, --limit [size]',
'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]',
parseInt
)
.option(
'-b, --batch [size]',
'batch size to process verifications and repairs of documents [default: 1000]',
parseInt
)
.action(database);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
util.shutdown();
}
+2 -1
View File
@@ -63,5 +63,6 @@ process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
// 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;
console.error(err);
process.exit(1);
});
-194
View File
@@ -1,194 +0,0 @@
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'
);
}
}
}
};
@@ -1,140 +0,0 @@
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'
);
}
}
};
-10
View File
@@ -1,10 +0,0 @@
// This will import all the verifications that should be run by the:
//
// cli verify database
//
// command. They exist in the form:
//
// async ({fix = false, batch = 1000}) => {}
//
// where their options are derived.
module.exports = [require('./comment_replies'), require('./action_counts')];
+1 -3
View File
@@ -37,7 +37,7 @@ dependencies:
# Install node dependencies.
- yarn --version
- yarn global add node-gyp nsp --force
- yarn global add node-gyp --force
- yarn
post:
@@ -59,8 +59,6 @@ test:
- yarn test
# Run the end to end tests
- yarn e2e:ci
# Check dependancies using nsp.
- nsp check
deployment:
release:
+1 -1
View File
@@ -5,7 +5,7 @@ import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
import Configure from 'routes/Configure';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community/containers/Community';
import Community from 'routes/Community';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
-174
View File
@@ -1,174 +0,0 @@
import bowser from 'bowser';
import * as actions from '../constants/auth';
import t from 'coral-framework/services/i18n';
import jwtDecode from 'jwt-decode';
//==============================================================================
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (
dispatch,
_,
{ rest, client, storage }
) => {
dispatch({ type: actions.LOGIN_REQUEST });
const params = {
method: 'POST',
body: {
email,
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse,
};
}
return rest('/auth/local', params)
.then(({ user, token }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
}
return dispatch(checkLoginFailure('not logged in'));
}
dispatch(handleAuthToken(token));
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch(error => {
console.error(error);
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'),
});
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: t(`error.${error.translation_key}`),
});
} else {
dispatch({
type: actions.LOGIN_FAILURE,
message: errorMessage,
});
}
});
};
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
});
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
});
const forgotPasswordFailure = error => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return rest('/account/password/reset', {
method: 'POST',
body: { email, loc: redirectUri },
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin,
});
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error,
});
export const checkLogin = () => (dispatch, _, { rest, client, storage }) => {
dispatch(checkLoginRequest());
return rest('/auth')
.then(({ user }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
}
return dispatch(checkLoginFailure('not logged in'));
}
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
//==============================================================================
// LOGOUT
//==============================================================================
export const logout = () => (dispatch, _, { rest, client, storage }) => {
return rest('/auth', { method: 'DELETE' }).then(() => {
if (storage) {
storage.removeItem('token');
}
// Reset the websocket.
client.resetWebsocket();
dispatch({ type: actions.LOGOUT });
});
};
//==============================================================================
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = token => (dispatch, _, { storage }) => {
if (storage) {
storage.setItem('exp', jwtDecode(token).exp);
storage.setItem('token', token);
}
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
};
@@ -11,6 +11,7 @@ import {
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG,
SET_INDICATOR_TRACK,
} from '../constants/community';
import t from 'coral-framework/services/i18n';
@@ -68,3 +69,9 @@ export const showRejectUsernameDialog = user => ({
export const hideRejectUsernameDialog = () => ({
type: HIDE_REJECT_USERNAME_DIALOG,
});
// Enable or disable the activity indicator subscriptions.
export const setIndicatorTrack = track => ({
type: SET_INDICATOR_TRACK,
track,
});
-7
View File
@@ -1,7 +0,0 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => dispatch => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({ type: CONFIG_UPDATED, data });
};
+11 -5
View File
@@ -4,10 +4,10 @@ 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, _, { localStorage }) => {
try {
if (storage) {
storage.setItem('coral:shortcutsNote', 'hide');
if (localStorage) {
localStorage.setItem('coral:shortcutsNote', 'hide');
}
} catch (e) {
// above will fail in Safari private mode
@@ -31,10 +31,16 @@ export const storySearchChange = value => ({
});
export const clearState = () => ({
type: actions.MODERATION_CLEAR_STATE,
type: actions.CLEAR_STATE,
});
export const selectCommentId = id => ({
type: actions.MODERATION_SELECT_COMMENT,
type: actions.SELECT_COMMENT,
id,
});
// Enable or disable the activity indicator subscriptions.
export const setIndicatorTrack = track => ({
type: actions.SET_INDICATOR_TRACK,
track,
});
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import Layout from 'coral-admin/src/components/Layout';
import styles from './NotFound.css';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
@@ -5,23 +5,22 @@
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
width: 100%;
margin: 0 0 .5em;
&:hover {
&:not(:disabled):hover {
box-shadow: none;
color: white;
background-color: #519954;
cursor: pointer;
}
&:disabled {
cursor: not-allowed;
}
}
.active {
@@ -7,7 +7,7 @@ import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({ active, minimal, onClick, className }) => {
const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
@@ -17,6 +17,7 @@ const ApproveButton = ({ active, minimal, onClick, className }) => {
className
)}
onClick={onClick}
disabled={disabled || active}
>
<Icon name={'done'} className={styles.icon} />
{!minimal && text}
@@ -28,6 +29,7 @@ ApproveButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
};
@@ -4,7 +4,7 @@ import { CSSTransitionGroup } from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
import PropTypes from 'prop-types';
const CommentBodyHighlighter = ({ children, body }) => {
const CommentAnimatedEdit = ({ children, body }) => {
return (
<CSSTransitionGroup
component={'div'}
@@ -27,9 +27,9 @@ const CommentBodyHighlighter = ({ children, body }) => {
);
};
CommentBodyHighlighter.propTypes = {
CommentAnimatedEdit.propTypes = {
children: PropTypes.node,
body: PropTypes.string,
};
export default CommentBodyHighlighter;
export default CommentAnimatedEdit;
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { matchLinks } from '../utils';
import memoize from 'lodash/memoize';
@@ -62,16 +63,47 @@ function markLinks(body) {
return content;
}
export default ({ suspectWords, bannedWords, body, ...rest }) => {
// First highlight links.
const content = markLinks(body).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
const CommentFormatter = ({
body,
suspectWords,
bannedWords,
className = 'comment',
...rest
}) => {
// Breaking the body by line break
const textbreaks = body.split('\n');
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return <div {...rest}>{content}</div>;
return (
<span className={`${className}-text`} {...rest}>
{textbreaks.map((line, i) => {
const content = markLinks(line).map((element, index) => {
// 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 (
<span key={i} className={`${className}-line`}>
{content}
{i !== textbreaks.length - 1 && (
<br className={`${className}-linebreak`} />
)}
</span>
);
})}
</span>
);
};
CommentFormatter.propTypes = {
className: PropTypes.string,
bannedWords: PropTypes.array,
suspectWords: PropTypes.array,
body: PropTypes.string,
};
export default CommentFormatter;
@@ -5,8 +5,9 @@ import Slot from 'coral-framework/components/Slot';
import FlagLabel from 'coral-ui/components/FlagLabel';
import cn from 'classnames';
import styles from './CommentLabels.css';
import { ADMIN, MODERATOR, STAFF } from 'coral-framework/constants/roles';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
const staffRoles = [ADMIN, MODERATOR, STAFF];
function isUserFlagged(actions) {
return actions.some(
+6 -6
View File
@@ -7,12 +7,12 @@ import t from 'coral-framework/services/i18n';
import { can } from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({ handleLogout, auth = {} }) => (
const CoralDrawer = ({ handleLogout, currentUser }) => (
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
<div>
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
{can(currentUser, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
@@ -35,7 +35,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
>
{t('configure.community')}
</Link>
{can(auth.user, 'UPDATE_CONFIG') && (
{can(currentUser, 'UPDATE_CONFIG') && (
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
@@ -44,7 +44,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
{t('configure.configure')}
</Link>
)}
<a onClick={handleLogout}>Sign Out</a>
<a onClick={handleLogout}>{t('configure.sign_out')}</a>
<span>{`v${process.env.VERSION}`}</span>
</Navigation>
</div>
@@ -55,7 +55,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool, // hide app elements from a logged out user
auth: PropTypes.object,
currentUser: PropTypes.object,
};
export default CoralDrawer;
@@ -0,0 +1,20 @@
.header, .cta, .success {
text-align: center;
font-size: 16px;
}
.success {
cursor: pointer;
padding: 8px 14px;
}
.signInLink {
color: blue;
font-weight: normal;
text-decoration: none;
}
.signInLink:hover {
text-decoration: underline;
}
@@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ForgotPassword.css';
import { Button, TextField, Alert, Success } from 'coral-ui';
import t from 'coral-framework/services/i18n';
class ForgotPassword extends React.Component {
constructor(props) {
super(props);
}
handleEmailChange = e => this.props.onEmailChange(e.target.value);
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
};
handleSignInLink = e => {
e.preventDefault();
this.props.onSignInLink();
};
renderSuccess() {
return (
<div className={styles.success} onClick={this.handleSignInLink}>
{t('password_reset.mail_sent')}{' '}
<a
className={styles.signInLink}
href="#"
onClick={this.handleSignInLink}
>
Sign in
</a>
<Success />
</div>
);
}
renderForm() {
const { email, errorMessage } = this.props;
return (
<form onSubmit={this.handleSubmit}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
label="Email Address"
value={email}
onChange={this.handleEmailChange}
/>
<Button type="submit" cStyle="black" full>
Reset Password
</Button>
<p className={styles.cta}>
Go back to{' '}
<a
href="#"
className={styles.signInLink}
onClick={this.handleSignInLink}
>
Sign In
</a>
.
</p>
</form>
);
}
render() {
return this.props.success ? this.renderSuccess() : this.renderForm();
}
}
ForgotPassword.propTypes = {
success: PropTypes.bool.isRequired,
email: PropTypes.string.isRequired,
onEmailChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
onSignInLink: PropTypes.func.isRequired,
};
export default ForgotPassword;
@@ -7,22 +7,24 @@ import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import Indicator from './Indicator';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import CommunityIndicator from '../routes/Community/containers/Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
currentUser,
root,
data,
}) => {
return (
<div className={styles.headerWrapper}>
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
{can(currentUser, 'MODERATE_COMMENTS') && (
<IndexLink
id="moderateNav"
className={cn('talk-admin-nav-moderate', styles.navLink)}
@@ -30,9 +32,7 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && (
<Indicator />
)}
<ModerationIndicator root={root} data={data} />
</IndexLink>
)}
<Link
@@ -51,10 +51,10 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
<CommunityIndicator root={root} data={data} />
</Link>
{can(auth.user, 'UPDATE_CONFIG') && (
{can(currentUser, 'UPDATE_CONFIG') && (
<Link
id="configureNav"
className={cn('talk-admin-nav-configure', styles.navLink)}
@@ -97,12 +97,14 @@ const CoralHeader = ({
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
{currentUser && (
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
)}
</Menu>
</div>
</li>
@@ -116,10 +118,11 @@ const CoralHeader = ({
};
CoralHeader.propTypes = {
auth: PropTypes.object,
currentUser: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
};
export default CoralHeader;
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Layout as LayoutMDL } from 'react-mdl';
import Header from '../../containers/Header';
import Drawer from '../Drawer';
import Header from '../containers/Header';
import Drawer from './Drawer';
import styles from './Layout.css';
const Layout = ({
@@ -10,22 +10,26 @@ const Layout = ({
handleLogout = () => {},
toggleShortcutModal = () => {},
restricted = false,
auth,
currentUser,
}) => (
<LayoutMDL className={styles.layout} fixedDrawer>
<Header
handleLogout={handleLogout}
showShortcuts={toggleShortcutModal}
auth={auth}
currentUser={currentUser}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
currentUser={currentUser}
/>
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
<div className={styles.layout}>{children}</div>
</LayoutMDL>
);
Layout.propTypes = {
children: PropTypes.node,
auth: PropTypes.object,
currentUser: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool, // hide elements from a user that's logged out
@@ -0,0 +1,18 @@
.layout {
max-width: 400px;
margin: 0 auto;
}
.header, .cta {
text-align: center;
font-size: 16px;
}
.layout h1 {
font-size: 40px;
}
.header {
font-size: 30px;
}
@@ -0,0 +1,37 @@
import React, { Component } from 'react';
import SignIn from '../containers/SignIn';
import ForgotPassword from '../containers/ForgotPassword';
import PropTypes from 'prop-types';
import styles from './Login.css';
import Layout from 'coral-admin/src/components/Layout';
import cn from 'classnames';
class LoginContainer extends Component {
renderForm() {
return this.props.forgotPassword ? (
<ForgotPassword onSignInLink={this.props.onSignInLink} />
) : (
<SignIn onForgotPasswordLink={this.props.onForgotPasswordLink} />
);
}
render() {
return (
<Layout fixedDrawer restricted={true}>
<div className={cn(styles.layout, 'talk-admin-login')}>
<h1 className={styles.header}>Team sign in</h1>
<p className={styles.cta}>Sign in to interact with your community.</p>
{this.renderForm()}
</div>
</Layout>
);
}
}
LoginContainer.propTypes = {
forgotPassword: PropTypes.bool.isRequired,
onForgotPasswordLink: PropTypes.func.isRequired,
onSignInLink: PropTypes.func.isRequired,
};
export default LoginContainer;
@@ -23,7 +23,7 @@ export default class ModerationKeysModal extends React.Component {
'ctrl+f': 'modqueue.toggle_search',
t: 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
s: 'modqueue.singleview',
z: 'modqueue.singleview',
'?': 'modqueue.thismenu',
},
},
@@ -3,35 +3,3 @@
margin: 0 auto;
}
.loginLayout {
max-width: 400px;
margin: 0 auto;
}
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
text-align: center;
font-size: 16px;
}
.forgotPasswordLink, .signInLink {
color: blue;
font-weight: normal;
text-decoration: none;
}
.forgotPasswordLink:hover, .signInLink:hover {
text-decoration: underline;
}
.layout h1 {
font-size: 40px;
}
.loginHeader {
font-size: 30px;
}
.passwordRequestSuccess {
cursor: pointer;
padding: 8px 14px;
}
@@ -5,23 +5,22 @@
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
width: 100%;
margin: 0 0 .5em;
&:hover {
&:not(:disabled):hover {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: pointer;
}
&:disabled {
cursor: not-allowed;
}
}
.active {
@@ -7,7 +7,7 @@ import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({ active, minimal, onClick, className }) => {
const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
@@ -17,6 +17,7 @@ const RejectButton = ({ active, minimal, onClick, className }) => {
className
)}
onClick={onClick}
disabled={disabled || active}
>
<Icon name={'close'} className={styles.icon} />
{!minimal && text}
@@ -29,6 +30,7 @@ RejectButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
disabled: PropTypes.bool,
};
export default RejectButton;
@@ -0,0 +1,23 @@
.forgotPasswordCTA {
text-align: center;
font-size: 16px;
}
.forgotPasswordLink:hover {
text-decoration: underline;
}
.forgotPasswordLink {
color: blue;
font-weight: normal;
text-decoration: none;
}
.recaptcha {
margin-top: 16px;
margin-bottom: 6px;
}
.signInButton {
margin-top: 10px;
}
@@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './SignIn.css';
import { Button, TextField, Alert } from 'coral-ui';
import cn from 'classnames';
import Recaptcha from 'coral-framework/components/Recaptcha';
class SignIn extends React.Component {
recaptcha = null;
handleForgotPasswordLink = e => {
e.preventDefault();
this.props.onForgotPasswordLink();
};
handleEmailChange = e => this.props.onEmailChange(e.target.value);
handlePasswordChange = e => this.props.onPasswordChange(e.target.value);
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
// Reset recaptcha because each response can only
// be used once.
if (this.recaptcha) {
this.recaptcha.reset();
}
};
handleRecaptchaRef = ref => {
this.recaptcha = ref;
};
render() {
const { email, password, errorMessage, requireRecaptcha } = this.props;
return (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label="Email Address"
value={email}
onChange={this.handleEmailChange}
/>
<TextField
id="password"
label="Password"
value={password}
onChange={this.handlePasswordChange}
type="password"
/>
{requireRecaptcha && (
<div className={styles.recaptcha}>
<Recaptcha
ref={this.handleRecaptchaRef}
onVerify={this.props.onRecaptchaVerify}
/>
</div>
)}
<Button
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
type="submit"
cStyle="black"
full
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={this.handleForgotPasswordLink}
>
Request a new one.
</a>
</p>
</form>
);
}
}
SignIn.propTypes = {
email: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
onEmailChange: PropTypes.func.isRequired,
onPasswordChange: PropTypes.func.isRequired,
onForgotPasswordLink: PropTypes.func.isRequired,
onRecaptchaVerify: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
requireRecaptcha: PropTypes.bool.isRequired,
};
export default SignIn;
+45 -42
View File
@@ -2,7 +2,6 @@ import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import capitalize from 'lodash/capitalize';
import { getErrorMessages } from 'coral-framework/utils';
import styles from './UserDetail.css';
import AccountHistory from './AccountHistory';
import { Slot } from 'coral-framework/components';
@@ -26,46 +25,27 @@ import {
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
import t from 'coral-framework/services/i18n';
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));
}
await this.props.rejectComment(info);
this.props.data.refetch();
};
acceptThenReload = async info => {
try {
await this.props.acceptComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
await this.props.acceptComment(info);
this.props.data.refetch();
};
bulkAcceptThenReload = async () => {
try {
await this.props.bulkAccept();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
await this.props.bulkAccept();
this.props.data.refetch();
};
bulkRejectThenReload = async () => {
try {
await this.props.bulkReject();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
await this.props.bulkReject();
this.props.data.refetch();
};
changeTab = tab => {
@@ -94,6 +74,16 @@ class UserDetail extends React.Component {
);
}
renderError() {
return (
<ClickOutside onClickOutside={this.props.hideUserDetail}>
<Drawer onClose={this.props.hideUserDetail}>
<div>{this.props.data.error.message}</div>
</Drawer>
</ClickOutside>
);
}
getActionMenuLabel() {
const { root: { user } } = this.props;
@@ -163,27 +153,27 @@ class UserDetail extends React.Component {
>
{suspended ? (
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
Remove Suspension
{t('user_detail.remove_suspension')}
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}
>
Suspend User
{t('user_detail.suspend')}
</ActionsMenuItem>
)}
{banned ? (
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
Remove Ban
{t('user_detail.remove_ban')}
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}
>
Ban User
{t('user_detail.ban')}
</ActionsMenuItem>
)}
</ActionsMenu>
@@ -201,14 +191,18 @@ class UserDetail extends React.Component {
<ul className={styles.userDetailList}>
<li>
<Icon name="assignment_ind" />
<span className={styles.userDetailItem}>Member Since:</span>
<span className={styles.userDetailItem}>
{t('user_detail.member_since')}:
</span>
{new Date(user.created_at).toLocaleString()}
</li>
{user.profiles.map(({ id }) => (
<li key={id}>
<Icon name="email" />
<span className={styles.userDetailItem}>Email:</span>
<span className={styles.userDetailItem}>
{t('user_detail.email')}:
</span>
{id}{' '}
<ButtonCopyToClipboard
className={styles.copyButton}
@@ -221,17 +215,23 @@ class UserDetail extends React.Component {
<ul className={styles.stats}>
<li className={styles.stat}>
<span className={styles.statItem}>Total Comments</span>
<span className={styles.statItem}>
{t('user_detail.total_comments')}
</span>
<span className={styles.statResult}>{totalComments}</span>
</li>
<li className={styles.stat}>
<span className={styles.statItem}>Reject Rate</span>
<span className={styles.statItem}>
{t('user_detail.reject_rate')}
</span>
<span className={styles.statResult}>
{rejectedPercent.toFixed(1)}%
</span>
</li>
<li className={styles.stat}>
<span className={styles.statItem}>Reports</span>
<span className={styles.statItem}>
{t('user_detail.reports')}
</span>
<span
className={cn(
styles.statReportResult,
@@ -270,13 +270,13 @@ class UserDetail extends React.Component {
'talk-admin-user-detail-all-tab'
)}
>
All
{t('user_detail.all')}
</Tab>
<Tab
tabId={'rejected'}
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
>
Rejected
{t('user_detail.rejected')}
</Tab>
<Tab
tabId={'history'}
@@ -286,7 +286,7 @@ class UserDetail extends React.Component {
'talk-admin-user-detail-history-tab'
)}
>
Account History
{t('user_detail.account_history')}
</Tab>
</TabBar>
@@ -345,6 +345,10 @@ class UserDetail extends React.Component {
}
render() {
if (this.props.data.error) {
return this.renderError();
}
if (this.props.loading) {
return this.renderLoading();
}
@@ -371,7 +375,6 @@ UserDetail.propTypes = {
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,
@@ -81,6 +81,8 @@
}
.sideActions {
/* IE 11 needs this... */
width: 105px;
}
.editedMarker {
@@ -5,7 +5,7 @@ import { Link } from 'react-router';
import { Icon } from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import CommentAnimatedEdit from './CommentAnimatedEdit';
@@ -78,11 +78,12 @@ class UserDetailComment extends React.Component {
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
<div className={styles.body}>
<CommentBodyHighlighter
<CommentFormatter
suspectWords={suspect}
bannedWords={banned}
body={comment.body}
/>{' '}
className="talk-admin-user-detail-comment"
/>
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -18,3 +18,5 @@ export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALO
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
+2 -2
View File
@@ -2,8 +2,8 @@ export const NEXT_STEP = 'NEXT_STEP';
export const GO_TO_STEP = 'GO_TO_STEP';
export const PREVIOUS_STEP = 'PREVIOUS_STEP';
export const ADD_ERROR = 'ADD_ERROR';
export const HAS_ERROR = 'HAS_ERROR';
export const SHOW_ERRORS = 'SHOW_ERRORS';
export const HAS_ERROR = 'HAS_ERROR';
export const SHOW_ERRORS = 'SHOW_ERRORS';
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
export const INSTALL_REQUEST = 'INSTALL_REQUEST';
export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
+12 -9
View File
@@ -1,9 +1,12 @@
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
export const SINGLE_VIEW = 'SINGLE_VIEW';
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
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';
const prefix = `MODERATION`;
export const TOGGLE_MODAL = `${prefix}_TOGGLE_MODAL`;
export const SINGLE_VIEW = `${prefix}_SINGLE_VIEW`;
export const HIDE_SHORTCUTS_NOTE = `${prefix}_HIDE_SHORTCUTS_NOTE`;
export const SET_SORT_ORDER = `${prefix}_SET_SORT_ORDER`;
export const SHOW_STORY_SEARCH = `${prefix}_SHOW_STORY_SEARCH`;
export const HIDE_STORY_SEARCH = `${prefix}_HIDE_STORY_SEARCH`;
export const STORY_SEARCH_CHANGE_VALUE = `${prefix}_STORY_SEARCH_CHANGE_VALUE`;
export const CLEAR_STATE = `${prefix}_CLEAR_STATE`;
export const SELECT_COMMENT = `${prefix}_SELECT_COMMENT`;
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
@@ -10,8 +10,6 @@ import {
} 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';
class BanUserDialogContainer extends Component {
banUser = async () => {
@@ -22,16 +20,11 @@ class BanUserDialogContainer extends Component {
banUser,
setCommentStatus,
hideBanUserDialog,
notify,
} = this.props;
try {
await banUser({ id: userId, message: '' });
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({ commentId, status: 'REJECTED' });
}
} catch (err) {
notify('error', getErrorMessages(err));
await banUser({ id: userId, message: '' });
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({ commentId, status: 'REJECTED' });
}
};
@@ -78,14 +71,13 @@ const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideBanUserDialog,
notify,
},
dispatch
),
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withBanUser,
withSetCommentStatus,
connect(mapStateToProps, mapDispatchToProps)
withSetCommentStatus
)(BanUserDialogContainer);
@@ -25,6 +25,7 @@ export default withFragments({
id
role
}
created_at
}
${getSlotFragmentSpreads(slots, 'comment')}
}
@@ -0,0 +1,41 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withForgotPassword } from 'coral-framework/hocs';
import { compose } from 'recompose';
import ForgotPassword from '../components/ForgotPassword';
class ForgotPasswordContainer extends Component {
state = {
email: '',
};
handleSubmit = () => {
this.props.forgotPassword(this.state.email);
};
handleEmailChange = email => {
this.setState({ email });
};
render() {
return (
<ForgotPassword
onSubmit={this.handleSubmit}
onEmailChange={this.handleEmailChange}
email={this.state.email}
errorMessage={this.props.errorMessage}
success={this.props.success}
onSignInLink={this.props.onSignInLink}
/>
);
}
}
ForgotPasswordContainer.propTypes = {
success: PropTypes.bool.isRequired,
forgotPassword: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
onSignInLink: PropTypes.func.isRequired,
};
export default compose(withForgotPassword)(ForgotPasswordContainer);
+10 -14
View File
@@ -1,26 +1,22 @@
import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/ui/Header';
import Header from '../components/Header';
import CommunityIndicator from '../routes/Community/containers/Indicator';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import { getDefinitionName } from 'coral-framework/utils';
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] } }
}
)
query TalkAdmin_Header($nullID: ID) {
...${getDefinitionName(ModerationIndicator.fragments.root)}
...${getDefinitionName(CommunityIndicator.fragments.root)}
}
${ModerationIndicator.fragments.root}
${CommunityIndicator.fragments.root}
`,
{
options: {
pollInterval: 10000,
variables: { nullID: null },
},
}
)(Header);
+36 -73
View File
@@ -1,87 +1,59 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/ui/Layout';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import Layout from '../components/Layout';
import Login from '../containers/Login';
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 { logout } from 'coral-framework/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;
checkLogin();
fetchConfig();
}
render() {
const {
user,
loggedIn,
loadingUser,
loginError,
loginMaxExceeded,
passwordRequestSuccess,
} = this.props.auth;
const {
currentUser,
checkedInitialLogin,
children,
logout,
toggleShortcutModal,
TALK_RECAPTCHA_PUBLIC,
} = this.props;
if (loadingUser) {
if (!checkedInitialLogin) {
return <FullLoading />;
}
if (!loggedIn) {
return (
<AdminLogin
loginMaxExceeded={loginMaxExceeded}
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError}
/>
);
if (!currentUser) {
return <Login />;
}
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
return (
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
auth={this.props.auth}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{children}
</Layout>
);
} 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>
</Layout>
);
if (currentUser) {
if (can(currentUser, 'ACCESS_ADMIN')) {
return (
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
currentUser={this.props.currentUser}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{children}
</Layout>
);
} else {
return (
<Layout {...this.props} handleLogout={logout}>
<p>
This page is for team use only. Please contact an administrator if
you want to join this team.
</p>
</Layout>
);
}
}
return <FullLoading />;
}
@@ -89,29 +61,20 @@ class LayoutContainer extends React.Component {
LayoutContainer.propTypes = {
children: PropTypes.node,
requestPasswordReset: PropTypes.func,
handleLogin: PropTypes.func,
auth: PropTypes.object,
handleLogout: PropTypes.func,
currentUser: PropTypes.object,
checkedInitialLogin: PropTypes.bool,
logout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
checkLogin: PropTypes.func,
fetchConfig: PropTypes.func,
};
const mapStateToProps = state => ({
auth: state.auth,
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
currentUser: state.auth.user,
checkedInitialLogin: state.auth.checkedInitialLogin,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout,
},
@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import Login from '../components/Login';
class LoginContainer extends Component {
state = {
forgotPassword: false,
};
switchToForgotPassword = () => {
this.setState({ forgotPassword: true });
};
switchToSignIn = () => {
this.setState({ forgotPassword: false });
};
render() {
return (
<Login
forgotPassword={this.state.forgotPassword}
onForgotPasswordLink={this.switchToForgotPassword}
onSignInLink={this.switchToSignIn}
/>
);
}
}
LoginContainer.propTypes = {};
export default LoginContainer;
@@ -0,0 +1,58 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withSignIn } from 'coral-framework/hocs';
import { compose } from 'recompose';
import SignIn from '../components/SignIn';
class SignInContainer extends Component {
state = {
email: '',
password: '',
recaptchaResponse: null,
};
handleSubmit = () => {
this.props.signIn(
this.state.email,
this.state.password,
this.state.recaptchaResponse
);
};
handleEmailChange = email => {
this.setState({ email });
};
handlePasswordChange = password => {
this.setState({ password });
};
handleRecaptchaVerify = recaptchaResponse => {
this.setState({ recaptchaResponse });
};
render() {
return (
<SignIn
onSubmit={this.handleSubmit}
onEmailChange={this.handleEmailChange}
onPasswordChange={this.handlePasswordChange}
email={this.state.email}
password={this.state.password}
errorMessage={this.props.errorMessage}
onForgotPasswordLink={this.props.onForgotPasswordLink}
onRecaptchaVerify={this.handleRecaptchaVerify}
requireRecaptcha={this.props.requireRecaptcha}
/>
);
}
}
SignInContainer.propTypes = {
signIn: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
onForgotPasswordLink: PropTypes.func.isRequired,
requireRecaptcha: PropTypes.bool.isRequired,
};
export default compose(withSignIn)(SignInContainer);
@@ -11,7 +11,6 @@ import {
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 get from 'lodash/get';
import { notify } from 'coral-framework/actions/notification';
@@ -28,17 +27,13 @@ class SuspendUserDialogContainer extends Component {
notify,
} = this.props;
hideSuspendUserDialog();
try {
await suspendUser({ id: userId, message, until });
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until))
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({ commentId, status: 'REJECTED' });
}
} catch (err) {
notify('error', getErrorMessages(err));
await suspendUser({ id: userId, message, until });
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until))
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({ commentId, status: 'REJECTED' });
}
};
@@ -24,7 +24,6 @@ import {
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import { notify } from 'coral-framework/actions/notification';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
@@ -130,6 +129,7 @@ class UserDetailContainer extends React.Component {
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
error={this.props.data && this.props.data.error}
loadMore={this.loadMore}
{...this.props}
/>
@@ -271,7 +271,6 @@ const mapDispatchToProps = dispatch => ({
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify,
},
dispatch
),
+97 -4
View File
@@ -1,6 +1,7 @@
import update from 'immutability-helper';
import { mapLeaves } from 'coral-framework/utils';
import { gql } from 'react-apollo';
import get from 'lodash/get';
const userStatusFragment = gql`
fragment Talk_UpdateUserStatus on User {
@@ -163,9 +164,26 @@ export default {
},
}),
ApproveUsername: ({ variables: { id } }) => ({
optimisticResponse: {
approveUsername: {
__typename: 'ApproveUsernameResponse',
errors: null,
isOptimistic: true,
},
},
updateQueries: {
TalkAdmin_Community: prev => {
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
const decrement = {
flaggedUsernamesCount: { $apply: count => count - 1 },
};
// Remove from list after the mutation was "really" completed.
if (get(mutationResult, 'data.approveUsername.isOptimistic')) {
return update(prev, decrement);
}
const updated = update(prev, {
...decrement,
flaggedUsers: {
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
@@ -173,20 +191,95 @@ export default {
return updated;
},
},
update: proxy => {
proxy.writeFragment({
fragment: gql`
fragment Talk_ApproveUsername on User {
state {
status {
username {
status
}
}
}
}
`,
id: `User_${id}`,
data: {
__typename: 'User',
state: {
__typename: 'UserState',
status: {
__typename: 'UserStatus',
username: {
__typename: 'UsernameStatus',
status: 'APPROVED',
},
},
},
},
});
},
}),
RejectUsername: ({ variables: { id: userId } }) => ({
RejectUsername: ({ variables: { id } }) => ({
optimisticResponse: {
rejectUsername: {
__typename: 'RejectUsernameResponse',
errors: null,
isOptimistic: true,
},
},
updateQueries: {
TalkAdmin_Community: prev => {
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
const decrement = {
flaggedUsernamesCount: { $apply: count => count - 1 },
};
// Remove from list after the mutation was "really" completed.
if (get(mutationResult, 'data.rejectUsername.isOptimistic')) {
return update(prev, decrement);
}
const updated = update(prev, {
...decrement,
flaggedUsers: {
nodes: {
$apply: nodes => nodes.filter(node => node.id !== userId),
$apply: nodes => nodes.filter(node => node.id !== id),
},
},
});
return updated;
},
},
update: proxy => {
proxy.writeFragment({
fragment: gql`
fragment Talk_RejectUsername on User {
state {
status {
username {
status
}
}
}
}
`,
id: `User_${id}`,
data: {
__typename: 'User',
state: {
__typename: 'UserState',
status: {
__typename: 'UserStatus',
username: {
__typename: 'UsernameStatus',
status: 'REJECTED',
},
},
},
},
});
},
}),
UpdateSettings: ({ variables: { input } }) => ({
updateQueries: {
+2 -2
View File
@@ -14,8 +14,8 @@ import { hideShortcutsNote } from './actions/moderation';
smoothscroll.polyfill();
function init({ store, storage }) {
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
function init({ store, localStorage }) {
if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') {
store.dispatch(hideShortcutsNote());
}
}
-65
View File
@@ -1,65 +0,0 @@
import * as actions from '../constants/auth';
const initialState = {
loggedIn: false,
user: null,
loginError: null,
loginMaxExceeded: false,
passwordRequestSuccess: null,
};
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;
}
}
@@ -9,6 +9,7 @@ import {
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG,
SET_INDICATOR_TRACK,
} from '../constants/community';
const initialState = {
@@ -24,6 +25,10 @@ const initialState = {
user: {},
banDialog: false,
rejectUsernameDialog: false,
// If true the activity indicator will track flagged account changes
// in order to determine the current queue count. Set this to false
// if the queue count is determined by other means.
indicatorTrack: true,
};
export default function community(state = initialState, action) {
@@ -91,6 +96,11 @@ export default function community(state = initialState, action) {
...state,
searchValue: action.value,
};
case SET_INDICATOR_TRACK:
return {
...state,
indicatorTrack: action.track,
};
default:
return state;
}
-4
View File
@@ -1,17 +1,14 @@
import auth from './auth';
import stories from './stories';
import configure from './configure';
import community from './community';
import moderation from './moderation';
import install from './install';
import config from './config';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
import userDetail from './userDetail';
import ui from './ui';
export default {
auth,
banUserDialog,
configure,
suspendUserDialog,
@@ -20,6 +17,5 @@ export default {
community,
moderation,
install,
config,
ui,
};
+12 -2
View File
@@ -8,14 +8,19 @@ const initialState = {
shortcutsNoteVisible: 'show',
sortOrder: 'DESC',
selectedCommentId: '',
// If true the activity indicator will turn on subscriptions
// in order to determine queue counts. Set this to false
// if the queue count is determined by other means.
indicatorTrack: true,
};
export default function moderation(state = initialState, action) {
switch (action.type) {
case actions.MODERATION_CLEAR_STATE:
case actions.CLEAR_STATE:
return {
...initialState,
shortcutsNoteVisible: state.shortcutsNoteVisible,
indicatorTrack: state.indicatorTrack,
};
case actions.TOGGLE_MODAL:
return {
@@ -52,11 +57,16 @@ export default function moderation(state = initialState, action) {
...state,
sortOrder: action.order,
};
case actions.MODERATION_SELECT_COMMENT:
case actions.SELECT_COMMENT:
return {
...state,
selectedCommentId: action.id,
};
case actions.SET_INDICATOR_TRACK:
return {
...state,
indicatorTrack: action.track,
};
default:
return state;
}
@@ -2,44 +2,31 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './Community.css';
import People from '../containers/People';
import CommunityMenu from './CommunityMenu';
import RejectUsernameDialog from './RejectUsernameDialog';
import CommunityMenu from '../containers/CommunityMenu';
import RejectUsernameDialog from '../containers/RejectUsernameDialog';
import FlaggedAccounts from '../containers/FlaggedAccounts';
class Community extends Component {
renderTab() {
const { route, community, ...props } = this.props;
const { route } = this.props;
const activeTab = route.path === ':id' ? 'flagged' : route.path;
if (activeTab === 'people') {
return (
<People
community={community}
data={this.props.data}
root={this.props.root}
/>
);
return <People />;
}
return (
<div>
<FlaggedAccounts data={this.props.data} root={this.props.root} />
<RejectUsernameDialog
user={community.user}
open={community.rejectUsernameDialog}
rejectUsername={props.rejectUsername}
handleClose={props.hideRejectUsernameDialog}
/>
<FlaggedAccounts />
<RejectUsernameDialog />
</div>
);
}
render() {
const { root: { flaggedUsernamesCount } } = this.props;
return (
<div className="talk-admin-community">
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
<CommunityMenu />
<div className={styles.container}>{this.renderTab()}</div>
</div>
);
@@ -48,11 +35,6 @@ class Community extends Component {
Community.propTypes = {
route: PropTypes.object,
community: PropTypes.object,
rejectUsername: PropTypes.func.isRequired,
hideRejectUsernameDialog: PropTypes.func.isRequired,
data: PropTypes.object,
root: PropTypes.object,
};
export default Community;
@@ -17,6 +17,7 @@ class FlaggedAccounts extends React.Component {
approveUser,
me,
viewUserDetail,
hasMore,
} = this.props;
const hasResults = users.nodes && !!users.nodes.length;
@@ -60,7 +61,7 @@ class FlaggedAccounts extends React.Component {
) : (
<EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
)}
<LoadMore loadMore={loadMore} showLoadMore={users.hasNextPage} />
<LoadMore loadMore={loadMore} showLoadMore={hasMore} />
</div>
</div>
);
@@ -70,6 +71,7 @@ class FlaggedAccounts extends React.Component {
FlaggedAccounts.propTypes = {
users: PropTypes.object,
loadMore: PropTypes.func,
hasMore: PropTypes.bool,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
me: PropTypes.object,
@@ -9,8 +9,8 @@
min-width: 400px;
margin: 0 auto;
position: relative;
transition: box-shadow 200ms, margin-bottom 200ms;
padding: 10px 0 0;
transition: background 200ms, box-shadow 200ms, margin-bottom 200ms;
padding: 10px 0 10px;
min-height: 220px;
&:hover {
@@ -18,6 +18,10 @@
}
}
.dangling {
background-color: #efefef;
}
.rootSelected {
composes: mdl-shadow--16dp from global;
}
@@ -3,9 +3,10 @@ import styles from './FlaggedUser.css';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import { username } from 'talk-plugin-flags/helpers/flagReasons';
import { username } from 'coral-framework/graphql/flagReasons';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import { isFlaggedUserDangling } from '../utils';
const shortReasons = {
[username.other]: t('community.other'),
@@ -28,12 +29,14 @@ class User extends React.Component {
render() {
const { user, viewUserDetail, selected, className } = this.props;
const dangling = isFlaggedUserDangling(user);
return (
<li
tabIndex={0}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
[styles.dangling]: dangling,
})}
>
<div
@@ -110,11 +113,15 @@ class User extends React.Component {
<div className={styles.actions}>
<ApproveButton
className="talk-admin-flagged-user-approve-button"
active={user.state.status.username.status === 'APPROVED'}
onClick={this.approveUser}
disabled={dangling}
/>
<RejectButton
className="talk-admin-flagged-user-reject-button"
active={user.state.status.username.status === 'REJECTED'}
onClick={this.showRejectUsernameDialog}
disabled={dangling}
/>
</div>
</div>
@@ -10,6 +10,12 @@ import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import { isSuspended, isBanned } from 'coral-framework/utils/user';
import moment from 'moment';
import {
ADMIN,
COMMENTER,
MODERATOR,
STAFF,
} from 'coral-framework/constants/roles';
const headers = [
{
@@ -200,19 +206,19 @@ class People extends React.Component {
onChange={role => setUserRole(user.id, role)}
>
<Option
value={'COMMENTER'}
value={COMMENTER}
label={t('community.commenter')}
/>
<Option
value={'STAFF'}
value={STAFF}
label={t('community.staff')}
/>
<Option
value={'MODERATOR'}
value={MODERATOR}
label={t('community.moderator')}
/>
<Option
value={'ADMIN'}
value={ADMIN}
label={t('community.admin')}
/>
</Dropdown>
@@ -94,7 +94,7 @@ class RejectUsernameDialog extends Component {
{/* {
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
stage === 1 &&
<div className={styles.writeContainer}>
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
@@ -1,62 +0,0 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import { getDefinitionName } from 'coral-framework/utils';
import { withRejectUsername } from 'coral-framework/graphql/mutations';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
import People from '../containers/People';
import { hideRejectUsernameDialog } from '../../../actions/community';
import Community from '../components/Community';
const mapStateToProps = state => ({
community: state.community,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
hideRejectUsernameDialog,
},
dispatch
);
const withData = withQuery(
gql`
query TalkAdmin_Community {
flaggedUsernamesCount: userCount(
query:{
action_type: FLAG,
state: {
status: {
username: [SET, CHANGED]
}
}
}
)
...${getDefinitionName(FlaggedAccounts.fragments.root)}
...${getDefinitionName(FlaggedUser.fragments.root)}
...${getDefinitionName(People.fragments.root)}
me {
...${getDefinitionName(FlaggedUser.fragments.me)}
__typename
}
}
${People.fragments.root}
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRejectUsername,
withData
)(Community);
@@ -0,0 +1,4 @@
import withCachedFlaggedUsernamesCount from '../hocs/withCachedFlaggedUsernamesCount';
import CommunityMenu from '../components/CommunityMenu';
export default withCachedFlaggedUsernamesCount(CommunityMenu);
@@ -2,22 +2,136 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { withFragments } from 'plugin-api/beta/client/hocs';
import withQuery from 'coral-framework/hocs/withQuery';
import { Spinner } from 'coral-ui';
import PropTypes from 'prop-types';
import { withApproveUsername } from 'coral-framework/graphql/mutations';
import { showRejectUsernameDialog } from '../../../actions/community';
import {
showRejectUsernameDialog,
setIndicatorTrack,
} from '../../../actions/community';
import { viewUserDetail } from '../../../actions/userDetail';
import { getDefinitionName } from 'coral-framework/utils';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { handleFlaggedAccountsChange } from '../graphql';
import { notify } from 'coral-framework/actions/notification';
import { isFlaggedUserDangling } from '../utils';
import t from 'coral-framework/services/i18n';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
function whoChangedTheStatus(statusObject) {
return statusObject.history[statusObject.history.length - 1].assigned_by
.username;
}
function whoFlagged(user) {
return user.actions[user.actions.length - 1].user.username;
}
class FlaggedAccountsContainer extends Component {
constructor(props) {
super(props);
subscriptions = [];
getCountWithoutDangling() {
return this.props.root.flaggedUsers.nodes.filter(
node => !isFlaggedUserDangling(node)
).length;
}
subscribeToUpdates() {
const parameters = [
{
document: USERNAME_FLAGGED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameFlagged: user } } }
) => {
return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_flagged',
whoFlagged(user),
user.username
);
this.props.notify('info', msg);
});
},
},
{
document: USERNAME_APPROVED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameApproved: user } } }
) => {
return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_approved',
whoChangedTheStatus(user.state.status.username),
user.username
);
this.props.notify('info', msg);
});
},
},
{
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameRejected: user } } }
) => {
return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_rejected',
whoChangedTheStatus(user.state.status.username),
user.username
);
this.props.notify('info', msg);
});
},
},
{
document: USERNAME_CHANGED_SUBSCRIPTION,
updateQuery: (
prev,
{
subscriptionData: {
data: { usernameChanged: { previousUsername, user } },
},
}
) => {
return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_changed',
previousUsername,
user.username
);
this.props.notify('info', msg);
});
},
},
];
this.subscriptions = parameters.map(param =>
this.props.data.subscribeToMore(param)
);
}
unsubscribe() {
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions = [];
}
componentWillMount() {
// Stop activity indicator tracking, as we'll handle it here.
this.props.setIndicatorTrack(false);
this.subscribeToUpdates();
}
componentWillUnmount() {
// Restart activity indicator tracking.
this.props.setIndicatorTrack(true);
this.unsubscribe();
}
approveUser = ({ userId: id }) => {
@@ -68,6 +182,9 @@ class FlaggedAccountsContainer extends Component {
data={this.props.data}
root={this.props.root}
users={this.props.root.flaggedUsers}
hasMore={
this.getCountWithoutDangling() < this.props.root.flaggedUsernamesCount
}
me={this.props.root.me}
/>
);
@@ -77,9 +194,12 @@ class FlaggedAccountsContainer extends Component {
FlaggedAccountsContainer.propTypes = {
showRejectUsernameDialog: PropTypes.func,
viewUserDetail: PropTypes.func,
notify: PropTypes.func,
flaggedUsernamesCount: PropTypes.number,
approveUsername: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
setIndicatorTrack: PropTypes.func,
};
const LOAD_MORE_QUERY = gql`
@@ -105,11 +225,73 @@ const LOAD_MORE_QUERY = gql`
${FlaggedUser.fragments.user}
`;
const historyFields = `
state {
status {
username {
history {
status
assigned_by {
id
username
}
created_at
}
}
}
}
`;
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameFlagged {
usernameFlagged {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
${FlaggedUser.fragments.user}
`;
const USERNAME_APPROVED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameApproved {
usernameApproved {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
${FlaggedUser.fragments.user}
`;
const USERNAME_REJECTED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameRejected {
usernameRejected {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
${FlaggedUser.fragments.user}
`;
const USERNAME_CHANGED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameChanged {
usernameChanged {
previousUsername
user {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
}
${FlaggedUser.fragments.user}
`;
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
showRejectUsernameDialog,
viewUserDetail,
notify,
setIndicatorTrack,
},
dispatch
);
@@ -117,9 +299,15 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(null, mapDispatchToProps),
withApproveUsername,
withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery {
withQuery(
gql`
query TalkAdmin_Community_FlaggedAccounts {
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
flaggedUsers: users(query:{
action_type: FLAG,
state: {
@@ -138,9 +326,16 @@ export default compose(
}
me {
id
...${getDefinitionName(FlaggedUser.fragments.me)}
}
}
${FlaggedUser.fragments.user}
${FlaggedUser.fragments.me}
`,
})
{
options: {
fetchPolicy: 'network-only',
},
}
)
)(FlaggedAccountsContainer);
@@ -4,19 +4,20 @@ import { withFragments } from 'plugin-api/beta/client/hocs';
export default withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
fragment TalkAdmin_Community_FlaggedUser_root on RootQuery {
__typename
}
`,
me: gql`
fragment TalkAdminCommunity_FlaggedUser_me on User {
fragment TalkAdmin_Community_FlaggedUser_me on User {
id
}
`,
user: gql`
fragment TalkAdminCommunity_FlaggedUser_user on User {
fragment TalkAdmin_Community_FlaggedUser_user on User {
id
username
created_at
state {
status {
username {
@@ -0,0 +1,161 @@
import React, { Component } from 'react';
import { compose, gql } from 'react-apollo';
import Indicator from '../../../components/Indicator';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { handleIndicatorChange } from '../graphql';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
class IndicatorContainer extends Component {
subscriptions = [];
subscribeToUpdates() {
const parameters = [
{
document: USERNAME_FLAGGED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameFlagged: user } } }
) => {
return handleIndicatorChange(prev, user);
},
},
{
document: USERNAME_APPROVED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameApproved: user } } }
) => {
return handleIndicatorChange(prev, user);
},
},
{
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameRejected: user } } }
) => {
return handleIndicatorChange(prev, user);
},
},
{
document: USERNAME_CHANGED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameChanged: { user } } } }
) => {
return handleIndicatorChange(prev, user);
},
},
];
this.subscriptions = parameters.map(param =>
this.props.data.subscribeToMore(param)
);
}
unsubscribe() {
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions = [];
}
componentWillMount() {
if (this.props.track) {
this.subscribeToUpdates();
}
}
componentWillUnmount() {
this.unsubscribe();
}
componentWillReceiveProps(nextProps) {
if (!this.props.track && nextProps.track) {
this.subscribeToUpdates();
}
if (this.props.track && !nextProps.track) {
this.unsubscribe();
}
}
render() {
if (!this.props.root || !this.props.root.flaggedUsernamesCount) {
return null;
}
return <Indicator />;
}
}
IndicatorContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
track: PropTypes.bool,
};
const fields = `
state {
status {
username {
status
}
}
}
`;
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
subscription TalkAdmin_CommunityIndicator_UsernameFlagged {
usernameFlagged {
${fields}
}
}
`;
const USERNAME_APPROVED_SUBSCRIPTION = gql`
subscription TalkAdmin_ComunityIndicator_UsernameApproved {
usernameApproved {
${fields}
}
}
`;
const USERNAME_REJECTED_SUBSCRIPTION = gql`
subscription TalkAdmin_CommunityIndicator_UsernameRejected {
usernameRejected {
${fields}
}
}
`;
const USERNAME_CHANGED_SUBSCRIPTION = gql`
subscription TalkAdmin_ComunityIndicator_UsernameChanged {
usernameChanged {
previousUsername
user {
${fields}
}
}
}
`;
const mapStateToProps = state => ({
track: state.community.indicatorTrack,
});
const enhance = compose(
connect(mapStateToProps),
withFragments({
root: gql`
fragment TalkAdmin_CommunityIndicator_root on RootQuery {
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
}
`,
})
);
export default enhance(IndicatorContainer);
@@ -4,7 +4,6 @@ import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import People from '../components/People';
import PropTypes from 'prop-types';
import { withFragments } from 'plugin-api/beta/client/hocs';
import {
withUnbanUser,
withUnsuspendUser,
@@ -16,6 +15,7 @@ import { viewUserDetail } from '../../../actions/userDetail';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { Spinner } from 'coral-ui';
import withQuery from 'coral-framework/hocs/withQuery';
class PeopleContainer extends React.Component {
timer = null;
@@ -39,7 +39,7 @@ class PeopleContainer extends React.Component {
query: SEARCH_QUERY,
variables: {
value,
limit: 5,
limit: 10,
},
updateQuery: (previous, { fetchMoreResult: { users } }) => {
const updated = update(previous, {
@@ -115,7 +115,6 @@ class PeopleContainer extends React.Component {
}
PeopleContainer.propTypes = {
community: PropTypes.object,
setUserRole: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
@@ -137,7 +136,7 @@ const mapDispatchToProps = dispatch =>
);
const LOAD_MORE_QUERY = gql`
query TalkAdminCommunity_People_LoadMoreUsers(
query TalkAdmin_Community_People_LoadMoreUsers(
$limit: Int
$cursor: Cursor
$value: String
@@ -171,7 +170,7 @@ const LOAD_MORE_QUERY = gql`
`;
const SEARCH_QUERY = gql`
query TalkAdminCommunity_People_SearchUsers($value: String, $limit: Int) {
query TalkAdmin_Community_People_SearchUsers($value: String, $limit: Int) {
users(query: { value: $value, limit: $limit }) {
hasNextPage
endCursor
@@ -205,9 +204,9 @@ export default compose(
withSetUserRole,
withUnsuspendUser,
withUnbanUser,
withFragments({
root: gql`
fragment TalkAdminCommunity_People_root on RootQuery {
withQuery(
gql`
query TalkAdmin_Community_People {
users(query: {}) {
hasNextPage
endCursor
@@ -235,5 +234,10 @@ export default compose(
}
}
`,
})
{
options: {
fetchPolicy: 'network-only',
},
}
)
)(PeopleContainer);
@@ -0,0 +1,24 @@
import RejectUsernameDialog from '../components/RejectUsernameDialog';
import { withRejectUsername } from 'coral-framework/graphql/mutations';
import { hideRejectUsernameDialog } from '../../../actions/community';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose } from 'react-apollo';
const mapStateToProps = state => ({
user: state.community.user,
open: state.community.rejectUsernameDialog,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
handleClose: hideRejectUsernameDialog,
},
dispatch
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRejectUsername
)(RejectUsernameDialog);
@@ -0,0 +1,125 @@
import update from 'immutability-helper';
function shouldAddFlaggedUser(root, user) {
const isEmpty = !root.flaggedUsers.nodes.length;
if (isEmpty) {
return true;
}
if (hasFlaggedUser(root, user)) {
return false;
}
const oldest = root.flaggedUsers.nodes.reduce((cur, node) => {
const createdAt = new Date(node.created_at);
return createdAt < cur ? createdAt : cur;
}, new Date());
return new Date(user.created_at) >= oldest;
}
function hasFlaggedUser(root, user) {
return root.flaggedUsers.nodes.find(u => u.id === user.id);
}
function applyUserChanges(root, user) {
const index = root.flaggedUsers.nodes.findIndex(({ id }) => id === user.id);
if (index > -1) {
return update(root, {
flaggedUsers: {
nodes: {
[index]: { $merge: user },
},
},
});
}
return root;
}
function incrementFlaggedUserCount(root) {
return update(root, {
flaggedUsernamesCount: { $apply: count => count + 1 },
});
}
function decrementFlaggedUserCount(root) {
return update(root, {
flaggedUsernamesCount: { $apply: count => count - 1 },
});
}
/**
* Assimilate flagged acount changes into current store.
* @param {Object} root current state of the store
* @param {Object} user user that was changed
* @param {function} notify callback to show notification
* @return {Object} next state of the store
*/
export function handleFlaggedAccountsChange(root, user, notify) {
if (user.state.status.username.status !== 'SET') {
// Check if change came from current user, if so ignore it.
const lastChange =
user.state.status.username.history[
user.state.status.username.history.length - 1
];
if (lastChange.assigned_by.id === root.me.id) {
return root;
}
}
if (!hasFlaggedUser(root, user)) {
switch (user.state.status.username.status) {
case 'SET':
case 'CHANGED':
root = incrementFlaggedUserCount(root);
if (!shouldAddFlaggedUser(root, user)) {
return root;
}
notify();
return update(root, {
flaggedUsers: {
nodes: { $push: [user] },
},
});
break;
case 'APPROVED':
case 'REJECTED':
return decrementFlaggedUserCount(root);
default:
}
}
if (hasFlaggedUser(root, user)) {
switch (user.state.status.username.status) {
case 'SET':
case 'CHANGED':
return root;
case 'APPROVED':
case 'REJECTED':
notify();
return applyUserChanges(decrementFlaggedUserCount(root), user);
default:
}
}
}
/**
* Track indicator status
* @param {Object} root current state of the store
* @param {Object} user user that was changed
* @return {Object} next state of the store
*/
export function handleIndicatorChange(root, user) {
switch (user.state.status.username.status) {
case 'SET':
case 'CHANGED':
return incrementFlaggedUserCount(root);
case 'APPROVED':
case 'REJECTED':
return decrementFlaggedUserCount(root);
default:
}
}
@@ -0,0 +1,29 @@
import { compose, gql } from 'react-apollo';
import { mapProps } from 'recompose';
import withQuery from 'coral-framework/hocs/withQuery';
import get from 'lodash/get';
const withData = withQuery(
gql`
query TalkAdmin_CommunityMenu {
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
}
`,
{
options: {
fetchPolicy: 'cache-only',
},
}
);
export default compose(
withData,
mapProps(ownProps => ({
flaggedUsernamesCount: get(ownProps, 'root.flaggedUsernamesCount'),
}))
);
@@ -0,0 +1 @@
export { default } from './components/Community';
@@ -0,0 +1,3 @@
export function isFlaggedUserDangling(user) {
return ['APPROVED', 'REJECTED'].includes(user.state.status.username.status);
}
@@ -24,7 +24,7 @@ export default class Configure extends Component {
render() {
const {
auth: { user },
currentUser,
canSave,
savePending,
setActiveSection,
@@ -32,7 +32,7 @@ export default class Configure extends Component {
} = this.props;
const SectionComponent = this.getSectionComponent(activeSection);
if (!can(user, 'UPDATE_CONFIG')) {
if (!can(currentUser, 'UPDATE_CONFIG')) {
return (
<p>
You must be an administrator to access config settings. Please find
@@ -86,9 +86,8 @@ export default class Configure extends Component {
}
Configure.propTypes = {
notify: PropTypes.func.isRequired,
savePending: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
@@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { withQuery, withMergedSettings } from 'coral-framework/hocs';
import { Spinner } from 'coral-ui';
import { notify } from 'coral-framework/actions/notification';
import PropTypes from 'prop-types';
import { withUpdateSettings } from 'coral-framework/graphql/mutations';
import { getErrorMessages, getDefinitionName } from 'coral-framework/utils';
import { getDefinitionName } from 'coral-framework/utils';
import StreamSettings from './StreamSettings';
import TechSettings from './TechSettings';
import ModerationSettings from './ModerationSettings';
@@ -16,23 +15,22 @@ import Configure from '../components/Configure';
class ConfigureContainer extends Component {
savePending = async () => {
try {
await this.props.updateSettings(this.props.pending);
this.props.clearPending();
} catch (err) {
this.props.notify('error', getErrorMessages(err));
}
await this.props.updateSettings(this.props.pending);
this.props.clearPending();
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <Spinner />;
}
return (
<Configure
notify={this.props.notify}
auth={this.props.auth}
currentUser={this.props.currentUser}
data={this.props.data}
root={this.props.root}
settings={this.props.mergedSettings}
@@ -67,12 +65,13 @@ const withConfigureQuery = withQuery(
{
options: () => ({
variables: {},
fetchPolicy: 'network-only',
}),
}
);
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
pending: state.configure.pending,
canSave: state.configure.canSave,
activeSection: state.configure.activeSection,
@@ -81,7 +80,6 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
notify,
clearPending,
setActiveSection,
},
@@ -89,9 +87,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUpdateSettings,
withConfigureQuery,
connect(mapStateToProps, mapDispatchToProps),
withMergedSettings('root.settings', 'pending', 'mergedSettings')
)(ConfigureContainer);
@@ -99,8 +97,7 @@ ConfigureContainer.propTypes = {
updateSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
setActiveSection: PropTypes.func.isRequired,
notify: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import styles from './Install.css';
import { Wizard, WizardNav } from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
import Layout from 'coral-admin/src/components/Layout';
import InitialStep from './Steps/InitialStep';
import AddOrganizationName from './Steps/AddOrganizationName';
@@ -143,7 +143,6 @@
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
@@ -8,7 +8,7 @@ import styles from './Comment.css';
import CommentLabels from 'coral-admin/src/components/CommentLabels';
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
@@ -126,11 +126,12 @@ class Comment extends React.Component {
<CommentAnimatedEdit body={comment.body}>
<div className={styles.itemBody}>
<div className={styles.body}>
<CommentBodyHighlighter
<CommentFormatter
suspectWords={settings.wordlist.suspect}
bannedWords={settings.wordlist.banned}
className="talk-admin-comment"
body={comment.body}
/>{' '}
/>
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -19,7 +19,7 @@ class Moderation extends Component {
componentWillMount() {
const { toggleModal, singleView } = this.props;
key('s', () => singleView());
key('z', () => singleView());
key('shift+/', () => toggleModal(true));
key('esc', () => toggleModal(false));
key('ctrl+f', () => this.openSearch());
@@ -113,7 +113,7 @@ class Moderation extends Component {
};
componentWillUnmount() {
key.unbind('s');
key.unbind('z');
key.unbind('shift+/');
key.unbind('esc');
key.unbind('ctrl+f');
@@ -185,7 +185,7 @@ class Moderation extends Component {
commentBelongToQueue={this.props.commentBelongToQueue}
isLoadingMore={this.state.isLoadingMore}
commentCount={activeTabCount}
currentUserId={this.props.auth.user.id}
currentUserId={this.props.currentUser.id}
viewUserDetail={viewUserDetail}
selectCommentId={props.selectCommentId}
cleanUpQueue={props.cleanUpQueue}
@@ -225,7 +225,7 @@ Moderation.propTypes = {
cleanUpQueue: PropTypes.func.isRequired,
storySearchChange: PropTypes.func.isRequired,
moderation: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
queueConfig: PropTypes.object.isRequired,
commentBelongToQueue: PropTypes.func.isRequired,
handleCommentChange: PropTypes.func.isRequired,
@@ -397,6 +397,13 @@ class ModerationQueue extends React.Component {
const index = comments.findIndex(
comment => comment.id === selectedCommentId
);
// This can happen temporarily when we call redux to change the selected comment
// but it didn't fully take effect yet.
if (index === -1) {
return null;
}
const comment = comments[index];
return (
<div className={styles.root}>
@@ -414,7 +421,7 @@ class ModerationQueue extends React.Component {
dangling={
!this.props.commentBelongToQueue(this.props.activeTab, comment)
}
/>;
/>
</div>
);
}
@@ -19,12 +19,12 @@ class ViewOptions extends React.Component {
'talk-admin-moderation-view-options-headline'
)}
>
View Options
{t('admin_sidebar.view_options')}
</h2>
<div className={styles.viewOptionsContent}>
<ul className={styles.viewOptionsList}>
<li className={styles.viewOptionsItem}>
Sort Comments
{t('admin_sidebar.sort_comments')}
<Dropdown
containerClassName={styles.dropdownContainer}
toggleClassName={styles.dropdownToggle}
@@ -0,0 +1,207 @@
import React, { Component } from 'react';
import { compose, gql } from 'react-apollo';
import Indicator from '../../../components/Indicator';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { handleIndicatorChange, subscriptionFields } from '../graphql';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import withQueueConfig from '../hoc/withQueueConfig';
import baseQueueConfig from '../queueConfig';
import Slot from 'coral-framework/components/Slot';
class IndicatorContainer extends Component {
subscriptions = [];
handleCommentChange = (root, comment) => {
return handleIndicatorChange(root, comment, this.props.queueConfig);
};
subscribeToUpdates() {
const parameters = [
{
document: COMMENT_ADDED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentAdded: comment } } }
) => {
return this.handleCommentChange(prev, comment);
},
},
{
document: COMMENT_FLAGGED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentFlagged: comment } } }
) => {
return this.handleCommentChange(prev, comment);
},
},
{
document: COMMENT_EDITED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentEdited: comment } } }
) => {
return this.handleCommentChange(prev, comment);
},
},
{
document: COMMENT_ACCEPTED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentAccepted: comment } } }
) => {
return this.handleCommentChange(prev, comment);
},
},
{
document: COMMENT_REJECTED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentRejected: comment } } }
) => {
return this.handleCommentChange(prev, comment);
},
},
{
document: COMMENT_RESET_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentReset: comment } } }
) => {
return this.handleCommentChange(prev, comment);
},
},
];
this.subscriptions = parameters.map(param =>
this.props.data.subscribeToMore(param)
);
}
unsubscribe() {
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions = [];
}
componentWillMount() {
if (this.props.track) {
this.subscribeToUpdates();
}
}
componentWillUnmount() {
this.unsubscribe();
}
componentWillReceiveProps(nextProps) {
if (!this.props.track && nextProps.track) {
this.subscribeToUpdates();
}
if (this.props.track && !nextProps.track) {
this.unsubscribe();
}
}
render() {
if (
!this.props.root ||
(!this.props.root.premodCount && !this.props.root.reportedCount)
) {
return null;
}
return (
<span>
<Indicator />
<Slot
data={this.props.data}
handleCommentChange={this.handleCommentChange}
fill="adminModerationIndicator"
/>
</span>
);
}
}
IndicatorContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
track: PropTypes.bool,
queueConfig: PropTypes.object,
};
const COMMENT_ADDED_SUBSCRIPTION = gql`
subscription TalkAdmin_ModerationIndicator_CommentAdded {
commentAdded(statuses: null) {
${subscriptionFields}
}
}
`;
const COMMENT_FLAGGED_SUBSCRIPTION = gql`
subscription TalkAdmin_ModerationIndicator_CommentFlagged {
commentFlagged {
${subscriptionFields}
}
}
`;
const COMMENT_EDITED_SUBSCRIPTION = gql`
subscription TalkAdmin_ModerationIndicator_CommentEdited {
commentEdited {
${subscriptionFields}
}
}
`;
const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
subscription TalkAdmin_ModerationIndicator_CommentAccepted {
commentAccepted {
${subscriptionFields}
}
}
`;
const COMMENT_REJECTED_SUBSCRIPTION = gql`
subscription TalkAdmin_ModerationIndicator_CommentRejected {
commentRejected {
${subscriptionFields}
}
}
`;
const COMMENT_RESET_SUBSCRIPTION = gql`
subscription TalkAdmin_ModerationIndicator_CommentReset {
commentReset {
${subscriptionFields}
}
}
`;
const mapStateToProps = state => ({
track: state.moderation.indicatorTrack,
});
const enhance = compose(
connect(mapStateToProps),
withFragments({
root: gql`
fragment TalkAdmin_Moderation_Indicator_root on RootQuery {
premodCount: commentCount(
query: { statuses: [PREMOD], asset_id: $nullID }
)
reportedCount: commentCount(
query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD]
action_type: FLAG
asset_id: $nullID
}
)
}
`,
}),
withQueueConfig(baseQueueConfig)
);
export default enhance(IndicatorContainer);
@@ -15,6 +15,7 @@ import {
handleCommentChange,
commentBelongToQueue,
cleanUpQueue,
subscriptionFields,
} from '../graphql';
import { viewUserDetail } from '../../../actions/userDetail';
@@ -27,6 +28,7 @@ import {
storySearchChange,
clearState,
selectCommentId,
setIndicatorTrack,
} from 'actions/moderation';
import withQueueConfig from '../hoc/withQueueConfig';
import { notify } from 'coral-framework/actions/notification';
@@ -108,7 +110,7 @@ class ModerationContainer extends Component {
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
this.props.currentUser.id === user.id
? ''
: t(
'modqueue.notify_accepted',
@@ -129,7 +131,7 @@ class ModerationContainer extends Component {
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
this.props.currentUser.id === user.id
? ''
: t(
'modqueue.notify_rejected',
@@ -150,7 +152,7 @@ class ModerationContainer extends Component {
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
this.props.currentUser.id === user.id
? ''
: t(
'modqueue.notify_reset',
@@ -198,21 +200,42 @@ class ModerationContainer extends Component {
}
componentWillMount() {
if (!this.props.data.variables.asset_id) {
// Stop activity indicator tracking, as we'll handle it here.
this.props.setIndicatorTrack(false);
}
this.props.clearState();
this.subscribeToUpdates();
}
componentWillUnmount() {
if (!this.props.data.variables.asset_id) {
// Restart activity indicator tracking.
this.props.setIndicatorTrack(true);
}
this.unsubscribe();
}
componentWillReceiveProps(nextProps) {
const currentAssetId = this.props.data.variables.asset_id;
const nextAssetId = nextProps.data.variables.asset_id;
// Resubscribe when we change between assets.
if (
this.props.data.variables.asset_id !== nextProps.data.variables.asset_id
) {
if (currentAssetId !== nextAssetId) {
this.resubscribe(nextProps.data.variables);
}
// We are only subscribing to a specific asset_id, so activity indicator
// needs to do its own tracking.
if (!currentAssetId && nextAssetId) {
this.props.setIndicatorTrack(true);
}
// We are subscribing to all comment changes, and as such there is no
// need for the activity indicator to do the same.
if (currentAssetId && !nextAssetId) {
this.props.setIndicatorTrack(false);
}
}
cleanUpQueue = queue => {
@@ -269,10 +292,6 @@ class ModerationContainer extends Component {
const { root, root: { asset, settings }, data } = this.props;
const assetId = getAssetId(this.props);
if (data.error) {
return <div>Error</div>;
}
if (assetId) {
if (asset === null) {
// Not found.
@@ -280,7 +299,11 @@ class ModerationContainer extends Component {
}
}
if (data.loading) {
if (data.error) {
return <div>{data.error.message}</div>;
}
if (data.loading && data.networkStatus !== 3) {
// loading.
return <Spinner />;
}
@@ -316,10 +339,12 @@ class ModerationContainer extends Component {
);
}
}
const COMMENT_ADDED_SUBSCRIPTION = gql`
subscription CommentAdded($asset_id: ID){
commentAdded(asset_id: $asset_id, statuses: null){
...${getDefinitionName(Comment.fragments.comment)}
${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -329,6 +354,7 @@ const COMMENT_EDITED_SUBSCRIPTION = gql`
subscription CommentEdited($asset_id: ID){
commentEdited(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -338,6 +364,7 @@ const COMMENT_FLAGGED_SUBSCRIPTION = gql`
subscription CommentFlagged($asset_id: ID){
commentFlagged(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -347,14 +374,7 @@ const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
subscription CommentAccepted($asset_id: ID){
commentAccepted(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
status_history {
type
created_at
assigned_by {
id
username
}
}
${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -364,14 +384,7 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
subscription CommentRejected($asset_id: ID){
commentRejected(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
status_history {
type
created_at
assigned_by {
id
username
}
}
${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -381,14 +394,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
subscription CommentReset($asset_id: ID){
commentReset(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
status_history {
type
created_at
assigned_by {
id
username
}
}
${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -509,7 +515,7 @@ const withModQueueQuery = withQuery(
const mapStateToProps = state => ({
moderation: state.moderation,
auth: state.auth,
currentUser: state.auth.user,
});
const mapDispatchToProps = dispatch => ({
@@ -525,6 +531,7 @@ const mapDispatchToProps = dispatch => ({
clearState,
notify,
selectCommentId,
setIndicatorTrack,
},
dispatch
),
@@ -20,20 +20,12 @@ function queueHasComment(root, queue, id) {
return root[queue].nodes.find(c => c.id === id);
}
function removeCommentFromQueue(root, queue, id, dangling = false) {
if (!queueHasComment(root, queue, id)) {
return root;
}
function removeCommentFromQueue(root, queue, id) {
const changes = {
[`${queue}Count`]: { $set: root[`${queue}Count`] - 1 },
};
if (!dangling) {
changes[queue] = {
[queue]: {
nodes: { $apply: nodes => nodes.filter(c => c.id !== id) },
};
}
},
};
return update(root, changes);
}
@@ -48,19 +40,21 @@ function shouldCommentBeAdded(root, queue, comment, sortOrder) {
: new Date(comment.created_at) >= cursor;
}
function addCommentToQueue(root, queue, comment, sortOrder, cleanup) {
if (queueHasComment(root, queue, comment.id)) {
return root;
}
function increaseCommentCount(root, queue) {
const changes = {
[`${queue}Count`]: { $set: root[`${queue}Count`] + 1 },
};
return update(root, changes);
}
if (!shouldCommentBeAdded(root, queue, comment, sortOrder)) {
return update(root, changes);
}
function decreaseCommentCount(root, queue) {
const changes = {
[`${queue}Count`]: { $set: root[`${queue}Count`] - 1 },
};
return update(root, changes);
}
function addCommentToQueue(root, queue, comment, sortOrder) {
const cursor = new Date(root[queue].startCursor);
const date = new Date(comment.created_at);
@@ -70,17 +64,13 @@ function addCommentToQueue(root, queue, comment, sortOrder, cleanup) {
? root[queue].nodes.concat(comment)
: [comment].concat(...root[queue].nodes);
changes[queue] = {
nodes: { $set: nodes },
const changes = {
[queue]: {
nodes: { $set: nodes },
},
};
const next = update(root, changes);
if (!cleanup) {
return next;
}
return cleanUpQueue(next, queue, sortOrder);
return update(root, changes);
}
function sortComments(nodes, sortOrder) {
@@ -101,6 +91,87 @@ function getCommentQueues(comment, queueConfig) {
return queues;
}
function getOlderDate(a, b) {
if (a) {
a = new Date(a);
}
if (b) {
b = new Date(b);
}
if (!b) {
return a;
}
if (!a) {
return b;
}
return a < b ? b : a;
}
function determineLatestChange(comment) {
let lc = null;
comment.body_history.forEach(item => {
lc = getOlderDate(lc, item.created_at);
});
comment.status_history.forEach(item => {
lc = getOlderDate(lc, item.created_at);
});
comment.actions.forEach(item => {
lc = getOlderDate(lc, item.created_at);
});
return lc;
}
function reconstructPreviousCommentState(comment) {
const statusHistory = comment.status_history;
const bodyHistory = comment.body_history;
const actions = comment.actions;
const lastChangeDate = determineLatestChange(comment);
const previousComment = {
...comment,
body_history: bodyHistory.filter(
item => new Date(item.created_at) < lastChangeDate
),
status_history: statusHistory.filter(
item => new Date(item.created_at) < lastChangeDate
),
actions: actions.filter(item => new Date(item.created_at) < lastChangeDate),
};
// Comment did not exist previously.
if (!previousComment.status_history.length) {
return null;
}
previousComment.status =
previousComment.status_history[
previousComment.status_history.length - 1
].type;
previousComment.body =
previousComment.body_history[previousComment.body_history.length - 1].body;
return previousComment;
}
/**
* getPreviousCommentQueues determines queues that this comment previously belonged to.
*/
function getPreviousCommentQueues(comment, queueConfig) {
const previousCommentState = reconstructPreviousCommentState(comment);
if (!previousCommentState) {
return [];
}
return getCommentQueues(previousCommentState, queueConfig);
}
/**
* Return whether or not the comment belongs to the queue.
*/
@@ -194,13 +265,13 @@ export function cleanUpQueue(root, queue, sortOrder, queueConfig) {
/**
* Assimilate comment changes into current store.
* @param {Object} root current state of the store
* @param {Object} comment comment that was changed
* @param {string} sortOrder current sort order of the queues
* @param {string} notify callback to show notification
* in the current active queue besides the 'all' queue.
* @param {Object} queueConfig queue configuration
* @return {Object} next state of the store
* @param {Object} root current state of the store
* @param {Object} comment comment that was changed
* @param {string} sortOrder current sort order of the queues
* @param {function} notify callback to show notification
* in the current active queue besides the 'all' queue.
* @param {Object} queueConfig queue configuration
* @return {Object} next state of the store
*/
export function handleCommentChange(
root,
@@ -212,6 +283,10 @@ export function handleCommentChange(
) {
let next = root;
// Queues that this comment previously belonged to.
const prevQueues = getPreviousCommentQueues(comment, queueConfig);
// Queues that this comment needs to be placed.
const nextQueues = getCommentQueues(comment, queueConfig);
let notificationShown = false;
@@ -224,15 +299,26 @@ export function handleCommentChange(
};
Object.keys(queueConfig).forEach(queue => {
// Comment should be placed in queue.
if (nextQueues.indexOf(queue) >= 0) {
// Comment was not previously in this queue.
if (prevQueues.indexOf(queue) === -1) {
// Increase count.
next = increaseCommentCount(next, queue);
}
if (!queueHasComment(next, queue, comment.id)) {
next = addCommentToQueue(
next,
queue,
comment,
sortOrder,
activeQueue !== queue
);
// Check if comment would be in the current view.
if (shouldCommentBeAdded(root, queue, comment, sortOrder)) {
next = addCommentToQueue(next, queue, comment, sortOrder);
// This will limit queue sizes.
if (activeQueue !== queue) {
cleanUpQueue(next, queue, sortOrder);
}
}
// Show notification if end of list is visible.
if (
notify &&
activeQueue === queue &&
@@ -241,17 +327,30 @@ export function handleCommentChange(
showNotificationOnce();
}
}
} else if (queueHasComment(next, queue, comment.id)) {
} else if (prevQueues.indexOf(queue) >= 0) {
// Comment previously was in this queue, but not anymore.
// If action was performed by another user, keep a dangling comment.
const dangling =
activeQueue === queue &&
comment.status_history[comment.status_history.length - 1].assigned_by
.id !== root.me.id;
next = removeCommentFromQueue(next, queue, comment.id, dangling);
if (notify && isVisible(comment.id)) {
showNotificationOnce();
// Otherwise remove it
if (!dangling) {
next = removeCommentFromQueue(next, queue, comment.id);
}
// In any case decrease comment count.
next = decreaseCommentCount(next, queue);
} else if (queueHasComment(next, queue, comment.id)) {
// Comment does not belong to his queue and did not belong to this queue previously.
// Must be a dangling comment that the current user took action on.
// Remove it completely from the queue.
next = removeCommentFromQueue(next, queue, comment.id);
}
// Show notification if operation affected a currently visible comment.
if (notify && isVisible(comment.id)) {
showNotificationOnce();
}
@@ -263,3 +362,55 @@ export function handleCommentChange(
});
return next;
}
const indicatorQueues = ['premod', 'reported'];
/**
* Track indicator status
* @param {Object} root current state of the store
* @param {Object} comment comment that was changed
* @return {Object} next state of the store
*/
export function handleIndicatorChange(root, comment, queueConfig) {
let next = root;
// Queues that this comment previously belonged to.
const prevQueues = getPreviousCommentQueues(comment, queueConfig);
// Queues that this comment needs to be placed.
const nextQueues = getCommentQueues(comment, queueConfig);
for (const queue of indicatorQueues) {
if (prevQueues.indexOf(queue) === -1 && nextQueues.indexOf(queue) >= 0) {
next = increaseCommentCount(next, queue);
}
if (prevQueues.indexOf(queue) >= 0 && nextQueues.indexOf(queue) === -1) {
next = decreaseCommentCount(next, queue);
}
}
return next;
}
export const subscriptionFields = `
status
body
body_history {
body
created_at
}
actions {
__typename
created_at
}
status_history {
type
assigned_by {
id
username
}
created_at
}
updated_at
created_at
`;
@@ -56,6 +56,7 @@
width: 100%;
border-left: none;
border-right: none;
white-space: pre-wrap;
a {
color: rgb(44, 44, 44);
+14
View File
@@ -0,0 +1,14 @@
document.addEventListener('DOMContentLoaded', () => {
// Get the auth element and parse it as JSON by decoding it.
const auth = document.getElementById('auth');
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = auth.innerText;
// Set the item in localStorage.
localStorage.setItem('auth', doc.body.textContent);
// Close the window.
setTimeout(() => {
window.close();
}, 50);
});
@@ -1,25 +0,0 @@
import React from 'react';
import { Router, Route } from 'react-router';
import PropTypes from 'prop-types';
import Embed from './containers/Embed';
import { LoginContainer } from 'coral-sign-in/containers/LoginContainer';
const routes = (
<div>
<Route exact path="/embed/stream/login" component={LoginContainer} />
<Route path="*" component={Embed} />
</div>
);
class AppRouter extends React.Component {
static contextTypes = {
history: PropTypes.object,
};
render() {
return <Router history={this.context.history} routes={routes} />;
}
}
export default AppRouter;

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