Compare commits

..
213 Commits
Author SHA1 Message Date
Wyatt Johnson 74d38f16ed Merge pull request #1388 from coralproject/bump-version-4-2-2
Update version to 4.2.2
2018-02-20 10:27:14 -07:00
Wyatt Johnson 7129896bd6 Merge branch 'master' into bump-version-4-2-2 2018-02-20 09:57:12 -07:00
Wyatt Johnson e2e718494c Merge pull request #1387 from coralproject/action-fixes
Action Count Fixes
2018-02-20 09:57:05 -07:00
Kim Gardner f918bbea20 Update version to 4.2.2 2018-02-20 11:45:36 -05:00
Wyatt Johnson 4467f6110f Merge branch 'master' into action-fixes 2018-02-20 09:44:00 -07:00
Wyatt Johnson a166404347 fix for action count null 2018-02-20 09:31:08 -07:00
Kim Gardner 3d2f3ebc59 Merge pull request #1384 from coralproject/user-tooltip-style
Making the User Info Tooltip more readable and friendly
2018-02-20 10:56:04 -05:00
Kim Gardner c4cdc672b7 Merge branch 'master' into user-tooltip-style 2018-02-20 10:44:15 -05:00
Kim Gardner b84a488518 Merge pull request #1380 from leeeandroo/facebook-translations
Fix translations keys and add german translations to facebook plugin
2018-02-20 10:44:05 -05:00
Kim Gardner 4b134cbfc2 Merge branch 'master' into facebook-translations 2018-02-20 10:34:22 -05:00
Kim Gardner c446098a1e Merge branch 'master' into user-tooltip-style 2018-02-20 10:24:00 -05:00
Kim Gardner b26e938ac5 Merge pull request #1381 from leeeandroo/package-version421
set a right last release 4.2.1 to package.json
2018-02-20 10:22:09 -05:00
okbel 846df0a5e0 trim dates, style tooltip 2018-02-19 12:27:55 -03:00
okbel e7934d9468 trim dates, style tooltip 2018-02-19 12:23:36 -03:00
Leandro 606d5734f9 set a right last release 4.2.1 to package.json 2018-02-19 11:18:56 +01:00
Leandro 87ff631eed fix translations keys and add germna translations to facebook plugin 2018-02-19 09:50:07 +01:00
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 6d662e4469 Merge branch 'master' into admin-user-details 2018-02-05 10:59:42 -05: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
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
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
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
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
Chi Vinh Le 118301c2b4 Implement a session storage 2018-02-01 12:25:51 +01:00
239 changed files with 6776 additions and 3435 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
-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
//==============================================================================
-177
View File
@@ -1,177 +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');
storage.removeItem('exp');
}
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');
storage.removeItem('exp');
}
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');
storage.removeItem('exp');
}
// 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' });
};
-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 });
};
+3 -3
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
+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;
+13 -11
View File
@@ -13,7 +13,7 @@ import CommunityIndicator from '../routes/Community/containers/Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
currentUser,
root,
data,
}) => {
@@ -22,9 +22,9 @@ const CoralHeader = ({
<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)}
@@ -54,7 +54,7 @@ const CoralHeader = ({
<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,7 +118,7 @@ const CoralHeader = ({
};
CoralHeader.propTypes = {
auth: PropTypes.object,
currentUser: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
+8 -4
View File
@@ -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;
@@ -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;
}
@@ -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;
+23 -12
View File
@@ -25,6 +25,7 @@ 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 => {
@@ -152,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>
@@ -190,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}
@@ -210,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,
@@ -259,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'}
@@ -275,7 +286,7 @@ class UserDetail extends React.Component {
'talk-admin-user-detail-history-tab'
)}
>
Account History
{t('user_detail.account_history')}
</Tab>
</TabBar>
@@ -28,8 +28,8 @@
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
z-index: 10;
top: 32px;
right: 0px;
width: 140px;
left: -100px;
width: 200px;
text-align: left;
color: #616161;
}
@@ -39,7 +39,7 @@
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 0px;
left: 96px;
top: -20px;
transform: rotate(180deg);
}
@@ -49,7 +49,7 @@
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 0px;
left: 96px;
top: -19px;
transform: rotate(180deg);
}
@@ -67,7 +67,7 @@ class UserInfoTooltip extends React.Component {
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
@@ -139,7 +139,7 @@ class UserInfoTooltip extends React.Component {
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
@@ -154,7 +154,7 @@ class UserInfoTooltip extends React.Component {
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
@@ -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);
+35 -72
View File
@@ -2,86 +2,58 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/Layout';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
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);
+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;
}
}
-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,
};
@@ -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
@@ -87,7 +87,7 @@ export default class Configure extends Component {
Configure.propTypes = {
savePending: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
@@ -30,7 +30,7 @@ class ConfigureContainer extends Component {
return (
<Configure
auth={this.props.auth}
currentUser={this.props.currentUser}
data={this.props.data}
root={this.props.root}
settings={this.props.mergedSettings}
@@ -71,7 +71,7 @@ const withConfigureQuery = withQuery(
);
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
pending: state.configure.pending,
canSave: state.configure.canSave,
activeSection: state.configure.activeSection,
@@ -97,7 +97,7 @@ ConfigureContainer.propTypes = {
updateSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
setActiveSection: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
@@ -143,7 +143,6 @@
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
@@ -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,
@@ -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}
@@ -110,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',
@@ -131,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',
@@ -152,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',
@@ -515,7 +515,7 @@ const withModQueueQuery = withQuery(
const mapStateToProps = state => ({
moderation: state.moderation,
auth: state.auth,
currentUser: state.auth.user,
});
const mapDispatchToProps = dispatch => ({
@@ -407,6 +407,7 @@ export const subscriptionFields = `
type
assigned_by {
id
username
}
created_at
}
@@ -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 './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;
@@ -1,415 +0,0 @@
import jwtDecode from 'jwt-decode';
import bowser from 'bowser';
import * as actions from '../constants/auth';
import { notify } from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
import get from 'lodash/get';
export const updateStatus = status => ({
type: actions.UPDATE_STATUS,
status,
});
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
});
export const hideSignInDialog = () => dispatch => {
if (window.opener && window.opener !== window) {
// TODO: We need to address this when we refactor the
// login popup out of the embed.
// we are in a popup
window.close();
} else {
dispatch(checkLogin());
}
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
};
export const resetSignInDialog = () => dispatch => {
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
};
export const focusSignInDialog = () => ({
type: actions.FOCUS_SIGNIN_DIALOG,
});
export const blurSignInDialog = () => ({
type: actions.BLUR_SIGNIN_DIALOG,
});
export const showCreateUsernameDialog = () => ({
type: actions.SHOW_CREATEUSERNAME_DIALOG,
});
export const hideCreateUsernameDialog = () => ({
type: actions.HIDE_CREATEUSERNAME_DIALOG,
});
export const updateUsername = username => ({
type: actions.UPDATE_USERNAME,
username,
});
export const changeView = view => dispatch => {
dispatch({
type: actions.CHANGE_VIEW,
view,
});
switch (view) {
case 'SIGNUP':
window.resizeTo(500, 800);
break;
case 'FORGOT':
window.resizeTo(500, 400);
break;
default:
window.resizeTo(500, 550);
}
};
export const cleanState = () => ({
type: actions.CLEAN_STATE,
});
// Sign In Actions
const signInRequest = email => ({
type: actions.FETCH_SIGNIN_REQUEST,
email,
});
const signInFailure = error => ({
type: actions.FETCH_SIGNIN_FAILURE,
error,
});
//==============================================================================
// 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' });
};
//==============================================================================
// SIGN IN
//==============================================================================
export const fetchSignIn = formData => {
return (dispatch, _, { rest }) => {
dispatch(signInRequest(formData.email));
return rest('/auth/local', { method: 'POST', body: formData })
.then(({ token }) => {
if (!bowser.safari && !bowser.ios) {
dispatch(handleAuthToken(token));
}
dispatch(hideSignInDialog());
})
.catch(error => {
console.error(error);
if (error.metadata) {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(
signInFailure(t('error.email_not_verified', error.metadata))
);
} else if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch(signInFailure(t('error.email_password'), error.metadata));
} else {
dispatch(signInFailure(error));
}
});
};
};
//==============================================================================
// SIGN IN - FACEBOOK
//==============================================================================
const signInFacebookRequest = () => ({
type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST,
});
const signInFacebookSuccess = user => ({
type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS,
user,
});
const signInFacebookFailure = error => ({
type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE,
error,
});
export const fetchSignInFacebook = () => (dispatch, _, { rest }) => {
dispatch(signInFacebookRequest());
window.open(
`${rest.uri}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
};
//==============================================================================
// SIGN UP - FACEBOOK
//==============================================================================
const signUpFacebookRequest = () => ({
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST,
});
export const fetchSignUpFacebook = () => (dispatch, _, { rest }) => {
dispatch(signUpFacebookRequest());
window.open(
`${rest.uri}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
};
export const facebookCallback = (err, data) => dispatch => {
if (err) {
dispatch(signInFacebookFailure(err));
return;
}
try {
dispatch(handleAuthToken(data.token));
dispatch(signInFacebookSuccess(data.user));
dispatch(hideSignInDialog());
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
}
};
//==============================================================================
// SIGN UP
//==============================================================================
const signUpRequest = () => ({ type: actions.FETCH_SIGNUP_REQUEST });
const signUpSuccess = user => ({ type: actions.FETCH_SIGNUP_SUCCESS, user });
const signUpFailure = error => ({ type: actions.FETCH_SIGNUP_FAILURE, error });
export const fetchSignUp = formData => (dispatch, getState, { rest }) => {
const redirectUri = getState().auth.redirectUri;
dispatch(signUpRequest());
rest('/users', {
method: 'POST',
body: formData,
headers: { 'X-Pym-Url': redirectUri },
})
.then(({ user }) => {
dispatch(signUpSuccess(user));
})
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(signUpFailure(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 fetchForgotPassword = email => (dispatch, getState, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = getState().auth.redirectUri;
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));
});
};
//==============================================================================
// LOGOUT
//==============================================================================
export const logout = () => async (
dispatch,
_,
{ rest, client, pym, storage }
) => {
await rest('/auth', { method: 'DELETE' });
if (storage) {
storage.removeItem('token');
storage.removeItem('exp');
}
// Reset the websocket.
client.resetWebsocket();
dispatch({ type: actions.LOGOUT });
pym.sendMessage('coral-auth-changed');
};
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin,
});
const ErrNotLoggedIn = new Error('Not logged in');
export const checkLogin = () => (
dispatch,
_,
{ rest, client, pym, storage }
) => {
dispatch(checkLoginRequest());
rest('/auth')
.then(result => {
if (!result.user) {
if (storage) {
storage.removeItem('token');
storage.removeItem('exp');
}
throw ErrNotLoggedIn;
}
// Reset the websocket.
client.resetWebsocket();
dispatch(checkLoginSuccess(result.user));
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
// This is for login via social. Usernames should be set.
if (
get(result.user, 'status.username.status') === 'UNSET' &&
!get(result.user, 'status.banned.status')
) {
dispatch(showCreateUsernameDialog());
}
})
.catch(error => {
if (error !== ErrNotLoggedIn) {
console.error(error);
}
if (error.status && error.status === 401 && storage) {
// Unauthorized.
storage.removeItem('token');
storage.removeItem('exp');
}
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
export const validForm = () => ({ type: actions.VALID_FORM });
export const invalidForm = error => ({ type: actions.INVALID_FORM, error });
//==============================================================================
// VERIFY EMAIL
//==============================================================================
const verifyEmailRequest = () => ({
type: actions.VERIFY_EMAIL_REQUEST,
});
const verifyEmailSuccess = () => ({
type: actions.VERIFY_EMAIL_SUCCESS,
});
const verifyEmailFailure = error => ({
type: actions.VERIFY_EMAIL_FAILURE,
error,
});
export const requestConfirmEmail = email => (dispatch, getState, { rest }) => {
const redirectUri = getState().auth.redirectUri;
dispatch(verifyEmailRequest());
return rest('/users/resend-verify', {
method: 'POST',
body: { email },
headers: { 'X-Pym-Url': redirectUri },
})
.then(() => {
dispatch(verifyEmailSuccess());
})
.catch(error => {
console.error(error);
dispatch(verifyEmailFailure(error));
throw error;
});
};
// Login Popup actions.
export const setRequireEmailVerification = required => ({
type: actions.SET_REQUIRE_EMAIL_VERIFICATION,
required,
});
export const setRedirectUri = uri => ({
type: actions.SET_REDIRECT_URI,
uri,
});
//==============================================================================
// Edit Username
//==============================================================================
const editUsernameFailure = error => ({
type: actions.EDIT_USERNAME_FAILURE,
error,
});
const editUsernameSuccess = () => ({ type: actions.EDIT_USERNAME_SUCCESS });
export const editName = username => (dispatch, _, { rest }) => {
return rest('/account/username', { method: 'PUT', body: { username } })
.then(() => {
dispatch(editUsernameSuccess());
dispatch(notify('success', t('framework.success_name_update')));
})
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(editUsernameFailure(errorMessage));
});
};
@@ -0,0 +1,19 @@
import * as actions from '../constants/login';
import { checkLogin } from 'coral-framework/actions/auth';
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
});
export const hideSignInDialog = () => dispatch => {
dispatch(checkLogin());
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
};
export const focusSignInDialog = () => ({
type: actions.FOCUS_SIGNIN_DIALOG,
});
export const blurSignInDialog = () => ({
type: actions.BLUR_SIGNIN_DIALOG,
});
@@ -16,17 +16,10 @@ import cn from 'classnames';
export default class Embed extends React.Component {
changeTab = tab => {
// TODO: move data fetching to appropiate containers.
switch (tab) {
case 'profile':
this.props.data.refetch();
break;
}
this.props.setActiveTab(tab);
};
getTabs() {
const { user } = this.props.auth;
const tabs = [
<Tab
key="stream"
@@ -43,7 +36,7 @@ export default class Embed extends React.Component {
{t('framework.my_profile')}
</Tab>,
];
if (can(user, 'UPDATE_ASSET_CONFIG')) {
if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) {
tabs.push(
<Tab
key="config"
@@ -64,11 +57,12 @@ export default class Embed extends React.Component {
root,
root: { asset },
data,
auth: { showSignInDialog, signInDialogFocus },
showSignInDialog,
signInDialogFocus,
blurSignInDialog,
focusSignInDialog,
hideSignInDialog,
router: { location: { query: { parentUrl } } },
parentUrl,
} = this.props;
const hasHighlightedComment = !!commentId;
@@ -81,9 +75,7 @@ export default class Embed extends React.Component {
<AutomaticAssetClosure asset={asset} />
<IfSlotIsNotEmpty slot="login">
<Popup
href={`embed/stream/login?parentUrl=${encodeURIComponent(
parentUrl
)}`}
href={`login?parentUrl=${encodeURIComponent(parentUrl)}`}
title="Login"
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
open={showSignInDialog}
@@ -138,11 +130,13 @@ export default class Embed extends React.Component {
Embed.propTypes = {
setActiveTab: PropTypes.func,
auth: PropTypes.object,
currentUser: PropTypes.object,
showSignInDialog: PropTypes.bool,
signInDialogFocus: PropTypes.bool,
blurSignInDialog: PropTypes.func,
focusSignInDialog: PropTypes.func,
hideSignInDialog: PropTypes.func,
router: PropTypes.object,
parentUrl: PropTypes.string,
commentId: PropTypes.string,
root: PropTypes.object,
activeTab: PropTypes.string,
@@ -1,57 +0,0 @@
export const CHANGE_VIEW = 'CHANGE_VIEW';
export const CLEAN_STATE = 'CLEAN_STATE';
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
export const CREATE_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
export const CREATE_USERNAME = 'CREATE_USERNAME';
export const SHOW_CREATEUSERNAME_DIALOG = 'SHOW_CREATEUSERNAME_DIALOG';
export const HIDE_CREATEUSERNAME_DIALOG = 'HIDE_CREATEUSERNAME_DIALOG';
export const EDIT_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
export const EDIT_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
export const EDIT_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
export const EDIT_USERNAME = 'CREATE_USERNAME';
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';
export const FETCH_SIGNIN_REQUEST = 'FETCH_SIGNIN_REQUEST';
export const FETCH_SIGNIN_FAILURE = 'FETCH_SIGNIN_FAILURE';
export const FETCH_SIGNIN_SUCCESS = 'FETCH_SIGNIN_SUCCESS';
export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST';
export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE';
export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS';
export const FETCH_SIGNUP_FACEBOOK_REQUEST = 'FETCH_SIGNUP_FACEBOOK_REQUEST';
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
export const LOGOUT = 'LOGOUT';
export const INVALID_FORM = 'INVALID_FORM';
export const VALID_FORM = 'VALID_FORM';
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
export const UPDATE_USERNAME = 'UPDATE_USERNAME';
// Login Popup actions.
export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION';
export const SET_REDIRECT_URI = 'SET_REDIRECT_URI';
export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG';
export const UPDATE_STATUS = 'UPDATE_STATUS';
@@ -0,0 +1,4 @@
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
@@ -1,14 +1,27 @@
import defaultTo from 'lodash/defaultTo';
const prefix = 'TALK_EMBED_STREAM';
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const VIEW_COMMENT = 'VIEW_COMMENT';
export const ADDTL_COMMENTS_ON_LOAD_MORE = parseInt(
defaultTo(process.env.TALK_ADDTL_COMMENTS_ON_LOAD_MORE, '10')
);
export const ASSET_COMMENTS_LOAD_DEPTH = parseInt(
defaultTo(process.env.TALK_ASSET_COMMENTS_LOAD_DEPTH, '10')
);
export const REPLY_COMMENTS_LOAD_DEPTH = parseInt(
defaultTo(process.env.TALK_REPLY_COMMENTS_LOAD_DEPTH, '3')
);
export const THREADING_LEVEL = parseInt(
defaultTo(process.env.TALK_THREADING_LEVEL, '3')
);
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const VIEW_COMMENT = 'VIEW_COMMENT';
@@ -8,8 +8,13 @@ import branch from 'recompose/branch';
import renderComponent from 'recompose/renderComponent';
import { Spinner } from 'coral-ui';
import * as authActions from '../actions/auth';
import * as assetActions from '../actions/asset';
import {
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
} from '../actions/login';
import { updateStatus } from 'coral-framework/actions/auth';
import { fetchAssetSuccess } from '../actions/asset';
import {
getDefinitionName,
getSlotFragmentSpreads,
@@ -24,16 +29,6 @@ import t from 'coral-framework/services/i18n';
import PropTypes from 'prop-types';
import { setActiveTab } from '../actions/embed';
const {
logout,
checkLogin,
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
updateStatus,
} = authActions;
const { fetchAssetSuccess } = assetActions;
class EmbedContainer extends React.Component {
static contextTypes = {
pym: PropTypes.object,
@@ -42,7 +37,7 @@ class EmbedContainer extends React.Component {
subscriptions = [];
subscribeToUpdates(props = this.props) {
if (props.auth.loggedIn) {
if (props.currentUser) {
const newSubscriptions = [
{
document: USER_BANNED_SUBSCRIPTION,
@@ -80,7 +75,7 @@ class EmbedContainer extends React.Component {
props.data.subscribeToMore({
document: s.document,
variables: {
user_id: props.auth.user.id,
user_id: props.currentUser.id,
},
updateQuery: s.updateQuery,
})
@@ -107,7 +102,7 @@ class EmbedContainer extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) {
if (this.props.currentUser !== nextProps.currentUser) {
// Refetch after login/logout.
this.props.data.refetch();
this.resubscribe(nextProps);
@@ -138,7 +133,23 @@ class EmbedContainer extends React.Component {
if (!this.props.root.asset) {
return <Spinner />;
}
return <Embed {...this.props} />;
return (
<Embed
setActiveTab={this.props.setActiveTab}
currentUser={this.props.currentUser}
blurSignInDialog={this.props.blurSignInDialog}
focusSignInDialog={this.props.focusSignInDialog}
hideSignInDialog={this.props.hideSignInDialog}
router={this.props.router}
commentId={this.props.commentId}
root={this.props.root}
activeTab={this.props.activeTab}
data={this.props.data}
showSignInDialog={this.props.showSignInDialog}
signInDialogFocus={this.props.signInDialogFocus}
parentUrl={this.props.parentUrl}
/>
);
}
}
@@ -255,21 +266,46 @@ const EMBED_QUERY = gql`
`;
export const withEmbedQuery = withQuery(EMBED_QUERY, {
options: ({ auth, commentId, assetId, assetUrl, sortBy, sortOrder }) => ({
options: ({
currentUser,
commentId,
assetId,
assetUrl,
sortBy,
sortOrder,
}) => ({
variables: {
assetId,
assetUrl,
commentId,
hasComment: commentId !== '',
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
excludeIgnored: Boolean(currentUser && currentUser.id),
sortBy,
sortOrder,
},
}),
});
EmbedContainer.propTypes = {
setActiveTab: PropTypes.func,
currentUser: PropTypes.object,
blurSignInDialog: PropTypes.func,
focusSignInDialog: PropTypes.func,
hideSignInDialog: PropTypes.func,
router: PropTypes.object,
commentId: PropTypes.string,
root: PropTypes.object,
activeTab: PropTypes.string,
parentUrl: PropTypes.string,
data: PropTypes.object,
fetchAssetSuccess: PropTypes.func,
showSignInDialog: PropTypes.bool,
signInDialogFocus: PropTypes.bool,
};
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
checkedInitialLogin: state.auth.checkedInitialLogin,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
@@ -277,13 +313,14 @@ const mapStateToProps = state => ({
config: state.config,
sortOrder: state.stream.sortOrder,
sortBy: state.stream.sortBy,
showSignInDialog: state.login.showSignInDialog,
signInDialogFocus: state.login.signInDialogFocus,
parentUrl: state.login.parentUrl,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
logout,
checkLogin,
setActiveTab,
fetchAssetSuccess,
notify,
@@ -297,6 +334,6 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch(props => !props.auth.checkedInitialLogin, renderComponent(Spinner)),
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
withEmbedQuery
)(EmbedContainer);
@@ -1,4 +0,0 @@
import React from 'react';
import Slot from 'coral-framework/components/Slot';
export const LoginContainer = () => <Slot fill="login" />;
+2 -47
View File
@@ -1,67 +1,22 @@
import React from 'react';
import { render } from 'react-dom';
import Embed from './containers/Embed';
import {
checkLogin,
handleAuthToken,
logout,
} from 'coral-embed-stream/src/actions/auth';
import graphqlExtension from './graphql';
import { addExternalConfig } from 'coral-embed-stream/src/actions/config';
import { createContext } from 'coral-framework/services/bootstrap';
import AppRouter from './AppRouter';
import reducers from './reducers';
import TalkProvider from 'coral-framework/components/TalkProvider';
import pluginsConfig from 'pluginsConfig';
function inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
// TODO: move init code into `bootstrap` service after auth has been refactored.
function preInit({ store, pym }) {
// TODO: This is popup specific code and needs to be refactored.
if (!inIframe()) {
store.dispatch(addExternalConfig({}));
store.dispatch(checkLogin());
return;
}
pym.onMessage('login', token => {
if (token) {
store.dispatch(handleAuthToken(token));
}
store.dispatch(checkLogin());
});
pym.onMessage('logout', () => {
store.dispatch(logout());
});
return new Promise(resolve => {
pym.sendMessage('getConfig');
pym.onMessage('config', config => {
store.dispatch(addExternalConfig(JSON.parse(config)));
store.dispatch(checkLogin());
resolve();
});
});
}
async function main() {
const context = await createContext({
reducers,
graphqlExtension,
pluginsConfig,
preInit,
});
render(
<TalkProvider {...context}>
<AppRouter />
<Embed />
</TalkProvider>,
document.querySelector('#talk-embed-stream-container')
);
@@ -1,249 +0,0 @@
import * as actions from '../constants/auth';
import pym from 'coral-framework/services/pym';
import merge from 'lodash/merge';
const initialState = {
isLoading: false,
loggedIn: false,
user: null,
showSignInDialog: false,
signInDialogFocus: false,
showCreateUsernameDialog: false,
checkedInitialLogin: false,
view: 'SIGNIN',
error: null,
passwordRequestSuccess: null,
passwordRequestFailure: null,
emailVerificationFailure: false,
emailVerificationLoading: false,
emailVerificationSuccess: false,
successSignUp: false,
fromSignUp: false,
requireEmailConfirmation: false,
redirectUri: pym.parentUrl || location.href,
};
const purge = user => {
const {settings, ...userData} = user; // eslint-disable-line
return userData;
};
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.FOCUS_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: true,
};
case actions.BLUR_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: false,
};
case actions.SHOW_SIGNIN_DIALOG:
return {
...state,
showSignInDialog: true,
signInDialogFocus: true,
};
case actions.RESET_SIGNIN_DIALOG:
case actions.HIDE_SIGNIN_DIALOG:
return {
...state,
isLoading: false,
showSignInDialog: false,
signInDialogFocus: false,
view: 'SIGNIN',
error: null,
passwordRequestFailure: null,
passwordRequestSuccess: null,
emailVerificationFailure: false,
emailVerificationSuccess: false,
emailVerificationLoading: false,
successSignUp: false,
};
case actions.SHOW_CREATEUSERNAME_DIALOG:
return {
...state,
showCreateUsernameDialog: true,
};
case actions.HIDE_CREATEUSERNAME_DIALOG:
return {
...state,
showCreateUsernameDialog: false,
};
case actions.CREATE_USERNAME_SUCCESS:
return {
...state,
showCreateUsernameDialog: false,
error: null,
};
case actions.CREATE_USERNAME_FAILURE:
return {
...state,
error: action.error,
};
case actions.CHANGE_VIEW:
return {
...state,
error: action.error,
view: action.view,
};
case actions.CLEAN_STATE:
return initialState;
case actions.FETCH_SIGNIN_REQUEST:
return {
...state,
email: action.email,
isLoading: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
checkedInitialLogin: true,
loggedIn: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
checkedInitialLogin: true,
loggedIn: true,
user: purge(action.user),
};
case actions.FETCH_SIGNIN_SUCCESS:
return {
...state,
loggedIn: true,
user: purge(action.user),
};
case actions.FETCH_SIGNIN_FAILURE:
return {
...state,
isLoading: false,
error: action.error,
user: null,
view:
action.error.translation_key === 'EMAIL_NOT_VERIFIED'
? 'RESEND_VERIFICATION'
: state.view,
};
case actions.FETCH_SIGNUP_FACEBOOK_REQUEST:
return {
...state,
fromSignUp: true,
};
case actions.FETCH_SIGNIN_FACEBOOK_REQUEST:
return {
...state,
fromSignUp: false,
};
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return {
...state,
loggedIn: true,
user: purge(action.user),
};
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
return {
...state,
error: action.error,
user: null,
};
case actions.FETCH_SIGNUP_REQUEST:
return {
...state,
isLoading: true,
};
case actions.FETCH_SIGNUP_FAILURE:
return {
...state,
error: action.error,
isLoading: false,
};
case actions.FETCH_SIGNUP_SUCCESS:
return {
...state,
isLoading: false,
successSignUp: true,
};
case actions.LOGOUT:
return {
...state,
user: null,
isLoading: false,
loggedIn: false,
};
case actions.INVALID_FORM:
return {
...state,
error: action.error,
};
case actions.VALID_FORM:
return {
...state,
error: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestFailure: null,
passwordRequestSuccess:
'If you have a registered account, a password reset link was sent to that email',
};
case actions.FETCH_FORGOT_PASSWORD_FAILURE:
return {
...state,
passwordRequestFailure:
'There was an error sending your password reset email. Please try again soon!',
passwordRequestSuccess: null,
};
case actions.UPDATE_USERNAME:
return {
...state,
user: {
...state.user,
username: action.username,
lowercaseUsername: action.username.toLowerCase(),
},
};
case actions.VERIFY_EMAIL_FAILURE:
return {
...state,
emailVerificationFailure: action.error,
emailVerificationLoading: false,
};
case actions.VERIFY_EMAIL_REQUEST:
return {
...state,
emailVerificationLoading: true,
};
case actions.VERIFY_EMAIL_SUCCESS:
return {
...state,
emailVerificationSuccess: true,
emailVerificationLoading: false,
};
case actions.SET_REQUIRE_EMAIL_VERIFICATION:
return {
...state,
requireEmailConfirmation: action.required,
};
case actions.SET_REDIRECT_URI:
return {
...state,
redirectUri: action.uri,
};
case actions.UPDATE_STATUS: {
return {
...state,
user: {
...state.user,
status: merge({}, state.user.status, action.status),
},
};
}
default:
return state;
}
}
@@ -1,15 +1,13 @@
import auth from './auth';
import login from './login';
import asset from './asset';
import embed from './embed';
import config from './config';
import configure from './configure';
import stream from './stream';
export default {
auth,
login,
asset,
embed,
config,
configure,
stream,
};
@@ -0,0 +1,38 @@
import * as actions from '../constants/login';
import pym from 'coral-framework/services/pym';
const initialState = {
parentUrl: pym.parentUrl || location.href,
showSignInDialog: false,
signInDialogFocus: false,
};
export default function login(state = initialState, action) {
switch (action.type) {
case actions.FOCUS_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: true,
};
case actions.BLUR_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: false,
};
case actions.SHOW_SIGNIN_DIALOG:
return {
...state,
showSignInDialog: true,
signInDialogFocus: true,
};
case actions.HIDE_SIGNIN_DIALOG:
return {
...state,
showSignInDialog: false,
signInDialogFocus: false,
};
default:
return state;
}
}
@@ -1,5 +1,5 @@
import * as actions from '../constants/stream';
import * as authActions from '../constants/auth';
import * as authActions from 'coral-framework/constants/auth';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
@@ -10,12 +10,7 @@ import NotLoggedIn from '../components/NotLoggedIn';
import { Spinner } from 'coral-ui';
import CommentHistory from '../components/CommentHistory';
// TODO: Auth logic needs refactoring.
import {
showSignInDialog,
checkLogin,
} from 'coral-embed-stream/src/actions/auth';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
@@ -24,7 +19,7 @@ import t from 'coral-framework/services/i18n';
class ProfileContainer extends Component {
componentWillReceiveProps(nextProps) {
if (!this.props.auth.loggedIn && nextProps.auth.loggedIn) {
if (!this.props.currentUser && nextProps.currentUser) {
// Refetch after login.
this.props.data.refetch();
}
@@ -55,13 +50,7 @@ class ProfileContainer extends Component {
};
render() {
const {
auth,
auth: { user: authUser },
showSignInDialog,
root,
data,
} = this.props;
const { currentUser, showSignInDialog, root, data } = this.props;
const { me } = this.props.root;
const loading = this.props.data.loading;
@@ -69,7 +58,7 @@ class ProfileContainer extends Component {
return <div>{this.props.data.error.message}</div>;
}
if (!auth.loggedIn) {
if (!currentUser) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
@@ -77,7 +66,7 @@ class ProfileContainer extends Component {
return <Spinner />;
}
const localProfile = authUser.profiles.find(p => p.provider === 'local');
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
const emailAddress = localProfile && localProfile.id;
return (
@@ -168,15 +157,20 @@ const withProfileQuery = withQuery(
${getSlotFragmentSpreads(slots, 'root')}
}
${CommentFragment}
`
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog, checkLogin }, dispatch);
bindActionCreators({ showSignInDialog }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'coral-ui';
import cn from 'classnames';
import Slot from 'coral-framework/components/Slot';
// TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest.
import { name } from '../containers/CommentBox';
import styles from './CommentForm.css';
import t from 'coral-framework/services/i18n';
import DraftArea from '../containers/DraftArea';
/**
* Common UI for Creating or Editing a Comment
@@ -61,10 +61,6 @@ export class CommentForm extends React.Component {
};
}
onBodyChange = e => {
this.props.onBodyChange(e.target.value);
};
onClickSubmit = () => {
this.props.onSubmit();
};
@@ -107,37 +103,16 @@ export class CommentForm extends React.Component {
return (
<div>
<div className={`${name}-container`}>
<label
htmlFor={this.props.bodyInputId}
className="screen-reader-text"
aria-hidden={true}
>
{this.props.bodyLabel}
</label>
<textarea
className={`${name}-textarea`}
value={body}
placeholder={this.props.bodyPlaceholder}
id={this.props.bodyInputId}
onChange={this.onBodyChange}
rows={3}
disabled={disableTextArea}
/>
<Slot fill="commentInputArea" />
</div>
{this.props.charCountEnable && (
<div
className={`${name}-char-count ${
length > maxCharCount ? `${name}-char-max` : ''
}`}
>
{maxCharCount &&
`${maxCharCount - length} ${t(
'comment_box.characters_remaining'
)}`}
</div>
)}
<DraftArea
id={this.props.bodyInputId}
label={this.props.bodyLabel}
value={body}
placeholder={this.props.bodyPlaceholder}
onChange={this.props.onBodyChange}
disabled={disableTextArea}
charCountEnable={this.props.charCountEnable}
maxCharCount={this.props.maxCharCount}
/>
<div className={`${name}-button-container`}>
{this.props.buttonContainerStart}
{typeof this.props.onCancel === 'function' && (
@@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import Slot from 'coral-framework/components/Slot';
// TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest.
/**
* An enhanced textarea to make comment drafts.
*/
export default class DraftArea extends React.Component {
renderCharCount() {
const { value, maxCharCount } = this.props;
const className = cn('talk-plugin-commentbox-char-count', {
['talk-plugin-commentbox-char-max']: value.length > maxCharCount,
});
const remaining = maxCharCount - value.length;
return (
<div className={className}>
{remaining} {t('comment_box.characters_remaining')}
</div>
);
}
render() {
const {
value,
placeholder,
id,
disabled,
rows,
label,
charCountEnable,
maxCharCount,
onChange,
} = this.props;
return (
<div>
<div className={'talk-plugin-commentbox-container'}>
<label htmlFor={id} className="screen-reader-text" aria-hidden={true}>
{label}
</label>
<textarea
className={'talk-plugin-commentbox-textarea'}
value={value}
placeholder={placeholder}
id={id}
onChange={onChange}
rows={rows}
disabled={disabled}
/>
<Slot fill="commentInputArea" />
</div>
{charCountEnable && maxCharCount > 0 && this.renderCharCount()}
</div>
);
}
}
DraftArea.defaultProps = {
rows: 3,
};
DraftArea.propTypes = {
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
id: PropTypes.string,
value: PropTypes.string,
placeholder: PropTypes.string,
label: PropTypes.string,
onChange: PropTypes.func,
disabled: PropTypes.bool,
rows: PropTypes.number,
};
@@ -21,6 +21,7 @@ export class EditableCommentContent extends React.Component {
// comment that is being edited
comment: PropTypes.shape({
id: PropTypes.string,
body: PropTypes.string,
editing: PropTypes.shape({
edited: PropTypes.bool,
@@ -120,10 +121,12 @@ export class EditableCommentContent extends React.Component {
};
render() {
const id = `edit-draft_${this.props.comment.id}`;
return (
<div className={styles.editCommentForm}>
<CommentForm
defaultValue={this.props.comment.body}
bodyInputId={id}
charCountEnable={this.props.charCountEnable}
maxCharCount={this.props.maxCharCount}
submitEnabled={this.isSubmitEnabled}
@@ -7,7 +7,8 @@ const name = 'talk-plugin-replies';
class ReplyBox extends Component {
componentDidMount() {
document.getElementById('replyText').focus();
// TODO: (kiwi) This does not follow best practices, better to move this logic into the component.
document.getElementById(`comment-draft_${this.props.parentId}`).focus();
}
cancelReply = () => {
@@ -54,6 +55,8 @@ ReplyBox.propTypes = {
notify: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired,
currentUser: PropTypes.object,
styles: PropTypes.object,
};
export default ReplyBox;
@@ -59,7 +59,7 @@ class Stream extends React.Component {
deleteAction,
showSignInDialog,
loadNewReplies,
auth: { user },
currentUser,
emit,
viewAllComments,
} = this.props;
@@ -111,7 +111,7 @@ class Stream extends React.Component {
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
currentUser={currentUser}
highlighted={comment.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
@@ -150,7 +150,7 @@ class Stream extends React.Component {
setActiveStreamTab,
loadNewReplies,
loadMoreComments,
auth: { user },
currentUser,
emit,
sortOrder,
sortBy,
@@ -200,7 +200,7 @@ class Stream extends React.Component {
notify={notify}
disableReply={asset.isClosed}
postComment={postComment}
currentUser={user}
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadMoreComments}
@@ -230,21 +230,23 @@ class Stream extends React.Component {
postComment,
notify,
updateItem,
auth: { loggedIn, user },
currentUser,
} = this.props;
const { keepCommentBox } = this.state;
const open = !asset.isClosed;
const banned = get(user, 'status.banned.status');
const suspensionUntil = get(user, 'status.suspension.until');
const rejectedUsername = get(user, 'status.username.status') === 'REJECTED';
const changedUsername = get(user, 'status.username.status') === 'CHANGED';
const banned = get(currentUser, 'status.banned.status');
const suspensionUntil = get(currentUser, 'status.suspension.until');
const rejectedUsername =
get(currentUser, 'status.username.status') === 'REJECTED';
const changedUsername =
get(currentUser, 'status.username.status') === 'CHANGED';
const temporarilySuspended =
user && suspensionUntil && new Date(suspensionUntil) > new Date();
currentUser && suspensionUntil && new Date(suspensionUntil) > new Date();
const showCommentBox =
loggedIn &&
currentUser &&
((!banned &&
!temporarilySuspended &&
!rejectedUsername &&
@@ -289,7 +291,8 @@ class Stream extends React.Component {
</RestrictedMessageBox>
)}
{changedUsername && <ChangedUsername />}
{!banned && rejectedUsername && <ChangeUsername user={user} />}
{!banned &&
rejectedUsername && <ChangeUsername user={currentUser} />}
{banned && <BannedAccount />}
{showCommentBox && (
<CommentBox
@@ -300,7 +303,7 @@ class Stream extends React.Component {
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
currentUser={user}
currentUser={currentUser}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>
@@ -312,10 +315,10 @@ class Stream extends React.Component {
<Slot fill="stream" queryData={slotQueryData} {...slotProps} />
{loggedIn && (
{currentUser && (
<ModerationLink
assetId={asset.id}
isAdmin={can(user, 'MODERATE_COMMENTS')}
isAdmin={can(currentUser, 'MODERATE_COMMENTS')}
/>
)}
@@ -342,12 +345,11 @@ Stream.propTypes = {
deleteAction: PropTypes.func,
showSignInDialog: PropTypes.func,
loadNewReplies: PropTypes.func,
auth: PropTypes.object,
currentUser: PropTypes.object,
emit: PropTypes.func,
sortOrder: PropTypes.string,
sortBy: PropTypes.string,
loading: PropTypes.bool,
editName: PropTypes.func,
appendItemArray: PropTypes.func,
updateItem: PropTypes.func,
viewAllComments: PropTypes.func,
@@ -4,7 +4,10 @@ import Comment from '../components/Comment';
import { withFragments } from 'coral-framework/hocs';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { withSetCommentStatus } from 'coral-framework/graphql/mutations';
import { THREADING_LEVEL } from '../../../constants/stream';
import {
THREADING_LEVEL,
REPLY_COMMENTS_LOAD_DEPTH,
} from '../../../constants/stream';
import hoistStatics from 'recompose/hoistStatics';
import { nest } from '../../../graphql/utils';
@@ -118,7 +121,7 @@ const withCommentFragments = withFragments({
...CoralEmbedStream_Comment_SingleComment
${nest(
`
replies(query: {limit: 3, excludeIgnored: $excludeIgnored}) {
replies(query: {limit: ${REPLY_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored}) {
nodes {
...CoralEmbedStream_Comment_SingleComment
...nest
@@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import t, { timeago } from 'coral-framework/services/i18n';
import { can } from 'coral-framework/services/perms';
import { isSuspended } from 'coral-framework/utils/user';
import Slot from 'coral-framework/components/Slot';
import { connect } from 'react-redux';
@@ -20,7 +21,6 @@ class CommentBox extends React.Component {
super(props);
this.state = {
username: '',
body: '',
loadingState: '',
@@ -41,6 +41,17 @@ class CommentBox extends React.Component {
currentUser,
} = this.props;
if (isSuspended(currentUser)) {
notify(
'error',
t(
'error.temporarily_suspended',
timeago(currentUser.status.suspension.until)
)
);
return;
}
if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) {
notify('error', t('error.NOT_AUTHORIZED'));
return;
@@ -129,7 +140,7 @@ class CommentBox extends React.Component {
};
render() {
const { isReply, maxCharCount } = this.props;
const { isReply, maxCharCount, assetId, parentId } = this.props;
let { onCancel } = this.props;
if (isReply && typeof onCancel !== 'function') {
@@ -139,6 +150,11 @@ class CommentBox extends React.Component {
onCancel = () => {};
}
// Generate id for the DraftArea.
const id = parentId
? `comment-draft_${parentId}`
: `comment-draft_${assetId}`;
return (
<div>
<CommentForm
@@ -147,7 +163,7 @@ class CommentBox extends React.Component {
maxCharCount={maxCharCount}
charCountEnable={this.props.charCountEnable}
bodyPlaceholder={t('comment.comment')}
bodyInputId={isReply ? 'replyText' : 'commentText'}
bodyInputId={id}
body={this.state.body}
buttonContainerStart={
<Slot
@@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';
import DraftArea from '../components/DraftArea';
const STORAGE_PATH = 'DraftArea';
/**
* An enhanced textarea to make comment drafts.
*/
export default class DraftAreaContainer extends React.Component {
constructor(props, context) {
super(props, context);
this.initValue();
}
async initValue() {
const value = await this.context.pymSessionStorage.getItem(this.getPath());
if (value && this.props.onChange) {
this.props.onChange(value);
}
}
getPath = () => {
return `${STORAGE_PATH}_${this.props.id}`;
};
onChange = e => {
this.props.onChange && this.props.onChange(e.target.value);
};
componentWillReceiveProps(nextProps) {
if (this.props.value !== nextProps.value) {
if (nextProps.value) {
this.context.pymSessionStorage.setItem(this.getPath(), nextProps.value);
} else {
this.context.pymSessionStorage.removeItem(this.getPath());
}
}
}
render() {
return (
<DraftArea
value={this.props.value}
placeholder={this.props.placeholder}
id={this.props.id}
onChange={this.onChange}
rows={this.props.rows}
disabled={this.props.disabled}
charCountEnable={this.props.charCountEnable}
maxCharCount={this.props.maxCharCount}
label={this.props.label}
/>
);
}
}
DraftAreaContainer.contextTypes = {
// We use pymSessionStorage instead to persist the data directly on the parent page,
// in order to mitigate strict cross domain security settings.
pymSessionStorage: PropTypes.object,
};
DraftAreaContainer.propTypes = {
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
placeholder: PropTypes.string,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
rows: PropTypes.number,
label: PropTypes.string.isRequired,
};
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gql, compose } from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
ADDTL_COMMENTS_ON_LOAD_MORE,
ASSET_COMMENTS_LOAD_DEPTH,
THREADING_LEVEL,
} from '../../../constants/stream';
import {
@@ -14,8 +16,8 @@ import {
withEditComment,
} from 'coral-framework/graphql/mutations';
import * as authActions from 'coral-embed-stream/src/actions/auth';
import * as notificationActions from 'coral-framework/actions/notification';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
import { notify } from 'coral-framework/actions/notification';
import {
setActiveReplyBox,
setActiveTab,
@@ -40,9 +42,6 @@ import {
} from '../../../graphql/utils';
import StreamError from '../components/StreamError';
const { showSignInDialog, editName } = authActions;
const { notify } = notificationActions;
class StreamContainer extends React.Component {
commentsAddedSubscription = null;
commentsEditedSubscription = null;
@@ -60,8 +59,8 @@ class StreamContainer extends React.Component {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (
this.props.auth.user &&
commentEdited.user.id === this.props.auth.user.id
this.props.currentUser &&
commentEdited.user.id === this.props.currentUser.id
) {
return prev;
}
@@ -92,8 +91,8 @@ class StreamContainer extends React.Component {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (
this.props.auth.user &&
commentAdded.user.id === this.props.auth.user.id
this.props.currentUser &&
commentAdded.user.id === this.props.currentUser.id
) {
return prev;
}
@@ -204,8 +203,8 @@ class StreamContainer extends React.Component {
}
}
userIsDegraged({ auth: { user } } = this.props) {
return !can(user, 'INTERACT_WITH_COMMUNITY');
userIsDegraged({ currentUser } = this.props) {
return !can(currentUser, 'INTERACT_WITH_COMMUNITY');
}
render() {
@@ -225,8 +224,28 @@ class StreamContainer extends React.Component {
return (
<Stream
{...this.props}
loadMore={this.loadMore}
asset={this.props.asset}
activeStreamTab={this.props.activeStreamTab}
data={this.props.data}
root={this.props.root}
activeReplyBox={this.props.activeReplyBox}
setActiveReplyBox={this.props.setActiveReplyBox}
commentClassNames={this.props.commentClassNames}
setActiveStreamTab={this.props.setActiveStreamTab}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
currentUser={this.props.currentUser}
emit={this.props.emit}
sortOrder={this.props.sortOrder}
sortBy={this.props.sortBy}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
viewAllComments={this.props.viewAllComments}
notify={this.props.notify}
postComment={this.props.postComment}
editComment={this.props.editComment}
loadMoreComments={this.loadMoreComments}
loadNewReplies={this.loadNewReplies}
userIsDegraged={this.userIsDegraged()}
@@ -236,6 +255,33 @@ class StreamContainer extends React.Component {
}
}
StreamContainer.propTypes = {
asset: PropTypes.object,
activeStreamTab: PropTypes.string,
data: PropTypes.object,
root: PropTypes.object,
activeReplyBox: PropTypes.string,
setActiveReplyBox: PropTypes.func,
commentClassNames: PropTypes.array,
setActiveStreamTab: PropTypes.func,
postFlag: PropTypes.func,
postDontAgree: PropTypes.func,
deleteAction: PropTypes.func,
showSignInDialog: PropTypes.func,
currentUser: PropTypes.object,
emit: PropTypes.func,
sortOrder: PropTypes.string,
sortBy: PropTypes.string,
loading: PropTypes.bool,
appendItemArray: PropTypes.func,
updateItem: PropTypes.func,
viewAllComments: PropTypes.func,
notify: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
editComment: PropTypes.func,
previousTab: PropTypes.string,
};
const commentFragment = gql`
fragment CoralEmbedStream_Stream_comment on Comment {
id
@@ -379,7 +425,7 @@ const fragments = {
requireEmailConfirmation
}
totalCommentCount @skip(if: $hasComment)
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
comments(query: {limit: ${ASSET_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
nodes {
...CoralEmbedStream_Stream_comment
}
@@ -396,7 +442,7 @@ const fragments = {
};
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
@@ -417,7 +463,6 @@ const mapDispatchToProps = dispatch =>
showSignInDialog,
notify,
setActiveReplyBox,
editName,
viewAllComments,
setActiveStreamTab: setActiveTab,
},
@@ -72,17 +72,6 @@ body {
font-weight: bold;
}
/* Coral sign in button */
#coralSignInButton {
background-color: #2a2a2a;
color: #FFF;
}
#coralSignInButton:hover {
background-color: #767676;
}
/* Info Box Styles */
+13 -2
View File
@@ -146,8 +146,19 @@ export default class Stream {
// If the user clicks outside the embed, then tell the embed.
document.addEventListener('click', this.handleClick.bind(this), true);
// Listens to storage requests on pym and relay it to local storage.
connectStorageToPym(createStorage(), this.pym);
// Listens to local storage requests on pym and relay it to local storage.
connectStorageToPym(
createStorage('localStorage'),
this.pym,
'localStorage'
);
// Listens to session storage requests on pym and relay it to session storage.
connectStorageToPym(
createStorage('sessionStorage'),
this.pym,
'sessionStorage'
);
}
login(token) {
+110
View File
@@ -0,0 +1,110 @@
import * as actions from '../constants/auth';
import jwtDecode from 'jwt-decode';
function cleanAuthData(localStorage) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
export const checkLogin = () => (
dispatch,
_,
{ rest, client, pym, localStorage }
) => {
dispatch(checkLoginRequest());
rest('/auth')
.then(result => {
if (!result.user) {
if (localStorage) {
cleanAuthData(localStorage);
}
dispatch(checkLoginSuccess(null));
return;
}
// Reset the websocket.
client.resetWebsocket();
dispatch(checkLoginSuccess(result.user));
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
})
.catch(error => {
if (error.status && error.status === 401 && localStorage) {
// Unauthorized.
cleanAuthData(localStorage);
} else {
console.error(error);
}
dispatch(checkLoginFailure(error));
});
};
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error,
});
const checkLoginSuccess = user => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
});
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
if (localStorage) {
localStorage.setItem('exp', jwtDecode(token).exp);
localStorage.setItem('token', token);
}
dispatch(checkLogin());
};
export const handleSuccessfulLogin = (user, token) => (
dispatch,
_,
{ client, localStorage }
) => {
if (localStorage) {
localStorage.setItem('exp', jwtDecode(token).exp);
localStorage.setItem('token', token);
}
client.resetWebsocket();
dispatch({
type: actions.HANDLE_SUCCESSFUL_LOGIN,
user,
});
};
/**
* Logout
*/
export const logout = () => async (
dispatch,
_,
{ rest, client, pym, localStorage }
) => {
await rest('/auth', { method: 'DELETE' });
if (localStorage) {
cleanAuthData(localStorage);
}
// Reset the websocket.
client.resetWebsocket();
dispatch({ type: actions.LOGOUT });
pym.sendMessage('coral-auth-changed');
};
export const updateStatus = status => ({
type: actions.UPDATE_STATUS,
status,
});
export const updateUsername = username => ({
type: actions.UPDATE_USERNAME,
username,
});
+6
View File
@@ -0,0 +1,6 @@
import { MERGE_CONFIG } from '../constants/config';
export const mergeConfig = config => ({
type: MERGE_CONFIG,
config,
});
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactRecaptcha from 'react-recaptcha';
class Recaptcha extends React.Component {
static contextTypes = {
store: PropTypes.object,
};
ref = null;
handleRef = ref => {
this.ref = ref;
};
reset = () => this.ref.reset();
getSiteKey() {
// This should be fine because it's static and will never change.
// Prefer this to connect HOC because wie expose the instance method
// `reset`
return this.context.store.getState().config.static.TALK_RECAPTCHA_PUBLIC;
}
render() {
return (
<ReactRecaptcha
ref={this.handleRef}
sitekey={this.getSiteKey()}
render={this.props.render}
theme={this.props.theme}
onloadCallback={this.props.onLoad}
verifyCallback={this.props.onVerify}
size={this.props.size}
className={this.props.className}
/>
);
}
}
Recaptcha.defaultProps = {
render: 'explicit',
theme: 'light',
size: 'normal',
};
Recaptcha.propTypes = {
onLoad: PropTypes.func,
onVerify: PropTypes.func.isRequired,
theme: PropTypes.string,
render: PropTypes.string,
size: PropTypes.string,
className: PropTypes.string,
};
export default Recaptcha;
+2 -1
View File
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';
import { getShallowChanges } from 'coral-framework/utils';
const emptyConfig = {};
@@ -68,7 +69,7 @@ class Slot extends React.Component {
} = this.props;
const { plugins } = this.context;
let children = this.getChildren();
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
const pluginConfig = get(reduxState, 'config.pluginConfig') || emptyConfig;
if (children.length === 0 && DefaultComponent) {
const props = plugins.getSlotComponentProps(
DefaultComponent,
@@ -11,9 +11,12 @@ class TalkProvider extends React.Component {
rest: this.props.rest,
graphql: this.props.graphql,
notification: this.props.notification,
storage: this.props.storage,
localStorage: this.props.localStorage,
sessionStorage: this.props.sessionStorage,
history: this.props.history,
store: this.props.store,
pymLocalStorage: this.props.pymLocalStorage,
pymSessionStorage: this.props.pymSessionStorage,
};
}
@@ -30,9 +33,14 @@ TalkProvider.childContextTypes = {
rest: PropTypes.func,
graphql: PropTypes.object,
notification: PropTypes.object,
storage: PropTypes.object,
localStorage: PropTypes.object,
sessionStorage: PropTypes.object,
pymLocalStorage: PropTypes.object,
pymSessionStorage: PropTypes.object,
history: PropTypes.object,
store: PropTypes.object,
};
TalkProvider.propTypes = TalkProvider.childContextTypes;
export default TalkProvider;
+11
View File
@@ -0,0 +1,11 @@
const prefix = `TALK_FRAMEWORK`;
export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`;
export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`;
export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`;
export const LOGOUT = `${prefix}_LOGOUT`;
export const HANDLE_SUCCESSFUL_LOGIN = `${prefix}_HANDLE_SUCCESSFUL_LOGIN`;
export const UPDATE_STATUS = '${prefix}_UPDATE_STATUS';
export const UPDATE_USERNAME = '${prefix}_UPDATE_USERNAME';
@@ -0,0 +1,3 @@
const prefix = `TALK_FRAMEWORK`;
export const MERGE_CONFIG = `${prefix}_MERGE_CONFIG`;
+7
View File
@@ -6,3 +6,10 @@ export { default as withEmit } from './withEmit';
export { default as excludeIf } from './excludeIf';
export { default as connect } from './connect';
export { default as withMergedSettings } from './withMergedSettings';
export { default as withSignIn } from './withSignIn';
export { default as withSignUp } from './withSignUp';
export { default as withForgotPassword } from './withForgotPassword';
export { default as withSetUsername } from './withSetUsername';
export {
default as withResendEmailConfirmation,
} from './withResendEmailConfirmation';
@@ -0,0 +1,68 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { translateError } from '../utils';
/**
* WithForgotPassword provides properties
* `forgotPasssword`,
* `loading`,
* `errorMessage`,
* `success`.
*/
export default hoistStatics(WrappedComponent => {
class WithForgotPassword extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
pym: PropTypes.object,
};
state = {
error: null,
loading: false,
success: false,
};
forgotPassword = (email, redirectUri) => {
if (!redirectUri) {
redirectUri = this.context.pym.parentUrl || location.href;
}
const { rest } = this.context;
this.setState({ loading: true, error: null, success: false });
rest('/account/password/reset', {
method: 'POST',
body: { email, loc: redirectUri },
})
.then(() => {
this.setState({ loading: false, error: null, success: true });
})
.catch(error => {
console.error(error);
this.setState({ loading: false, error });
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
forgotPassword={this.forgotPassword}
success={this.state.success}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
/>
);
}
}
return WithForgotPassword;
});
@@ -0,0 +1,69 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { translateError } from '../utils';
/**
* WithResendEmailConfirmaton provides properties
* `resendEmailConfirmation`,
* `loading`,
* `errorMessage`,
* `success`.
*/
export default hoistStatics(WrappedComponent => {
class WithResendEmailConfirmaton extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
pym: PropTypes.object,
};
state = {
error: null,
loading: false,
success: false,
};
resendEmailConfirmation = (email, redirectUri) => {
if (!redirectUri) {
redirectUri = this.context.pym.parentUrl || location.href;
}
const { rest } = this.context;
this.setState({ loading: true, error: null, success: false });
rest('/users/resend-verify', {
method: 'POST',
body: { email },
headers: { 'X-Pym-Url': redirectUri },
})
.then(() => {
this.setState({ loading: false, error: null, success: true });
})
.catch(error => {
console.error(error);
this.setState({ loading: false, error });
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
resendEmailConfirmation={this.resendEmailConfirmation}
success={this.state.success}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
/>
);
}
}
return WithResendEmailConfirmaton;
});
@@ -0,0 +1,100 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { getErrorMessages } from '../utils';
import validate from '../helpers/validate';
import errorMsg from 'coral-framework/helpers/error';
import t from '../services/i18n';
import { withSetUsername as withSetUsernameMutation } from 'coral-framework/graphql/mutations';
import { updateUsername, updateStatus } from '../actions/auth';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import get from 'lodash/get';
/**
* withSetUsername provides properties
* `setUsername`,
* `loading`,
* `errorMessage`,
* `requireEmailVerification`,
* `success`,
* `validateUsername`.
*/
const withSetUsername = hoistStatics(WrappedComponent => {
class WithSetUsername extends React.Component {
static propTypes = {
setUsername: PropTypes.func.isRequired,
currentUserId: PropTypes.string,
updateUsername: PropTypes.func.isRequired,
updateStatus: PropTypes.func.isRequired,
};
state = {
error: null,
loading: false,
success: false,
};
validateUsername = value => {
if (!value) {
return t('error.required_field');
}
return validate.username(value) ? null : errorMsg.username;
};
setUsername = async username => {
if (!this.props.currentUserId) {
throw new Error('User not logged in');
}
try {
await this.props.setUsername(this.props.currentUserId, username);
this.props.updateUsername(username);
this.props.updateStatus({ username: { status: 'SET' } });
this.setState({ success: true, loading: false, error: null });
} catch (error) {
if (!error.status || error.status !== 401) {
console.error(error);
}
const changeSet = { success: false, loading: false, error };
this.setState(changeSet);
}
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return getErrorMessages(this.state.error).join(', ');
}
render() {
return (
<WrappedComponent
{...this.props}
setUsername={this.setUsername}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
success={this.state.success}
validateUsername={this.validateUsername}
/>
);
}
}
return WithSetUsername;
});
const mapStateToProps = ({ auth }) => ({
currentUserId: get(auth, 'user.id'),
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ updateUsername, updateStatus }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withSetUsernameMutation,
withSetUsername
);
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { handleSuccessfulLogin } from '../actions/auth';
import { translateError } from '../utils';
import { t } from '../services/i18n';
/**
* WithSignIn provides properties
* `signIn`
* `loading`
* `errorMessage`
* `requireRecaptcha`
* `requireEmailConfirmation`
* 'success'
*/
export default hoistStatics(WrappedComponent => {
class WithSignIn extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
};
state = {
error: null,
loading: false,
success: false,
requireRecaptcha: false,
requireEmailConfirmation: false,
};
signIn = (email, password, recaptchaResponse) => {
const { store, rest } = this.context;
const params = {
method: 'POST',
body: {
email,
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse,
};
}
rest('/auth/local', params)
.then(({ user, token }) => {
this.setState({ success: true, loading: false, error: null });
store.dispatch(handleSuccessfulLogin(user, token));
})
.catch(error => {
if (!error.status || error.status !== 401) {
console.error(error);
}
const changeSet = { success: false, loading: false, error };
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
changeSet.requireRecaptcha = !!this.context.store.getState().config
.static.TALK_RECAPTCHA_PUBLIC;
} else if (error.translation_key === 'EMAIL_NOT_VERIFIED') {
changeSet.requireEmailConfirmation = true;
}
this.setState(changeSet);
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return this.state.error.translation_key === 'NOT_AUTHORIZED'
? t('error.email_password')
: translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
signIn={this.signIn}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
requireRecaptcha={this.state.requireRecaptcha}
requireEmailConfirmation={this.state.requireEmailConfirmation}
success={this.state.success}
/>
);
}
}
return WithSignIn;
});
+124
View File
@@ -0,0 +1,124 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import { compose, gql } from 'react-apollo';
import PropTypes from 'prop-types';
import { translateError } from '../utils';
import validate from '../helpers/validate';
import errorMsg from 'coral-framework/helpers/error';
import t from '../services/i18n';
import withQuery from './withQuery';
import get from 'lodash/get';
const requiredFields = ['username', 'email', 'password'];
const allFields = requiredFields;
const QUERY = gql`
query TalkFramework_WithSignUpQuery {
settings {
requireEmailConfirmation
}
}
`;
export const withSettingsQuery = withQuery(QUERY);
/**
* withSignUp provides properties
* `signUp`,
* `loading`,
* `errorMessage`,
* `requireEmailVerification`,
* `success`,
* `validate`.
*/
const withSignUp = hoistStatics(WrappedComponent => {
class WithSignUp extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
pym: PropTypes.object,
};
static propTypes = {
root: PropTypes.object.isRequired,
};
state = {
error: null,
loading: false,
success: false,
};
validate = (field, value) => {
if (!allFields.includes(field)) {
return null;
}
if (requiredFields.includes(field) && !value) {
return t('error.required_field');
}
if (field in validate) {
return validate[field](value) ? null : errorMsg[field];
}
return null;
};
signUp = ({ username, email, password }, redirectUri) => {
if (!redirectUri) {
redirectUri = this.context.pym.parentUrl || location.href;
}
const { rest } = this.context;
const params = {
method: 'POST',
body: {
username,
email,
password,
},
headers: { 'X-Pym-Url': redirectUri },
};
rest('/users', params)
.then(() => {
this.setState({ success: true, loading: false, error: null });
})
.catch(error => {
if (!error.status || error.status !== 401) {
console.error(error);
}
const changeSet = { success: false, loading: false, error };
this.setState(changeSet);
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
signUp={this.signUp}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
requireEmailConfirmation={
!!get(this.props, 'root.settings.requireEmailConfirmation')
}
success={this.state.success}
validate={this.validate}
/>
);
}
}
return WithSignUp;
});
export default compose(withSettingsQuery, withSignUp);
+61
View File
@@ -0,0 +1,61 @@
import * as actions from '../constants/auth';
import merge from 'lodash/merge';
const initialState = {
checkedInitialLogin: false,
initialLoginError: null,
user: null,
};
const purge = user => {
const {settings, ...userData} = user; // eslint-disable-line
return userData;
};
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
initialLoginError: action.error,
checkedInitialLogin: true,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
checkedInitialLogin: true,
user: action.user ? purge(action.user) : null,
};
case actions.HANDLE_SUCCESSFUL_LOGIN:
return {
...state,
user: action.user ? purge(action.user) : null,
};
case actions.LOGOUT:
return {
...state,
user: null,
};
case actions.UPDATE_STATUS: {
return {
...state,
user: {
...state.user,
status: merge({}, state.user.status, action.status),
},
};
}
case actions.UPDATE_USERNAME:
return {
...state,
user: {
...state.user,
username: action.username,
lowercaseUsername: action.username.toLowerCase(),
},
};
default:
return state;
}
}
@@ -1,15 +1,13 @@
import * as actions from '../actions/config';
import { MERGE_CONFIG } from '../constants/config';
const initialState = {
data: {},
};
const initialState = {};
export default function config(state = initialState, action) {
switch (action.type) {
case actions.CONFIG_UPDATED:
case MERGE_CONFIG:
return {
...state,
data: action.data,
...action.config,
};
default:
return state;
+8
View File
@@ -0,0 +1,8 @@
import auth from './auth';
import config from './config';
export default {
auth,
login: auth,
config,
};
+78 -9
View File
@@ -22,6 +22,10 @@ import {
import { createHistory } from 'coral-framework/services/history';
import { createIntrospection } from 'coral-framework/services/introspection';
import introspectionData from 'coral-framework/graphql/introspection.json';
import coreReducers from '../reducers';
import { checkLogin as checkLoginAction } from '../actions/auth';
import { mergeConfig } from '../actions/config';
import { setAuthToken, logout } from '../actions/auth';
/**
* getAuthToken returns the active auth token or null
@@ -32,7 +36,7 @@ import introspectionData from 'coral-framework/graphql/introspection.json';
const getAuthToken = (store, storage) => {
let state = store.getState();
if (state.config.auth_token) {
if (state.config && state.config.auth_token) {
// if an auth_token exists in config, use it.
return state.config.auth_token;
} else if (!bowser.safari && !bowser.ios && storage) {
@@ -43,6 +47,27 @@ const getAuthToken = (store, storage) => {
return null;
};
function areWeInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
function initExternalConfig({ store, pym, inIframe }) {
if (!inIframe) {
return;
}
return new Promise(resolve => {
pym.sendMessage('getConfig');
pym.onMessage('config', config => {
store.dispatch(mergeConfig(JSON.parse(config)));
resolve();
});
});
}
/**
* createContext setups and returns Talk dependencies that should be
* passed to `TalkProvider`.
@@ -62,10 +87,19 @@ export async function createContext({
notification,
preInit,
init = noop,
checkLogin = true,
addExternalConfig = true,
} = {}) {
const inIframe = areWeInIframe();
const eventEmitter = new EventEmitter({ wildcard: true });
const storage = createStorage();
const pymStorage = createPymStorage(pym);
const localStorage = createStorage('localStorage');
const sessionStorage = createStorage('sessionStorage');
const pymLocalStorage = inIframe
? createPymStorage(pym, 'localStorage')
: localStorage;
const pymSessionStorage = inIframe
? createPymStorage(pym, 'sessionStorage')
: sessionStorage;
const history = createHistory(BASE_PATH);
const introspection = createIntrospection(introspectionData);
let store = null;
@@ -75,7 +109,7 @@ export async function createContext({
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
return getAuthToken(store, storage);
return getAuthToken(store, localStorage);
};
const rest = createRestClient({
@@ -83,7 +117,8 @@ export async function createContext({
token,
});
let { LIVE_URI: liveUri } = getStaticConfiguration();
const staticConfig = getStaticConfiguration();
let { LIVE_URI: liveUri } = staticConfig;
if (liveUri == null) {
// The protocol must match the origin protocol, secure/insecure.
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
@@ -116,10 +151,13 @@ export async function createContext({
rest,
graphql,
notification,
storage,
localStorage,
sessionStorage,
history,
introspection,
pymStorage,
pymLocalStorage,
pymSessionStorage,
inIframe,
};
// Load framework fragments.
@@ -143,6 +181,7 @@ export async function createContext({
// Create our redux store.
const finalReducers = {
...coreReducers,
...reducers,
...plugins.getReducers(),
};
@@ -162,9 +201,39 @@ export async function createContext({
[client.middleware(), apolloErrorReporter, createReduxEmitter(eventEmitter)]
);
// Run pre initialization.
if (inIframe) {
pym.onMessage('login', token => {
if (token) {
store.dispatch(setAuthToken(token));
}
});
pym.onMessage('logout', () => {
store.dispatch(logout());
});
}
const preInitList = [];
store.dispatch(
mergeConfig({
static: staticConfig,
})
);
if (preInit) {
await preInit(context);
preInitList.push(preInit(context));
}
if (addExternalConfig) {
preInitList.push(initExternalConfig(context));
}
// Run pre initialization.
await Promise.all(preInitList);
if (checkLogin) {
store.dispatch(checkLoginAction());
}
// Run initialization.
+15 -5
View File
@@ -5,6 +5,7 @@ import merge from 'lodash/merge';
import moment from 'moment';
import 'moment/locale/da';
import 'moment/locale/de';
import 'moment/locale/es';
import 'moment/locale/fr';
import 'moment/locale/pt-br';
@@ -12,6 +13,7 @@ import 'moment/locale/pt-br';
import { createStorage } from 'coral-framework/services/storage';
import daTA from 'timeago.js/locales/da';
import deTA from 'timeago.js/locales/de';
import esTA from 'timeago.js/locales/es';
import frTA from 'timeago.js/locales/fr';
import pt_BRTA from 'timeago.js/locales/pt_BR';
@@ -21,6 +23,7 @@ import nl from 'timeago.js/locales/nl';
import en from '../../../locales/en.yml';
import da from '../../../locales/da.yml';
import de from '../../../locales/de.yml';
import es from '../../../locales/es.yml';
import fr from '../../../locales/fr.yml';
import pt_BR from '../../../locales/pt_BR.yml';
@@ -32,6 +35,7 @@ const defaultLanguage = process.env.TALK_DEFAULT_LANG;
const translations = {
...en,
...da,
...de,
...es,
...fr,
...nl_NL,
@@ -68,7 +72,7 @@ function getLocale(storage) {
export function setupTranslations() {
// Setup the translation framework with the storage.
const storage = createStorage();
const storage = createStorage('localStorage');
const locale = getLocale(storage);
setLocale(storage, locale);
@@ -86,6 +90,7 @@ export function setupTranslations() {
ta.register('es', esTA);
ta.register('da', daTA);
ta.register('de', deTA);
ta.register('fr', frTA);
ta.register('pt_BR', pt_BRTA);
ta.register('zh_CN', zh_CNTA);
@@ -113,17 +118,22 @@ export function timeago(time) {
* any extra parameters are optional and replace a variable marked by {0}, {1}, etc in the translation.
*/
export function t(key, ...replacements) {
const fullKey = `${lang}.${key}`;
if (has(translations, fullKey)) {
let translation = get(translations, fullKey);
let translation;
if (has(translations[lang], key)) {
translation = get(translations[lang], key);
} else if (has(translations['en'], key)) {
translation = get(translations['en'], key);
console.warn(`${lang}.${key} language key not set`);
}
if (translation) {
// replace any {n} with the arguments passed to this method
replacements.forEach((str, i) => {
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
});
return translation;
} else {
console.warn(`${fullKey} language key not set`);
console.warn(`${lang}.${key} and en.${key} language key not set`);
return key;
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ import flattenDeep from 'lodash/flattenDeep';
import isEmpty from 'lodash/isEmpty';
import flatten from 'lodash/flatten';
import mapValues from 'lodash/mapValues';
import get from 'lodash/get';
import { getDisplayName } from 'coral-framework/helpers/hoc';
import camelize from '../helpers/camelize';
@@ -83,7 +84,7 @@ class PluginsService {
* query datas are only passed to the component if it is defined in `component.fragments`.
*/
getSlotComponentProps(component, reduxState, props, queryData) {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
return {
...props,
config: pluginConfig,
@@ -97,7 +98,7 @@ class PluginsService {
* Returns React Elements for given slot.
*/
getSlotElements(slot, reduxState, props = {}, queryData = {}) {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
const isDisabled = component => {
if (
+21 -10
View File
@@ -34,8 +34,8 @@ function getStorage(type) {
* createStorage returns a localStorage wrapper if available
* @return {Object} localStorage wrapper
*/
export function createStorage() {
return getStorage('localStorage');
export function createStorage(type = 'localStorage') {
return getStorage(type);
}
/**
@@ -44,7 +44,7 @@ export function createStorage() {
* @param {string} pym pym
* @return {Object} storage
*/
export function createPymStorage(pym) {
export function createPymStorage(pym, type = 'localStorage') {
// A Map of requestID => {resolve, reject}
const requests = {};
@@ -54,21 +54,21 @@ export function createPymStorage(pym) {
return new Promise((resolve, reject) => {
requests[id] = { resolve, reject };
pym.sendMessage(
'pymStorage.request',
`pymStorage.${type}.request`,
JSON.stringify({ id, method, parameters })
);
});
};
// Receive successful responses.
pym.onMessage('pymStorage.response', msg => {
pym.onMessage(`pymStorage.${type}.response`, msg => {
const { id, result } = JSON.parse(msg);
requests[id].resolve(result);
delete requests[id];
});
// Receive error responses.
pym.onMessage('pymStorage.error', msg => {
pym.onMessage(`pymStorage.${type}.error`, msg => {
const { id, error } = JSON.parse(msg);
requests[id].reject(error);
delete requests[id];
@@ -88,8 +88,13 @@ export function createPymStorage(pym) {
* @param {Object} pym pym to listen to storage requests
* @param {string} prefix namespace requests by prepending a prefix to the keys
*/
export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') {
pym.onMessage('pymStorage.request', msg => {
export function connectStorageToPym(
storage,
pym,
type = 'localStorage',
prefix = 'talkPymStorage:'
) {
pym.onMessage(`pymStorage.${type}.request`, msg => {
const { id, method, parameters } = JSON.parse(msg);
const { key, value } = parameters;
const prefixedKey = `${prefix}${key}`;
@@ -99,7 +104,10 @@ export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') {
const sendError = error => {
console.error(error);
pym.sendMessage('pymStorage.error', JSON.stringify({ id, error }));
pym.sendMessage(
`pymStorage.${type}.error`,
JSON.stringify({ id, error })
);
};
try {
@@ -122,6 +130,9 @@ export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') {
return;
}
pym.sendMessage('pymStorage.response', JSON.stringify({ id, result }));
pym.sendMessage(
`pymStorage.${type}.response`,
JSON.stringify({ id, result })
);
});
}
+9
View File
@@ -252,3 +252,12 @@ export function mapLeaves(o, mapper) {
return mapper(val);
});
}
export function translateError(error) {
if (error.translation_key) {
return t(`error.${error.translation_key}`);
} else if (error.networkError) {
return t('error.network_error');
}
return error.toString();
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import Slot from 'coral-framework/components/Slot';
class Main extends React.Component {
render() {
return <Slot fill="login" />;
}
}
export default Main;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { render } from 'react-dom';
import { createContext } from 'coral-framework/services/bootstrap';
import Main from './containers/Main';
import TalkProvider from 'coral-framework/components/TalkProvider';
import pluginsConfig from 'pluginsConfig';
async function main() {
const context = await createContext({
pluginsConfig,
});
render(
<TalkProvider {...context}>
<Main />
</TalkProvider>,
document.querySelector('#talk-login-container')
);
}
main();
@@ -0,0 +1,3 @@
.bare {
composes: buttonReset from "coral-framework/styles/reset.css";
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './BareButton.css';
import cn from 'classnames';
/**
* BareButton is a button whose styling is stripped off to a minimum.
* Can pass anchor=true to use `a` instead of `button`
*/
const BareButton = ({ anchor, className, ...props }) => {
let Element = 'button';
if (anchor) {
Element = 'a';
}
return <Element {...props} className={cn(styles.bare, className)} />;
};
BareButton.propTypes = {
className: PropTypes.string,
anchor: PropTypes.bool,
};
export default BareButton;
+1
View File
@@ -10,6 +10,7 @@
line-height: 22px;
min-width: 80px;
text-align: center;
vertical-align: text-top;
}
.icon {
+1
View File
@@ -28,3 +28,4 @@ export { default as Label } from './components/Label';
export { default as FlagLabel } from './components/FlagLabel';
export { default as Dropdown } from './components/Dropdown';
export { default as Option } from './components/Option';
export { default as BareButton } from './components/BareButton';
+4
View File
@@ -44,6 +44,10 @@ const CONFIG = {
// fetching again.
SETTINGS_CACHE_TIME: ms(process.env.TALK_SETTINGS_CACHE_TIME || '1hr'),
// ALLOW_NO_LIMIT_QUERIES enables some queries to specify a limit of -1 to
// request all of the records. Otherwise, minimum limits of 0 are enforced.
ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE',
//------------------------------------------------------------------------------
// JWT based configuration
//------------------------------------------------------------------------------
@@ -84,23 +84,3 @@ TALK_JWT_SECRET=jX9y8G2ApcVLwyL{$6s3
Be default, we sign our tokens with HMAC using a SHA-256 hash algorithm. If you
want to change the signing algorithm, or use multiple signing/verifying keys,
refer to our [Advanced Configuration]({{ "/advanced-configuration/" | relative_url }}) documentation.
## TALK_FACEBOOK_APP_ID
The Facebook App ID for your Facebook Login enabled app. You can learn more
about getting a Facebook App ID at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
## TALK_FACEBOOK_APP_SECRET
The Facebook App Secret for your Facebook Login enabled app. You can learn more
about getting a Facebook App Secret at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
+53 -1
View File
@@ -57,6 +57,26 @@ When `TRUE`, it will not mount the static asset serving routes on the router.
This is used primarily in conjunction with [TALK_STATIC_URI](#talk_static_uri){: .param}
when the static assets are being hosted on an external domain. (Default `FALSE`)
## TALK_FACEBOOK_APP_ID
The Facebook App ID for your Facebook Login enabled app. You can learn more
about getting a Facebook App ID at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
## TALK_FACEBOOK_APP_SECRET
The Facebook App Secret for your Facebook Login enabled app. You can learn more
about getting a Facebook App Secret at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
## TALK_HELMET_CONFIGURATION
A JSON string representing the configuration passed to the
@@ -303,6 +323,8 @@ the websocket to keep the socket alive, parsed by
## TALK_RECAPTCHA_PUBLIC
Setting a reCAPTCHA Public and Secret key will enable and require reCAPTCHA upon multiple failed login attempts.
Client secret used for enabling reCAPTCHA powered logins. If
[TALK_RECAPTCHA_SECRET](#talk_recaptcha_secret){: .param} and
[TALK_RECAPTCHA_PUBLIC](#talk_recaptcha_public){: .param} are not provided it will instead
@@ -486,4 +508,34 @@ Used to set the key for use with
[Apollo Engine](https://www.apollographql.com/engine/){:target="_blank"} for
tracing of GraphQL requests.
**Note: Apollo Engine is a premium service, charges may apply.**
**Note: Apollo Engine is a premium service, charges may apply.**
## ALLOW_NO_LIMIT_QUERIES
Setting this to `TRUE` will allow queries to execute without a limit (returns
all documents). This introduces a significant performance regression, and should
be used with caution. (Default `FALSE`)
## TALK_ADDTL_COMMENTS_ON_LOAD_MORE
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10`.
Specifies the number of additional comments to load when a user clicks `Load More`. (Default `10`)
## TALK_ASSET_COMMENTS_LOAD_DEPTH
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_ASSET_COMMENTS_LOAD_DEPTH=10`.
Specifies the initial number of comments to load for an asset. (Default `10`)
## TALK_REPLY_COMMENTS_LOAD_DEPTH
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_REPLY_COMMENTS_LOAD_DEPTH=3`.
Specifies the initial replies to load for a comment. (Default `3`)
+15
View File
@@ -103,3 +103,18 @@ Source: [plugins/talk-plugin-subscriber](https://github.com/coralproject/talk/tr
Enables a `Subscriber` badge to be added to comments where the author has the
`SUBSCRIBER` tag. This must match with a custom auth integration that adds the
tag to the users that are subscribed to the service.
## talk-plugin-akismet
Source: [plugins/talk-plugin-akismet](https://github.com/coralproject/talk/tree/master/plugins/talk-plugin-akismet){:target="_blank"}
Enables spam detection from [Akismet](https://akismet.com/). Comments will be passed to the Akismet API for spam detection. If a comment
is determined to be spam, it will prompt the user, indicating that the comment might be considered spam. If the user continues after this
point with the still spam-like comment, the comment will be reported as containing spam, and sent for moderator approval.
**Note: [Akismet](https://akismet.com/) is a premium service, charges may apply.**
Configuration:
- `TALK_AKISMET_API_KEY` (**required**) - The Akismet API key located on your account page.
- `TALK_AKISMET_SITE` (**required**) - The URL where you are embedding the comment stream on to provide context to Akismet. If you're hosting talk on https://talk.mynews.org/, and your news site is https://mynews.org/, then you should set this parameter to `https://mynews.org/`
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = {
// Schema is created already, so just include it.
schema,
// Load in the new context here, this'll create the loaders + mutators for
// Load in the new context here, this will create the loaders + mutators for
// the lifespan of this request.
context: new Context(req),
+158 -28
View File
@@ -1,54 +1,184 @@
const DataLoader = require('dataloader');
const util = require('./util');
const ActionsService = require('../../services/actions');
const ActionModel = require('../../models/action');
const { first, get, merge, remove, groupBy, reduce, isNil } = require('lodash');
/**
* Gets actions based on their item id's.
*/
const genActionsByItemID = (_, item_ids) => {
return ActionsService.findByItemIdArray(item_ids).then(
const genActionsByItemID = (
{ connectors: { services: { Actions } } },
item_ids
) => {
return Actions.findByItemIdArray(item_ids).then(
util.arrayJoinBy(item_ids, 'item_id')
);
};
/**
* Looks up actions based on the requested id's all bounded by the user.
* @param {Object} context the context of the request
* @param {Array} ids array of id's to get
* @return {Promise} resolves to the promises of the requested actions
* Looks up the actions for each of the items.
*
* @param {Object} ctx the graph context of the request
* @param {Array<String>} itemIDs the items that we need to get the actions for
*/
const genActionSummariessByItemID = ({ user = {} }, item_ids) => {
return ActionsService.getActionSummaries(item_ids, user.id).then(
util.arrayJoinBy(item_ids, 'item_id')
const genActionsAuthoredWithID = (
{ user = {}, connectors: { services: { Actions } } },
itemIDs
) =>
Actions.getUserActions(user.id, itemIDs).then(
util.arrayJoinBy(itemIDs, 'item_id')
);
};
/**
* Search for actions based on their action_type and item_type and ensures that
* the actions returned have unique item id's.
* @param {String} action_type the action to search by
* @param {String} item_type the item id to search by
* @return {Promise} resolves to distinct items actions
* iterateActionCounts will create an iterable object that can be used to
* compute action summaries.
*
* @param {Object} action_counts the action count object
*/
const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
return ActionModel.distinct('item_id', { action_type, item_type });
const iterateActionCounts = action_counts =>
!isNil(action_counts)
? Object.keys(action_counts).map(action_type => ({
count: action_counts[action_type],
action_type: action_type.toUpperCase(),
}))
: [];
/**
* getUserActions will get the actions made by the user for this specific
* item.
*
* @param {Object} ctx the graph context of the request
* @param {Object} item the item that we're getting the actions for
*/
async function getUserActions(ctx, { action_counts, id }) {
const { loaders: { Actions } } = ctx;
// Get the total count for all action types.
const totalActionCount = reduce(
action_counts,
(total, count) => total + count,
0
);
// Check to see if there are any user actions to get.
const hasUserActions = ctx.user && totalActionCount > 0;
if (!hasUserActions) {
return {};
}
// Possibly get the list of user actions completed by the user. This will be
// used later to join together with the action summaries to provide context.
const userActions = await Actions.getAuthoredByID.load(id);
if (userActions.length === 0) {
return {};
}
// Group the user actions in the same way that the action counts are
// grouped. This will let us extract it easy.
return reduce(
groupBy(userActions, ({ action_type, group_id }) =>
(group_id ? `${action_type}_${group_id}` : action_type).toUpperCase()
),
(allUserActions, userActions, actionType) =>
merge(allUserActions, { [actionType]: first(userActions) }),
{}
);
}
// This will match any action count that is specific for a group id.
const nonGroupIDTest = /^([A-Z]+)_([A-Z_]+)$/;
/**
* resolveActionSummariesForItem will resolve the action summaries for an item.
*
* @param {Object} ctx the graph context of the request
* @param {Object} item the item that we are resolving an action summary for
*/
async function resolveActionSummariesForItem(ctx, { id, action_counts }) {
// Cache all those entries for which we got the group id of, because we
// don't want to include them twice.
const groupIDCache = {};
// Get the user actions for this specific item.
const groupedUserActions = await getUserActions(ctx, { id, action_counts });
// Generate the action summaries for the item.
return iterateActionCounts(action_counts).reduce(
(actionTypeList, { count, action_type }) => {
// Get the current user's actions (if they have any).
const current_user = get(groupedUserActions, action_type, null);
// Check to see if this is a action without a corresponding group id.
if (nonGroupIDTest.test(action_type)) {
// This action type does have a group id associated with it.
const results = nonGroupIDTest.exec(action_type);
const groupActionType = results[1];
const groupID = results[2];
// Purge out the summary if it already exists, and mark that this
// group id has been found so we don't include it in the future.
remove(
actionTypeList,
({ action_type }) => action_type === groupActionType
);
groupIDCache[groupActionType] = true;
// Push the new entry in.
actionTypeList.push({
action_type: groupActionType,
group_id: groupID,
count,
current_user,
});
} else {
// This does not have a group id. Check to see if this group id
// already has an specific (group id) entry.
if (groupIDCache[action_type]) {
// It does. Don't add anything.
return actionTypeList;
}
// It does not, add the entry.
actionTypeList.push({
action_type,
group_id: null,
count,
current_user,
});
}
return actionTypeList;
},
[]
);
}
/**
* Looks up the action summaries for a set of items.
*
* @param {Object} ctx the graph context of the request
* @param {Array<Object>} items the items that should have their items looked up for
*/
const genActionSummariesByItem = async (ctx, items) => {
// This is designed to match the action_counts value that is embedded on
// documents which cache action counts. For users that are not logged in, we
// don't need to hit the actions collection at all!
// We will literate over all the items that we're comparing.
return items.map(item => resolveActionSummariesForItem(ctx, item));
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @param {Object} ctx the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = context => ({
module.exports = ctx => ({
Actions: {
getByID: new DataLoader(ids => genActionsByItemID(context, ids)),
getSummariesByItemID: new DataLoader(ids =>
genActionSummariessByItemID(context, ids)
getByID: new DataLoader(ids => genActionsByItemID(ctx, ids)),
getSummariesByItem: new DataLoader(
items => genActionSummariesByItem(ctx, items),
{ cacheKeyFn: ({ id }) => id }
),
getByTypes: ({ action_type, item_type }) =>
getItemIdsByActionTypeAndItemType(context, action_type, item_type),
getAuthoredByID: new DataLoader(ids => genActionsAuthoredWithID(ctx, ids)),
},
});
+44 -43
View File
@@ -4,7 +4,10 @@ const {
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
SEARCH_OTHERS_COMMENTS,
} = require('../../perms/constants');
const { CACHE_EXPIRY_COMMENT_COUNT } = require('../../config');
const {
CACHE_EXPIRY_COMMENT_COUNT,
ALLOW_NO_LIMIT_QUERIES,
} = require('../../config');
const ms = require('ms');
const sc = require('snake-case');
@@ -148,12 +151,11 @@ const getCommentCountByQuery = (ctx, options) => {
* @param {Object} params the params from the client describing the query
*/
const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[0].created_at : null;
case 'REPLIES':
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
if (sortBy === 'CREATED_AT') {
return nodes.length ? nodes[0].created_at : null;
} else if (sortBy === 'REPLIES') {
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
}
const SORT_KEY = sortBy.toLowerCase();
@@ -181,11 +183,10 @@ const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
* @param {Object} params the params from the client describing the query
*/
const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[nodes.length - 1].created_at : null;
case 'REPLIES':
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
if (sortBy === 'CREATED_AT') {
return nodes.length ? nodes[nodes.length - 1].created_at : null;
} else if (sortBy === 'REPLIES') {
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
}
const SORT_KEY = sortBy.toLowerCase();
@@ -212,36 +213,33 @@ const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
* @param {Object} params the params from the client describing the query
*/
const applySort = (ctx, query, { cursor, sortOrder, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT': {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
if (sortBy === 'CREATED_AT') {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
}
case 'REPLIES': {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({
reply_count: sortOrder === 'DESC' ? -1 : 1,
created_at: sortOrder === 'DESC' ? -1 : 1,
});
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
} else if (sortBy === 'REPLIES') {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({
reply_count: sortOrder === 'DESC' ? -1 : 1,
created_at: sortOrder === 'DESC' ? -1 : 1,
});
}
const SORT_KEY = sortBy.toLowerCase();
@@ -280,7 +278,7 @@ const executeWithSort = async (
query = applySort(ctx, query, { cursor, sortOrder, sortBy });
// Apply the limit (if it exists, as it's applied universally).
if (limit) {
if (limit >= 0) {
query = query.limit(limit + 1);
}
@@ -290,7 +288,7 @@ const executeWithSort = async (
// The hasNextPage is always handled the same (ask for one more than we need,
// if there is one more, than there is more).
let hasNextPage = false;
if (limit && nodes.length > limit) {
if (limit >= 0 && nodes.length > limit) {
// There was one more than we expected! Set hasNextPage = true and remove
// the last item from the array that we requested.
hasNextPage = true;
@@ -302,11 +300,9 @@ const executeWithSort = async (
return {
startCursor: getStartCursor(ctx, nodes, {
cursor,
sortOrder,
sortBy,
limit,
}),
endCursor: getEndCursor(ctx, nodes, { cursor, sortOrder, sortBy, limit }),
endCursor: getEndCursor(ctx, nodes, { cursor, sortBy }),
hasNextPage,
nodes,
};
@@ -338,6 +334,11 @@ const getCommentsByQuery = async (
) => {
let comments = CommentModel.find();
// Enforce that the limit must be gte 0 if this option is not true.
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
throw new Error('cannot query for limit < 0');
}
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
// special privileges.
if (
+4 -4
View File
@@ -40,12 +40,12 @@ const Comment = {
return Actions.getByID.load(id);
},
action_summaries({ id, action_summaries }, _, { loaders: { Actions } }) {
if (action_summaries) {
return action_summaries;
action_summaries(comment, _, { loaders: { Actions } }) {
if (comment.action_summaries) {
return comment.action_summaries;
}
return Actions.getSummariesByItemID.load(id);
return Actions.getSummariesByItem.load(comment);
},
asset({ asset_id }, _, { loaders: { Assets } }) {
return Assets.getByID.load(asset_id);
+2 -2
View File
@@ -10,8 +10,8 @@ const {
} = require('../../perms/constants');
const User = {
action_summaries({ id }, _, { loaders: { Actions } }) {
return Actions.getSummariesByItemID.load(id);
action_summaries(user, _, { loaders: { Actions } }) {
return Actions.getSummariesByItem.load(user);
},
actions({ id }, _, { user, loaders: { Actions } }) {
// Only return the actions if the user is not an admin.
+80 -1
View File
@@ -12,11 +12,26 @@ da:
note_reject_comment: "Hvis du banner denne bruger vil det også placere denne kommentar i den afviste kø"
note_ban_user: "Hvis du banner denne bruger vil det forhindre dem i at redigere kommentarer eller fjerne noget"
yes_ban_user: "Ja, Ban Bruger"
write_a_message: "Write a message"
send: "Send"
notify_ban_headline: "Notify the user of ban"
notify_ban_description: "This will notify the user by email that they have been banned from the community"
email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team."
bio_offensive: "Denne biografi er stødende"
cancel: "Afbryd"
confirm_email:
click_to_confirm: "Click below to confirm your email address"
confirm: "Confirm"
password_reset:
set_new_password: "Change Your Password"
new_password: "New Password"
new_password_help: "Password must be at least 8 characters"
confirm_new_password: "Confirm New Password"
change_password: "Change Password"
characters_remaining: "tegn tilbage"
comment:
anon: "Anonym"
undo_reject: "Undo"
ban_user: "Ban Bruger"
comment: "Skriv en kommentar"
edited: "Redigeret"
@@ -46,6 +61,11 @@ da:
reaction: "reaktion"
reactions: "reaktioner"
story: "Historie"
flagged_usernames:
notify_approved: '{0} approved username {1}'
notify_rejected: '{0} rejected username {1}'
notify_flagged: '{0} reported username {1}'
notify_changed: 'user {0} changed their username to {1}'
community:
account_creation_date: "Oprettelsedato for konto"
active: "Aktive"
@@ -75,6 +95,7 @@ da:
status: "Status"
username_and_email: "Brugernavn eller email-adresse"
yes_ban_user: "Ja Ban Bruger"
commenter: "Commenter"
configure:
apply: "Anvend"
banned_word_text: "Kommentarer, der indeholder disse ord eller sætninger (Ikke følsom over for store eller små bogstaver) fjernes automatisk fra kommentarstrømmen. Indtast et ord og tryk på Enter eller Tab for at tilføje. Indsæt eventuelt en kommasepareret liste."
@@ -161,7 +182,14 @@ da:
edit_window_timer_prefix: "Luk vindue: "
second: "sekund"
seconds_plural: "sekunder"
minute: "minute"
minutes_plural: "minutes"
email:
suspended:
subject: "Your account has been suspended"
banned:
subject: "Your account has been banned"
body: "In accordance with The Coral Projects community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community."
confirm:
has_been_requested: "Der er anmodet om en e-mail bekræftelse for følgende konto:"
to_confirm: "For at bekræfte kontoen, skal du besøge følgende link:"
@@ -175,13 +203,18 @@ da:
embedlink:
copy: "Kopier til udklipsholder"
error:
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
COMMENT_TOO_SHORT: "Din kommentar skal indeholde noget"
NOT_AUTHORIZED: "Du har ikke tilladelse til at udføre denne handling."
NO_SPECIAL_CHARACTERS: "Brugernavne kan kun indeholder bogstaver og _"
PASSWORD_LENGTH: "Adgangskoden er for kort"
PROFANITY_ERROR: "Brugernavne må ikke inholde stødende indhold. Kontakt venligst administratoren, hvis du mener at dette er en fejl."
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
USERNAME_IN_USE: "Brugernavnet er allerede i brug"
USERNAME_REQUIRED: "Du skal indtaste et brugernavn"
EMAIL_NOT_VERIFIED: "E-mail address not verified"
EDIT_WINDOW_ENDED: "Du kan ikke længere redigere denne kommentar. Tidsvinduet for at gøre det er udløbet."
EDIT_USERNAME_NOT_AUTHORIZED: "Du har ikke tilladelse til at opdatere dit brugernavn."
SAME_USERNAME_PROVIDED: "Du skal indsende et andet brugernavn."
@@ -193,6 +226,7 @@ da:
NOT_FOUND: "Ressource ikke fundet"
ALREADY_EXISTS: "Ressource eksisterer allerede"
INVALID_ASSET_URL: "Aktivitetswebadresse er ugyldig"
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
email: "Ikke en gylding e-mail"
confirm_password: "Kodeordene matcher ikke. Tjek venligst igen."
network_error: "Det lykkedes ikke at oprette forbindelse til serveren. Tjek din internetforbindelse og prøv igen."
@@ -202,6 +236,7 @@ da:
password: "Adgangskoden skal være mindst 8 tegn"
username: "Brugernavne kan kun indeholde bogstaver og _"
unexpected: "Der opstod en uventet fejl. Undskyld!"
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
flag_comment: "Rapportér kommentar"
flag_reason: "Årsag til rapportering (Valgfrit)"
flag_username: "Rapporter brugernavn"
@@ -220,6 +255,8 @@ da:
error: "Brugernavne kan kun indeholde bogstaver og _"
label: "Nyt brugernavn"
msg: "Din konto er midlertidigt suspenderet, fordi dit brugernavn er blevet anset for upassende. For at gendanne din konto skal du indtaste et nyt brugernavn. Kontakt os venligst, hvis du har spørgsmål."
changed_name:
msg: "Your username change is under review by our moderation team."
my_comments: "Mine kommentarer"
my_profile: "Min profil"
new_count: "Se {0} mere {1}"
@@ -237,6 +274,24 @@ da:
loading_results: "Indlæser resultater"
marketing: "Dette ligner en annonce/markedsføring"
moderate_this_stream: "Moderer denne strøm"
flags:
reasons:
user:
username_offensive: "Offensive"
username_nolike: "Dislike"
username_impersonating: "Impersonation"
username_spam: "Spam"
username_other: "Other"
comment:
comment_offensive: "Offensive"
comment_spam: "Spam"
comment_noagree: "Disagree"
comment_other: "Other"
suspect_word: "Suspect Word"
banned_word: "Banned Word"
body_count: "Body exceeds max length"
trust: "Trust"
links: "Link"
modqueue:
account: "konto flag"
actions: "Handlinger"
@@ -246,6 +301,7 @@ da:
notify_accepted: "{0} accepteret kommentar {1}"
notify_rejected: "{0} afvist kommentar {1}"
notify_flagged: "{0} flagget kommentar {1}"
notify_reset: '{0} reset status of comment "{1}"'
approve: "Godkend"
approved: "Godkendt"
ban_user: "Ban"
@@ -254,6 +310,7 @@ da:
empty_queue: "Ikke flere kommentarer til at moderer! Du har indhentet det hele. Gå ud og tag noget ☕️"
flagged: "flagged"
reported: "Rapporteret"
jump_to_queue: "Jump to specific queue"
less_detail: "Færre detajler"
likes: "Syntes godt om"
million: "M"
@@ -264,6 +321,7 @@ da:
newest_first: "Nyeste først"
navigation: "Navigation"
next_comment: "Gå til næste kommentar"
next_queue: "Switch queues"
oldest_first: "Ældste Først"
premod: "pre-mod"
prev_comment: "Gå til den forrige kommentar"
@@ -275,15 +333,18 @@ da:
shortcuts: "Genveje"
show_shortcuts: "Vis genveje"
singleview: "Zen mode"
sort: "Sort"
thismenu: "Åben denne menu"
thousand: "k"
try_these: "Prøv disse"
toggle_search: "Open search"
view_more_shortcuts: "Se flere genveje"
my_comment_history: "Min kommentar Historie"
name: "Navn"
no_agree_comment: "Jeg er ikke enig med denne kommentar"
no_like_bio: "Jeg kan ikke lide denne biografi"
no_like_username: "Jeg kan ikke lide dette brugernavn"
already_flagged_username: "You have already flagged this username."
other: "Andet"
permalink: "Delen"
personal_info: "Denne kommentar afslører personligt identificerbare oplysninger"
@@ -306,7 +367,9 @@ da:
stream:
all_comments: "Alle kommentarer"
temporarily_suspended: "I overensstemmelse med {0}'s retningslinjer for fællesskabet er din konto midlertidigt suspenderet. Venligst tilslut dig samtalen {1}."
comment_not_found: "Kommentar blev ikke fundet"
comment_not_found: "Denne kommentar er blevet fjernet eller findes ikke."
no_comments: "There are no comments yet, why dont you write one?"
no_comments_and_closed: "There were no comments on this article."
step_1_header: "Rapportér et problem"
step_2_header: "Hjælp os med at forstå"
step_3_header: "Tak for din indsats"
@@ -356,6 +419,19 @@ da:
bio_flags: "flag for denne bio"
user_bio: "Bruger Biografi"
username_flags: "flag for dette brugernavn"
user_detail:
remove_suspension: "Remove Suspension"
suspend: "Suspend User"
remove_ban: "Remove Ban"
ban: "Ban User"
member_since: "Member Since"
email: "Email"
total_comments: "Total Comments"
reject_rate: "Reject Rate"
reports: "Reports"
all: "All"
rejected: "Rejected"
account_history: "Account History"
user_impersonating: "Denne bruger efterligner"
user_no_comment: "Du har aldrig efterladt en kommentar. Deltag i samtalen"
username_offensive: "Dette brugernavn er stødende"
@@ -382,3 +458,6 @@ da:
description: "Tak fordi du installerede Talk! Vi sendte en email for at bekræfte din e-mail-adresse. Mens du færdigøre oprettelsen af kontoen, kan du begynde at engagere med dine læsere."
launch: "Lancere Talk"
close: "Luk dette installations program"
admin_sidebar:
view_options: "View Options"
sort_comments: "Sort Comments"
+462
View File
@@ -0,0 +1,462 @@
de:
your_account_has_been_suspended: Ihr Zugang wurde vorübergehend gesperrt.
your_account_has_been_banned: Ihr Zugang wurde gesperrt.
your_username_has_been_rejected: Ihr Zugang wurde vorübergehend gesperrt, da Ihr Nutzername als unangemessen eingestuft wurde. Um Ihren Zugang wieder herzustellen, geben Sie bitte einen neuen Nutzernamen ein.
embed_comments_tab: Kommentare
bandialog:
are_you_sure: "Sind Sie sich sicher, dass Sie {0} sperren möchten?"
ban_user: "Nutzer sperren?"
banned_user: "Gesperrter Nutzer"
cancel: "Beenden"
note: "Notiz: {0}"
note_reject_comment: "Das Sperren dieses Nutzers wird auch diesen Kommentar in die Abgelehnt-Liste verschieben."
note_ban_user: "Nach dem Sperren wird dieser Nutzer weder Kommentare schreiben, noch anderweitig mit der Community interagieren können."
yes_ban_user: "Ja, Nutzer sperren"
write_a_message: "Nachricht schreiben"
send: "Senden"
notify_ban_headline: "Nutzer über Sperrung benachrichtigen"
notify_ban_description: "Dies wird den Nutzer per E-Mail über den Ausschluss aus der Community informieren"
email_message_ban: "Sehr geehrte/r {0},\n\nJemand mit Zugriff auf Ihr Konto hat gegen unsere Community-Richtlinien verstoßen. Daraufhin wurde Ihr Konto gesperrt. Sie können nun weder kommentieren noch anderweitig mit der Community interagieren. Falls Sie denken, dass es sich hierbei um einen Fehler handelt, nehmen Sie bitte Kontakt mit unseren Moderatoren auf."
bio_offensive: "Diese Biographie ist unangemessen"
cancel: "Abbrechen"
confirm_email:
click_to_confirm: "Unten klicken, um E-Mail-Adresse zu bestätigen"
confirm: "Bestätigen"
password_reset:
set_new_password: "Passwort ändern"
new_password: "Neues Passwort"
new_password_help: "Das Passwort benötigt mindestens 8 Zeichen"
confirm_new_password: "Neues Passwort bestätigen"
change_password: "Passwort ändern"
characters_remaining: "verbleibende Zeichen"
comment:
anon: "Anonym"
undo_reject: "Rückgängig machen"
ban_user: "Nutzer sperren"
comment: "Kommentar verfassen…"
edited: Bearbeitet
flagged: "markiert"
view_context: "Kontext ansehen"
comment_box:
post: "Senden"
cancel: "Abbrechen"
reply: "Antworten"
comment: "Kommentar verfassen"
name: "Name"
comment_post_notif: "Ihr Kommentar wurde versandt."
comment_post_notif_premod: "Vielen Dank für Ihren Kommentar. Unsere Moderatoren werden ihn in Kürze bearbeiten."
comment_post_banned_word: "Ihr Kommentar enthält ein oder mehrere Wörter, die unzulässig sind, daher wird er nicht veröffentlicht. Falls Sie dies als ungerechtfertigt ansehen, nehmen Sie bitte Kontakt zu unseren Moderatoren auf."
characters_remaining: "verbleibende Zeichen"
comment_offensive: "Dieser Kommentar ist unangemessen"
comment_singular: Kommentar
comment_plural: Kommentare
comment_post_banned_word: "Ihr Kommentar enthält ein oder mehrere Wörter, die unzulässig sind, daher wird er nicht veröffentlicht. Falls Sie dies als ungerechtfertigt ansehen, nehmen Sie bitte Kontakt zu unseren Moderatoren auf."
comment_post_notif: "Ihr Kommentar wurde veröffentlicht."
comment_post_notif_premod: "Vielen Dank für Ihren Kommentar. Unsere Moderatoren werden ihn in Kürze bearbeiten."
common:
copy: 'Kopieren'
error: 'Ein Problem ist aufgetreten.'
reply: 'antworten'
replies: 'Antworten'
reaction: 'Reaktion'
reactions: 'Reaktionen'
story: 'Artikel'
flagged_usernames:
notify_approved: '{0} hat Nutzername {1} freigegeben'
notify_rejected: '{0} hat Nutzername {1} abgelehnt'
notify_flagged: '{0} hat Nutzername {1} markiert'
notify_changed: 'Nutzer {0} hat Nutzernamen nach {1} geändert'
community:
account_creation_date: "Datum der Kontoerstellung"
active: Aktiv
admin: Administrator
ads_marketing: "Dies scheint Werbung zu sein"
are_you_sure: "Sind Sie sich sicher, dass Sie {0} sperren möchten?"
ban_user: "Nutzer sperren?"
banned: Gesperrt
banned_user: "Gesperrter Nutzer"
cancel: Abbrechen
dont_like_username: "Unsympathischer Nutzername"
flaggedaccounts: "Gemeldete Nutzernamen"
flags: Markierungen
impersonating: "Identität unklar"
loading: "Lädt Ergebnisse"
moderator: Moderator
newsroom_role: "Redaktionsrolle"
no_flagged_accounts: "Die Liste der gemeldeten Nutzer ist aktuell leer."
no_results: "Keine Nutzer mit entsprechendem Namen oder E-Mail-Adresse gefunden."
offensive: "Unangemessen"
other: Etwas anderes
people: Nutzer
role: "Rolle auswählen..."
select_status: "Status wählen..."
spam_ads: "Spam/Werbung"
staff: "Mitarbeiter"
status: Status
username_and_email: "Nutzername und E-Mail"
yes_ban_user: "Ja, Nutzer sperren"
commenter: "Kommentator"
configure:
apply: Anwenden
banned_word_text: "Kommentare, die diese Wörter oder Phrasen enthalten (unabhängig von Groß-/Kleinschreibung), werden automatisch aus dem Kommentarbereich entfernt. Geben Sie ein Wort ein und bestätigen Sie mit Eingabetaste oder Tab. Es ist auch möglich, einen komma-separierten Text einzufügen."
banned_words_title: "Liste der unzulässigen Wörter"
close: "Schließen"
close_after: "Kommentarbereich schließen nach"
close_stream: "Kommentarbereich schließen"
close_stream_configuration: "Dieser Kommentarbereich ist momentan geschlossen. Durch das Öffnen dieses Kommentarbereichs können neue Kommentare abgegeben und veröffentlicht werden."
closed_comments_desc: "Wird angezeigt, wenn der Kommentarbereich für den Artikel geschlossen ist."
closed_comments_label: "Nachricht verfassen..."
closed_stream_settings: "Mitteilung über Schließung"
comment_count_error: "Geben Sie eine gültige Zahl an."
comment_count_header: "Zeichenlimit für Kommentare festlegen"
comment_count_text_post: Zeichen
comment_count_text_pre: "Kommentar wird begrenzt auf"
comment_settings: Einstellungen
comment_stream: "Kommentarbereich"
comment_stream_will_close: "Der Kommentarbereich wird geschlossen"
community: Community
configure: Einstellungen
copy_and_paste: "Kopieren Sie diesen Code in Ihr CMS, um die Kommentarfunktion an entsprechender Stelle anzuzeigen."
custom_css_url: "Benutzerdefinierte CSS-URL"
custom_css_url_desc: "URL eines CSS-Stylesheets zum Überschreiben des Standard-Designs"
days: Tage
description: "Als Administrator können Sie die Einstellungen für den Kommentarbereich dieses Artikels anpassen:"
domain_list_text: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll, z.B. für lokale Test- oder Produktionsumgebungen (Bsp.: localhost:3000 staging.domain.com domain.com)."
domain_list_title: "Zugelassene Domains"
edit_comment_timeframe_heading: "Zeitlimit zur Bearbeitung von Kommentaren"
edit_comment_timeframe_text_pre: "Kommentatoren haben"
edit_comment_timeframe_text_post: "Sekunden Zeit, um ihre Kommentare zu bearbeiten."
embed_comment_stream: "Kommentarbereich einbinden"
enable_premod_links_text: "Moderatoren müssen jeden Kommentar, der einen Link enthält, freigeben, bevor er veröffentlicht wird."
enable_pre_moderation: "Vormoderation aktivieren"
enable_pre_moderation_text: "Moderatoren müssen jeden Kommentar freigeben, bevor er veröffentlicht wird."
enable_premod_links: "Vormoderation für Kommentare mit Links aktivieren"
enable_premod: "Vormoderation aktivieren"
enable_premod_description: "Moderatoren müssen jeden Kommentar freigeben, bevor er veröffentlicht wird."
enable_premod_links_description: "Moderatoren müssen jeden Kommentar, der einen Link enthält, freigeben, bevor er veröffentlicht wird."
enable_questionbox: "Stellen Sie den Lesern eine Frage"
enable_questionbox_description: "Diese Frage erscheint am Anfang des Kommentarbereichs. Regen Sie eine Diskussion an."
hours: Stunden
include_comment_stream: "Einleitung zum Kommentarbereich"
include_comment_stream_desc: "Verfassen Sie eine Einleitung, die über jedem Kommentarbereich erscheint. Nützlich z.B. für Community-Richtlinien."
include_text: "Fügen Sie Ihren Text hier ein."
include_question_here: "Stellen Sie Ihre Frage hier:"
moderate: Moderieren
moderation_settings: "Moderations-Einstellungen"
open: "Öffnen"
open_stream: "Kommentarbereich öffnen"
open_stream_configuration: "Dieser Kommentarbereich ist momentan geöffnet. Nach dem Schließen dieses Kommentarbereich wird es nicht mehr möglich sein, zu kommentieren. Bestehende Kommentare bleiben sichtbar."
require_email_verification: "E-Mail-Bestätigung erforderlich"
require_email_verification_text: "Neue Nutzer müssen ihre E-Mail-Adresse bestätigen."
save_changes: "Änderungen speichern"
shortcuts: Tastaturkürzel
sign_out: "Abmelden"
stories: Artikel
stream_settings: "Einstellungen Kommentarbereich"
suspect_word_title: "Liste verdächtiger Wörter"
suspect_word_text: "Kommentare, die diese Wörter oder Phrasen enthalten (unabhängig von Groß-/Kleinschreibung), werden im Kommentarbereich markiert. Geben Sie ein Wort ein und bestätigen Sie mit Eingabetaste oder Tab. Es ist auch möglich, einen komma-separierten Text einzufügen."
tech_settings: "Technische Einstellungen"
title: "Kommentarbereich konfigurieren"
weeks: Wochen
wordlist: "Gesperrte Wörter"
continue: "Fortfahren"
createdisplay:
check_the_form: "Ungültige Eingabe. Bitte prüfen Sie die Felder."
continue: "Mit diesem Facebook-Nutzernamen fortfahren"
error_create: "Fehler beim Ändern des Nutzernamens"
fake_comment_body: "Dies ist ein Beispiel-Kommentar. Leser können im Kommentarbereich Meinungen und Gedanken mit der Redaktion austauschen."
fake_comment_date: "Vor einer Minute"
if_you_dont_change_your_name: "Wenn Sie Ihren Nutzernamen an diesem Punkt nicht ändern, wird Ihr Facebook-Name mit Ihren Kommentaren angezeigt."
required_field: "Pflichtfeld"
save: Speichern
special_characters: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
username: Nutzername
write_your_username: "Nutzername bearbeiten"
your_username: "Ihr Nutzername erscheint an jedem Ihrer veröffentlichten Kommentare."
done: Fertig
edit_comment:
body_input_label: "Kommentar bearbeiten"
save_button: "Änderungen speichern"
edit_window_expired: "Sie können diesen Kommentar nicht mehr bearbeiten. Das Zeitfenster hierfür ist abgelaufen. Bitte schreiben Sie einen neuen Kommentar."
edit_window_expired_close: "Schließen"
edit_window_timer_prefix: "Verbleibende Zeit: "
second: "Sekunde"
seconds_plural: "Sekunden"
minute: "Minute"
minutes_plural: "Minuten"
email:
suspended:
subject: "Ihr Benutzerkonto wurde vorübergehend gesperrt"
banned:
subject: "Ihr Benutzerkonto wurde gesperrt"
body: "Entsprechend der Community-Richtlinien des Coral Project, wurde Ihr Benutzerkonto gesperrt. Sie dürfen ab sofort weder kommentieren noch an der Community teilnehmen."
confirm:
has_been_requested: "Für folgendes Nutzerkonto wurde eine E-Mail-Bestätigung angefordert:"
to_confirm: "Um Ihr Nutzerkonto zu bestätigen, öffnen Sie bitte folgenden Link:"
confirm_email: "E-Mail-Adresse bestätigen"
if_you_did_not: "Sollten Sie diese E-Mail nicht angefordert haben, können Sie diese Nachricht ignorieren."
subject: "Bestätigung der E-Mail-Adresse"
password_reset:
we_received_a_request: "Wir haben eine Anfrage erhalten, Ihr Passwort zurückzusetzen. Sollten Sie dies nicht angefordert haben, können Sie diese Nachricht ignorieren."
if_you_did: "Falls doch,"
please_click: "klicken Sie bitte hier zum Zurücksetzen"
embedlink:
copy: "In die Zwischenablage kopieren"
error:
COMMENT_PARENT_NOT_VISIBLE: "Der Kommentar, auf den Sie antworten möchten, wurde entfernt oder existiert nicht."
EMAIL_VERIFICATION_TOKEN_INVALID: "Code zur E-Mail-Bestätigung ist ungültig."
PASSWORD_RESET_TOKEN_INVALID: "Ihr Link zum Passwort zurücksetzen ist ungültig."
COMMENT_TOO_SHORT: "Kommentare sollten mehr als ein Zeichen enthalten, bitte überprüfen Sie Ihren Kommentar und probieren Sie es erneut."
NOT_AUTHORIZED: "Sie sind nicht berechtigt, diese Aktion auszuführen."
NO_SPECIAL_CHARACTERS: "Benutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
PASSWORD_LENGTH: "Passwort ist zu kurz"
PROFANITY_ERROR: "Nutzernamen dürfen nicht anstößig sein. Bei Fragen, wenden Sie sich an einen Administrator."
RATE_LIMIT_EXCEEDED: "Ratelimit überschritten"
USERNAME_IN_USE: "Nutzername bereits vergeben"
USERNAME_REQUIRED: "Nutzername muss angegeben werden"
EMAIL_NOT_VERIFIED: "E-Mail-Adresse nicht bestätigt"
EDIT_WINDOW_ENDED: "Sie können diesen Kommentar nicht mehr bearbeiten. Das Zeitfenster ist abgelaufen."
EDIT_USERNAME_NOT_AUTHORIZED: "Sie haben keine Berechtigung, Ihren Nutzernamen zu ändern."
SAME_USERNAME_PROVIDED: "Sie müssen einen anderen Nutzernamen wählen."
EMAIL_IN_USE: "E-Mail-Adresse bereits vergeben"
EMAIL_REQUIRED: "Eine E-Mail-Adresse ist erforderlich"
LOGIN_MAXIMUM_EXCEEDED: "Sie haben zu häufig erfolglos versucht, sich anzumelden. Bitte warten Sie."
PASSWORD_REQUIRED: "Passwort ist erforderlich"
COMMENTING_CLOSED: "Kommentarbereich ist bereits geschlossen"
NOT_FOUND: "Ressource nicht gefunden"
ALREADY_EXISTS: "Ressource existiert bereits"
INVALID_ASSET_URL: "Asset-URL ist ungültig"
CANNOT_IGNORE_STAFF: "Mitarbeiter können nicht ignoriert werden."
email: "E-Mail-Adresse ungültig"
confirm_password: "Passwörter nicht identisch. Bitte erneut überprüfen"
network_error: "Server-Verbindung fehlgeschlagen. Bitte überprüfen Sie ihre Internetverbindung und versuchen Sie es erneut."
email_not_verified: "E-Mail-Adresse {0} nicht bestätigt."
email_password: "E-Mail und/oder Passwort inkorrekt."
organization_name: "Namen von Organisationen dürfen nur Buchstaben und Zahlen enthalten."
password: "Passwort muss mindestens 8 Zeichen enthalten"
username: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
unexpected: "Unerwarteter Fehler aufgetreten. Es tut uns leid!"
flag_comment: "Kommentar melden"
flag_reason: "Grund der Meldung (optional)"
flag_username: "Nutzername melden"
framework:
banned_account_header: "Ihr Konto ist aktuell gesperrt."
banned_account_body: "Dies bedeutet, dass Sie weder Kommentare schreiben, noch auf andere Weise mit der Community interagieren können."
comment: Kommentar
comment_is_ignored: "Dieser Kommentar ist nicht sichtbar, da Sie den Nutzer ignorieren."
comment_is_rejected: "Sie haben diesen Kommentar abgelehnt."
comment_is_hidden: "Dieser Kommentar ist nicht verfügbar."
comments: Kommentare
configure_stream: "Konfigurieren"
content_not_available: "Dieser Inhalt ist nicht verfügbar"
edit_name:
button: Senden
error: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
label: "Neuer Nutzername"
msg: "Ihr Konto ist vorübergehend gesperrt, da Ihr Nutzername als unangemessen eingestuft wurde. Um Ihr Konto wieder herzustellen, geben Sie bitte einen neuen Nutzernamen ein. Bei Fragen, kontaktieren Sie uns bitte."
changed_name:
msg: "Die Änderung Ihres Nutzernamens wird von unseren Moderatoren geprüft."
my_comments: "Meine Kommentare"
my_profile: "Mein Profil"
new_count: "{0} {1} mehr anzeigen"
profile: Profil
show_all_comments: "Alle Kommentare anzeigen"
success_bio_update: "Ihre Biographie wurde aktualisiert"
success_name_update: "Ihr Nutzername wurde aktualisiert"
success_update_settings: "Ihre Änderungen wurden für den Kommentarbereich dieses Artikels angewandt"
show_all_replies: Alle Antworten anzeigen
show_more_replies: Weitere Antworten anzeigen
view_more_comments: "Weitere Kommentare anzeigen"
view_reply: "Antwort anzeigen"
from_settings_page: "Sie können Ihren Kommentarverlauf auf Ihrer Profilseite sehen."
like: Gefällt mir
loading_results: "Lädt Ergebnisse"
marketing: "Dies scheint Werbung zu sein"
moderate_this_stream: "Diesen Kommentarbereich moderieren"
flags:
reasons:
user:
username_offensive: "Unangemessen"
username_nolike: "Unerwünscht"
username_impersonating: "Identität unklar"
username_spam: "Spam"
username_other: "Anderes"
comment:
comment_offensive: "Unangemessen"
comment_spam: "Spam"
comment_noagree: "Andere Meinung"
comment_other: "Anderes"
suspect_word: "Verdächtiges Wort"
banned_word: "Unzulässiges Wort"
body_count: "Text überschreitet Zeichenlimit"
trust: "Vertrauen"
links: "Link"
modqueue:
account: "Konto-Markierungen"
actions: Aktionen
all: Alle
all_streams: "Alle Kommentarbereiche"
notify_edited: '{0} bearbeitete Kommentar "{1}"'
notify_accepted: '{0} genehmigte Kommentar "{1}"'
notify_rejected: '{0} lehnte Kommentar "{1}" ab'
notify_flagged: '{0} markierte Kommentar "{1}"'
notify_reset: '{0} setzte Status von "{1}" zurück'
approve: "Freigeben"
approved: "Freigegeben"
ban_user: "Sperren"
billion: Mrd
close: Schließen
empty_queue: "Keine weiteren Kommentare zu moderieren! Arbeite nicht zu viel, gönn dir eine Pause!"
flagged: markiert
reported: gemeldet
less_detail: "Weniger Details"
likes: Gefällt mir
million: Mio
mod_faster: "Schneller moderieren mit Tastaturkürzeln"
moderate: "Moderieren →"
more_detail: "Mehr Details"
new: Neu
newest_first: "Neueste zuerst"
navigation: Navigation
next_comment: "Gehe zum nächsten Kommentar"
toggle_search: "Suche öffnen"
next_queue: "Nächste Liste"
oldest_first: "Älteste zuerst"
premod: Unmoderiert
prev_comment: " Gehe zum vorherigen Kommentar"
reject: "Ablehnen"
rejected: "Abgelehnt"
reply: "Antworten"
select_stream: "Kommentarbereich wählen"
shift_key: "⇧"
shortcuts: "Tastaturkürzel"
sort: "Sortieren"
show_shortcuts: "Tastaturkürzel anzeigen"
singleview: "Zen-Modus"
thismenu: "Dieses Menü öffnen"
jump_to_queue: "Zu bestimmter Liste springen"
thousand: T
try_these: "Versuch diese"
view_more_shortcuts: "Weitere Tastaturkürzel anzeigen"
my_comment_history: "Mein Kommentarverlauf"
name: Name
no_agree_comment: "Ich bin mit diesem Kommentar nicht einverstanden"
no_like_bio: "Ich mag diese Biographie nicht"
no_like_username: "Ich mag diesen Nutzernamen nicht"
already_flagged_username: "Sie haben diesen Nutzernamen schon markiert."
other: Sonstiges
permalink: Teilen
personal_info: "Dieser Kommentar enthält zu viel personenbezogene Daten"
post: Post # Kontext?
profile: Profil
profile_settings: "Profil-Einstellungen"
reply: Antworten
report: Melden
report_notif: "Vielen Dank für Ihre Meldung. Unsere Moderatoren wurden informiert und werden sich in Kürze darum kümmern."
report_notif_remove: "Ihre Meldung wurde entfernt."
reported: Gemeldet
settings:
from_settings_page: "Sie können auf Ihrer Profilseite Ihren Kommentarverlauf einsehen."
my_comment_history: "Mein Kommentarverlauf"
profile: Profil
profile_settings: "Profil-Einstellungen"
sign_in: "Anmelden"
to_access: "um auf Profil zuzugreifen"
user_no_comment: "Keine Kommentare vorhanden. Nehmen Sie an der Diskussion teil!"
stream:
all_comments: "Alle Kommentare"
temporarily_suspended: "Entsprechend der Community-Regeln von {0} wurde Ihr Konto vorübergehend gesperrt. Nehmen Sie {1} wieder an der Diskussion teil."
comment_not_found: "Kommentar nicht gefunden"
no_comments: "Es gibt noch keine Kommentare. Schreiben Sie doch einen..."
no_comments_and_closed: "Es gab zu diesem Artikel keine Kommentare."
step_1_header: "Ein Problem melden"
step_2_header: "Begründung"
step_3_header: "Danke für Ihren Hinweis"
streams:
all: Alle
article: Artikel
closed: Geschlossen
empty_result: "Ihre Suche war ohne Ergebnisse. Versuchen Sie, Ihre Anfrage weiter zu fassen."
filter_streams: "Kommentarbereiche filtern"
newest: Neueste
oldest: Älteste
open: Offen
pubdate: "Veröffentlichungsdatum"
search: Suchen
sort_by: "Sortieren nach"
status: "Status"
stream_status: "Status"
suspenduser:
title_suspend: "Nutzer vorübergehend sperren"
description_suspend: "Sie sperren vorübergehend {0}. Dieser Kommentar wird auf die Abgelehnt-Liste verschoben und der Nutzer wird keine Aktionen ausführen können, bis die Sperrung abgelaufen ist."
select_duration: "Sperrdauer festlegen"
one_hour: "1 Stunde"
hours: "{0} Stunden"
days: "{0} Tage"
cancel: "Abbrechen"
suspend_user: "Nutzer vorübergehend sperren"
email_message_suspend: "Sehr geehrte/r {0}, entsprechend der Community-Richtlinien von {1} wurde Ihr Konto vorübergehend gesperrt. Während der Sperrung können Sie weder kommentieren noch andere Aktionen ausführen. Nehmen Sie {2} wieder an der Diskussion teil."
title_notify: "Den Nutzer über die vorübergehende Kontosperrung informieren"
notify_suspend_until: "Nutzer {0} wurde vorübergehend gesperrt. Diese Sperrung endet automatisch {1}."
description_notify: "Während der Sperrung hat der Nutzer keinen Zugriff auf das Konto."
write_message: "Nachricht verfassen"
send: Senden
reject_username:
username: Nutzername
no_cancel: "Nein, abbrechen"
description_reject: "Möchten Sie diesen Nutzer vorübergehend sperren (Grund: {0})? Der Nutzer kann das Konto erst wieder nach Änderung des {0} nutzen."
title_notify: "Nutzer über die vorübergehende Sperrung benachrichtigen"
description_notify: "Die Sperrung wird das Nutzerkonto vorübergehend deaktivieren."
title_reject: "Uns ist aufgefallen, dass Sie einen Nutzernamen abgelehnt haben"
suspend_user: "Nutzer vorübergehend sperren"
yes_suspend: "Ja, vorübergehend sperren"
email_message_reject: "Ein Community-Mitglied hat Ihren Nutzernamen zur Überprüfung gemeldet. Er wurde als unangemessen eingestuft und abgelehnt. Sie können Ihr Konto erst wieder voll nutzen, wenn Sie Ihren Namen ändern. Setzen Sie sich bei weiteren Fragen mit uns in Verbindung."
write_message: "Nachricht schreiben"
send: Senden
thank_you: "Vielen Dank für Ihre Nachricht. Wir nehmen Ihre Meldung ernst und ein Moderator wird sie umgehend prüfen."
user:
bio_flags: "Markierungen für diese Biographie"
user_bio: "Nutzer-Biographie"
username_flags: "Markierungen für diesen Nutzernamen"
user_detail:
remove_suspension: "Vorübergehende Sperrung aufheben"
suspend: "Nutzer vorübergehend sperren"
remove_ban: "Sperre aufheben"
ban: "Nutzer sperren"
member_since: "Mitglied seit"
email: "E-Mail"
total_comments: "Anzahl Kommentare"
reject_rate: "Ablehn-Rate"
reports: "Meldungen"
all: "Alle"
rejected: "Abgelehnte"
account_history: "Konto-Verlauf"
user_impersonating: "Gibt sich für jemand anderen aus"
user_no_comment: "Sie haben noch keinen Kommentar abgegeben. Teilen Sie Ihre Meinung mit uns!"
username_offensive: "Dieser Nutzername ist unangemessen"
view_conversation: "Diskussion ansehen"
install:
initial:
description: "Erstellen Sie in wenigen Schritten Ihre Talk-Community."
submit: "Los gehts!"
add_organization:
description: "Geben Sie den Namen Ihrer Organisation ein. Dieser erscheint in E-Mails, wenn Sie neue Mitglieder einladen."
label: "Name der Organisation"
save: "Speichern"
create:
email: "E-Mail-Adresse"
username: "Nutzername"
password: "Passwort"
confirm_password: "Passwort bestätigen"
save: "Speichern"
permitted_domains:
title: "Zugelassene Domains"
description: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll. Beispiel: localhost:3000 staging.domain.com domain.com."
submit: "Installation abschließen"
final:
description: "Danke, dass Sie Talk benutzen! Bitte bestätigen Sie Ihre E-Mail-Adresse über den Link, den wir an Sie versandt haben. Während Sie die Einrichtung Ihres Kontos abschließen, können Sie sich schon mit Ihren Lesern beschäftigen."
launch: "Talk starten"
close: "Schließen"
admin_sidebar:
view_options: "Ansichtsoptionen"
sort_comments: "Kommentare sortieren"
+20 -1
View File
@@ -23,6 +23,7 @@ en:
click_to_confirm: "Click below to confirm your email address"
confirm: "Confirm"
password_reset:
mail_sent: 'If you have a registered account, a password reset link was sent to that email'
set_new_password: "Change Your Password"
new_password: "New Password"
new_password_help: "Password must be at least 8 characters"
@@ -236,6 +237,8 @@ en:
password: "Password must be at least 8 characters"
username: "Usernames can contain letters numbers and _ only"
unexpected: "Unexpected error occurred. Sorry!"
required_field: "This field is required"
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
flag_comment: "Report comment"
flag_reason: "Reason for reporting (Optional)"
flag_username: "Report username"
@@ -366,7 +369,7 @@ en:
stream:
all_comments: "All Comments"
temporarily_suspended: "In accordance with {0}'s community guidelines, your account has been temporarily suspended. Please rejoin the conversation {1}."
comment_not_found: "Comment was not found"
comment_not_found: "This comment has been removed or does not exist."
no_comments: "There are no comments yet, why dont you write one?"
no_comments_and_closed: "There were no comments on this article."
step_1_header: "Report an issue"
@@ -418,6 +421,19 @@ en:
bio_flags: "flags for this bio"
user_bio: "User Bio"
username_flags: "flags for this username"
user_detail:
remove_suspension: "Remove Suspension"
suspend: "Suspend User"
remove_ban: "Remove Ban"
ban: "Ban User"
member_since: "Member Since"
email: "Email"
total_comments: "Total Comments"
reject_rate: "Reject Rate"
reports: "Reports"
all: "All"
rejected: "Rejected"
account_history: "Account History"
user_impersonating: "This user is impersonating"
user_no_comment: "You've never left a comment. Join the conversation!"
username_offensive: "This username is offensive"
@@ -444,3 +460,6 @@ en:
description: "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now."
launch: "Launch Talk"
close: "Close this Installer"
admin_sidebar:
view_options: "View Options"
sort_comments: "Sort Comments"

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