Compare commits

..
150 Commits
Author SHA1 Message Date
Kim Gardner 2564aa98bb Merge pull request #916 from coralproject/author-menu
Author menu, member since, and ignored users
2017-09-05 09:03:17 +01:00
Chi Vinh Le 612ed3a16d Merge branch 'author-menu' of ssh://github.com/coralproject/talk into author-menu 2017-09-05 02:24:55 +07:00
Chi Vinh Le a5acf8ba82 Add some global css classes 2017-09-05 02:22:10 +07:00
Kiwi 00b46fe0ea Merge branch 'master' into author-menu 2017-09-04 20:27:19 +02:00
Chi Vinh Le a722456359 reorder 2017-09-05 01:25:16 +07:00
Chi Vinh Le 943c9decba Add to default plugins 2017-09-05 01:18:14 +07:00
Chi Vinh Le 77396ccc31 Fix graphql tree bug 2017-09-05 01:17:05 +07:00
Chi Vinh Le e1a5a64b39 Pluginize stop ignoring user feature in profile 2017-09-05 01:09:43 +07:00
Chi Vinh Le 44e3d0b811 Add title 2017-09-05 00:28:07 +07:00
Chi Vinh Le 07f5f42870 Optimized render 2017-09-05 00:16:27 +07:00
Chi Vinh Le 123935c8bc Also notify on errors 2017-09-05 00:00:27 +07:00
Chi Vinh Le 2d4adb60be Show notification on success 2017-09-04 23:55:44 +07:00
Chi Vinh Le f8297e9652 Hide menu after action 2017-09-04 23:29:36 +07:00
Chi Vinh Le 74e8c28f45 Apply styling 2017-09-04 23:13:00 +07:00
Chi Vinh Le 89258638d4 Correct exclude if logic 2017-09-04 22:35:05 +07:00
Kim Gardner 902670cdac Merge pull request #914 from coralproject/ignored
Top-level comments by ignored user are tombstoned, but replies should be as well
2017-09-04 11:32:10 +01:00
Kim Gardner f11cd3c676 Merge branch 'master' into ignored 2017-09-04 11:19:48 +01:00
Kim Gardner f088b4d459 Merge pull request #915 from coralproject/fix-live-modqueue
Use correct moderation queues during updates
2017-09-04 11:18:29 +01:00
Chi Vinh Le c0252e160b Fix reply fragments 2017-09-04 16:44:53 +07:00
Chi Vinh Le 98cb34f554 Don't ignore myself 2017-09-04 16:25:19 +07:00
Chi Vinh Le b2ef7fa795 Fix tombstone bug 2017-09-04 15:51:04 +07:00
Chi Vinh Le ffef2f640d First implementation of ignore user plugin 2017-09-04 15:40:39 +07:00
Chi Vinh Le 26bae69454 Fix bug when determining moderation queues for live updates 2017-09-03 19:36:02 +07:00
Chi Vinh Le c07eaf0edb Add member since 2017-09-01 23:15:00 +07:00
Chi Vinh Le 9c2ea693b9 Add hover styles 2017-09-01 22:29:30 +07:00
Chi Vinh Le 7c339aa510 Render contentSlot if set 2017-09-01 22:25:26 +07:00
Belén Curcio 2ed6bb4067 Merge branch 'master' into ignored 2017-09-01 12:23:21 -03:00
Chi Vinh Le 692d92f645 Implement talk-plugin-author-menu 2017-09-01 22:20:49 +07:00
Belen Curcio 7bb4b03daa Retrieving me from Comment triggers a change 2017-09-01 12:17:48 -03:00
Kim Gardner e132f1d652 Merge pull request #913 from coralproject/redis-fix
Fixed Redis Connection Error
2017-09-01 15:53:10 +01:00
Wyatt Johnson 644788ea31 removed unused option 2017-09-01 08:47:08 -06:00
Wyatt Johnson e368c50687 ioredis changed config, fixes #901 2017-09-01 08:44:25 -06:00
Kim Gardner 4569c1bd4f Merge pull request #911 from coralproject/premodqueue
Create both a PreMod and a New Queue when stream is switched from one status to another
2017-09-01 10:30:13 +01:00
Kim Gardner d6c46cb6d1 Merge branch 'master' into premodqueue 2017-09-01 10:25:30 +01:00
Kim Gardner faeadeb61d Merge pull request #912 from coralproject/embed-fix
Fixed path joining for embed link
2017-09-01 10:01:34 +01:00
Wyatt Johnson d161733cdc fixed path joining 2017-08-31 13:59:00 -06:00
Wyatt Johnson 2fd6d370c8 Merge branch 'master' into premodqueue 2017-08-31 13:16:41 -06:00
Wyatt Johnson 824abcdfa3 Merge pull request #902 from coralproject/ioredis
Replace node_redis with ioredis
2017-08-31 11:57:38 -06:00
Belen Curcio 36c4c6a59f Ready 2017-08-31 14:22:59 -03:00
Belen Curcio 99235df566 No premod status to the New Queue 2017-08-31 13:17:34 -03:00
Belen Curcio af0e9bc880 New Queue should appear if there are comments with NONE status 2017-08-31 13:02:07 -03:00
Belen Curcio 23c8e2a0e6 Refetching settings :) 2017-08-31 12:53:47 -03:00
Wyatt Johnson c17fcfe5ef Merge branch 'master' into ioredis 2017-08-31 09:24:20 -06:00
Belen Curcio f81d5f8957 Grabbing settings from the store 2017-08-31 11:41:20 -03:00
Belen Curcio db55689f75 premod and new 2017-08-31 11:30:11 -03:00
Belen Curcio f140efe1bd Premod and New Queue Edge Case 2017-08-31 11:10:28 -03:00
Kim Gardner fe638a4c54 Merge pull request #910 from coralproject/edit-should-disappear
Make rendering `edit` pure
2017-08-31 13:13:25 +01:00
Chi Vinh Le b61afbc1d6 Make rendering edit pure 2017-08-31 19:04:54 +07:00
Kim Gardner 82e95dc4a1 Merge pull request #908 from coralproject/hide-empty-categories
Hide empty categories
2017-08-31 11:18:31 +01:00
Chi Vinh Le 3b7d006d1e Hide empty categories take 2 2017-08-31 16:59:04 +07:00
Chi Vinh Le b5daf5882b IfSlotIsEmpty and IfSlotIsNotEmpty now only renders a single child 2017-08-31 16:58:01 +07:00
Kim Gardner 11f5460604 Merge branch 'master' into hide-empty-categories 2017-08-31 10:45:52 +01:00
Kim Gardner 6e90dd6d62 Merge pull request #909 from coralproject/cleanup-plugins-default
Cleanup default plugins
2017-08-31 10:44:17 +01:00
Kiwi 635f6d7720 Merge branch 'master' into hide-empty-categories 2017-08-31 11:39:17 +02:00
Kiwi a4b40f2c46 Merge branch 'master' into cleanup-plugins-default 2017-08-31 11:39:13 +02:00
Kim Gardner 14541b48a1 Merge pull request #907 from coralproject/hide-on-click
Hide menu when clicking on a sort option
2017-08-31 10:38:56 +01:00
Chi Vinh Le c7c5167311 Cleanup default plugins 2017-08-31 16:36:08 +07:00
Chi Vinh Le e39c377e08 Hide empty categories 2017-08-31 16:33:51 +07:00
Chi Vinh Le 2887b20e38 Merge branch 'master' into hide-on-click 2017-08-31 16:32:46 +07:00
Chi Vinh Le 0a631cfcae Hide viewing options when changing sort 2017-08-31 16:12:28 +07:00
Kim Gardner 12c54c39db Merge branch 'master' into ioredis 2017-08-31 08:51:42 +01:00
Kim Gardner 5ad92972b1 Merge pull request #903 from coralproject/paginate-flagged-users
Paginate flagged users and more
2017-08-31 08:42:00 +01:00
Kim Gardner 201f6fd8ae Merge branch 'master' into paginate-flagged-users 2017-08-31 08:32:17 +01:00
Kim Gardner b0a6417848 Merge pull request #906 from gjtorikian/capitalize-the-h
Capitalize the H
2017-08-31 08:32:05 +01:00
Garen J. Torikian f80e4af683 Capitalize the H 2017-08-30 23:22:33 -04:00
Wyatt Johnson 7f9f864aeb Merge branch 'master' into paginate-flagged-users 2017-08-30 16:11:13 -06:00
Wyatt Johnson e9f5fcda2a Merge branch 'master' into ioredis 2017-08-30 16:06:36 -06:00
Wyatt Johnson b1c9d07ca7 Merge pull request #905 from coralproject/fix-query-event-names
Use correct event name in queries
2017-08-30 14:40:08 -06:00
Wyatt Johnson 53c32518b4 Merge branch 'master' into paginate-flagged-users 2017-08-30 14:14:45 -06:00
Kiwi 9d08dc2490 Merge branch 'master' into ioredis 2017-08-30 21:52:30 +02:00
Chi Vinh Le e5c385ae21 Use correct event name in queries 2017-08-31 02:05:00 +07:00
Belén Curcio 5e5e0028c9 Merge pull request #904 from coralproject/exclude-ignored-fc
Exclude Ignored users in Featured Comments Plugin
2017-08-30 16:01:36 -03:00
Belen Curcio cb7e7eae97 Using a variable 2017-08-30 15:45:25 -03:00
Belen Curcio e98432b17e Exclude Ignored 2017-08-30 15:35:52 -03:00
Chi Vinh Le 5434413308 Use fetchPolicy network-only 2017-08-30 23:47:48 +07:00
Chi Vinh Le 8b4f21ff4c Remove extra long transition timeout :-D 2017-08-30 23:27:48 +07:00
Chi Vinh Le 4072c39f05 Merge branch 'master' into paginate-flagged-users
Conflicts:
	client/coral-admin/src/routes/Community/components/FlaggedAccounts.js
	client/coral-admin/src/routes/Community/components/User.js
	client/coral-admin/src/routes/Community/containers/Community.js
	graph/loaders/users.js
2017-08-30 22:56:30 +07:00
Chi Vinh Le 9b9476d44f Add todos 2017-08-30 22:51:16 +07:00
Wyatt Johnson 2e72fec4d7 Merge branch 'master' into ioredis 2017-08-30 09:34:14 -06:00
Chi Vinh Le 1f489ff0e5 Always refetch when switching to FlaggedAccounts 2017-08-30 22:30:53 +07:00
Chi Vinh Le f582cc2534 Add transition effect 2017-08-30 22:25:03 +07:00
Wyatt Johnson a2536eb41b updated pubsub 2017-08-30 09:06:48 -06:00
Chi Vinh Le c66fb053e7 Implement mutation updators 2017-08-30 21:40:28 +07:00
Chi Vinh Le 797f0fd0e3 Docs in schema 2017-08-30 21:05:03 +07:00
Chi Vinh Le 02ec1f69ee More graphql fixes 2017-08-30 20:42:13 +07:00
Chi Vinh Le 37638cab86 Fix center styling 2017-08-30 20:41:49 +07:00
Chi Vinh Le 4ce6206516 Add deprecation and refactor notices to legacy styles 2017-08-30 19:56:24 +07:00
Chi Vinh Le 67b0624c90 Refactor styles 2017-08-30 19:48:54 +07:00
Chi Vinh Le 9348985191 Refactor into a ComponentClass 2017-08-30 19:04:30 +07:00
Chi Vinh Le 0df7df9dd6 Wrap ul before li 2017-08-30 19:03:55 +07:00
Chi Vinh Le ffab9ca8d5 Refactor users query 2017-08-30 18:48:20 +07:00
Kim Gardner b21eae78b0 Merge pull request #900 from coralproject/cli-fixes
Fixed errors when launching not-defined commands
2017-08-30 11:50:34 +01:00
Wyatt Johnson 8f3bfd2bd4 Replaced node_redis with ioredis 2017-08-29 17:52:29 -06:00
Wyatt Johnson 27661a9294 fixed errors when launching not-defined commands 2017-08-29 14:15:05 -06:00
Kim Gardner 2c3f5385ae Merge pull request #898 from coralproject/bump-version
Update version to 3.4.0
2017-08-29 19:31:42 +01:00
Wyatt Johnson 7bedfefecb Merge branch 'master' into bump-version 2017-08-29 12:10:56 -06:00
Wyatt Johnson 5ab78491b5 Merge pull request #890 from coralproject/lint-upgrade
Upgraded/pruned deps, applied linting fixes
2017-08-29 12:10:47 -06:00
Kim Gardner a83261397d Update version to 3.4.0 2017-08-29 19:06:46 +01:00
Wyatt Johnson c8d1871418 Merge branch 'master' into lint-upgrade 2017-08-29 11:56:59 -06:00
Kim Gardner 081cdfa145 Merge pull request #897 from coralproject/sort-enhancements
Sort enhancements
2017-08-29 18:50:34 +01:00
Kiwi fecfcf91ef Merge branch 'master' into sort-enhancements 2017-08-29 19:39:59 +02:00
Chi Vinh Le 90a0446afa Move Spinner down to replacing TabContent 2017-08-30 00:35:55 +07:00
Kim Gardner ec5c7f32b0 Merge pull request #895 from coralproject/my-comment-reactions
My comment reactions
2017-08-29 18:32:54 +01:00
Chi Vinh Le 21e4cf5865 Live update replies on all sort and always refetch 2017-08-30 00:27:14 +07:00
Kim Gardner 6894060648 Merge branch 'master' into my-comment-reactions 2017-08-29 18:24:54 +01:00
Kim Gardner 95d14fc5a6 Merge pull request #896 from coralproject/fix-popup-interval
Fix endless interval
2017-08-29 18:11:58 +01:00
Chi Vinh Le 46c031b35d Fix endless interval 2017-08-29 23:53:00 +07:00
Chi Vinh Le ddced5b8c9 Refactor styling 2017-08-29 23:43:58 +07:00
Wyatt Johnson c786805505 small bug fix 2017-08-29 10:28:30 -06:00
Wyatt Johnson 75125993b9 Added patch for missing action summary type 2017-08-29 10:21:31 -06:00
Kim Gardner 393005fce6 Merge pull request #889 from coralproject/my-comment-reactions
My Comment Reactions
2017-08-29 16:33:00 +01:00
Kim Gardner 952b253969 Merge branch 'master' into my-comment-reactions 2017-08-29 16:24:49 +01:00
Kim Gardner ad06d5136b Merge pull request #893 from coralproject/email-regex-fix
Changed email verification regex
2017-08-29 16:24:17 +01:00
Chi Vinh Le 18016ad23a Refactor and implement pagination 2017-08-29 22:23:35 +07:00
Kim Gardner d987724c7d Merge branch 'master' into email-regex-fix 2017-08-29 16:18:40 +01:00
Chi Vinh Le 87e90e956d Port current frontend to connections 2017-08-29 19:06:58 +07:00
Belen Curcio 9140688c65 Merge branch 'my-comment-reactions' of github.com:coralproject/talk into my-comment-reactions 2017-08-29 09:03:02 -03:00
Belen Curcio 1053c55d19 Adding zero state style for 0 counts 2017-08-29 09:01:40 -03:00
Chi Vinh Le eb67d86163 Implement UserConnection on the graph 2017-08-29 18:57:20 +07:00
Belen Curcio da92a8e7c2 Plural and singular translations 2017-08-29 08:47:23 -03:00
Kiwi d82932306d Merge branch 'master' into lint-upgrade 2017-08-29 12:47:47 +02:00
Kim Gardner a76464ea16 Merge branch 'master' into my-comment-reactions 2017-08-29 10:22:58 +01:00
Kim Gardner b76f1956dc Merge pull request #892 from coralproject/asset-tests
Added duplicated comments tests
2017-08-29 10:12:52 +01:00
Wyatt Johnson 7823b65c7d Changed email verification regex, fixes #891 2017-08-28 16:51:41 -06:00
Wyatt Johnson 38f98c7c43 Added duplicated comments tests 2017-08-28 16:41:58 -06:00
Wyatt Johnson 30e6f1d169 Merge branch 'master' into lint-upgrade 2017-08-28 16:19:10 -06:00
Wyatt Johnson df6f6a72ef Merge pull request #885 from coralproject/subscriber
Adding new Slot: commentAuthorTags
2017-08-28 16:18:32 -06:00
Wyatt Johnson c09381ff84 Merge branch 'master' into subscriber 2017-08-28 15:40:31 -06:00
Wyatt Johnson a172f5be51 Merge pull request #881 from coralproject/feature/heroku-deploy
Heroku Deploy
2017-08-28 15:39:52 -06:00
Wyatt Johnson 2fec9e3c0f Merge branch 'master' into feature/heroku-deploy 2017-08-28 15:33:24 -06:00
Wyatt Johnson eb586f217b Upgraded/pruned deps, applied linting fixes 2017-08-28 14:06:54 -06:00
Belen Curcio e77bdf0b90 linting 2017-08-28 16:31:46 -03:00
Belen Curcio 3e045291ee Translations added 2017-08-28 16:30:17 -03:00
Belen Curcio 93c1ecd12d My Comment Reactions 2017-08-28 16:25:26 -03:00
Kiwi 4f901d0d56 Merge branch 'master' into subscriber 2017-08-28 19:43:54 +02:00
Belén Curcio e14d6007a7 Merge branch 'master' into subscriber 2017-08-28 13:12:19 -03:00
Belén Curcio 96006a0196 Merge branch 'master' into subscriber 2017-08-28 12:26:08 -03:00
Belén Curcio 3108f3dac3 Merge branch 'master' into subscriber 2017-08-28 12:22:05 -03:00
Belen Curcio 12d66a49d0 Adding commentAuthorTags 2017-08-28 12:17:01 -03:00
Jeff Nelson 7c9e2297df don't hardcode branch 2017-08-28 09:54:20 -04:00
Jeff Nelson fe0c32898d Update README.md 2017-08-25 10:03:46 -04:00
Jeff Nelson 091d787e34 Update README.md 2017-08-25 10:02:16 -04:00
Jeff Nelson 7ee2794344 Merge branch 'master' into feature/heroku-deploy 2017-08-25 09:56:55 -04:00
Jeff Nelson cc66f1b459 update 2017-08-25 09:47:59 -04:00
Jeff Nelson 50caddee56 update 2017-08-25 09:47:40 -04:00
Jeff Nelson 28dafd3e07 update 2017-08-25 09:27:52 -04:00
Jeff Nelson 2f3d7c5329 set default root url 2017-08-25 09:17:51 -04:00
Jeff Nelson 4b7cc9ffcc whoops 2017-08-25 08:55:26 -04:00
Jeff Nelson faecd80543 better handling for heroku dynamic port 2017-08-25 08:51:36 -04:00
Jeff Nelson e73c113e0d use mailgun since postmark has been deprecated 2017-08-25 08:30:29 -04:00
198 changed files with 3885 additions and 3279 deletions
+3
View File
@@ -19,5 +19,8 @@ plugins/*
!plugins/talk-plugin-sort-most-liked
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
node_modules
+35 -26
View File
@@ -7,51 +7,53 @@
"parserOptions": {
"ecmaVersion": 2017
},
"plugins": [
"promise",
"json"
],
"rules": {
"indent": ["error",
2
],
"no-console": [
0
],
"no-console": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-template-curly-in-string": [1],
"no-unsafe-negation": [1],
"array-callback-return": [1],
"no-template-curly-in-string": "warn",
"no-unsafe-negation": "warn",
"array-callback-return": "warn",
"arrow-parens": ["warn", "always"],
"template-curly-spacing": "warn",
"eqeqeq": [2, "smart"],
"no-eval": [2],
"no-global-assign": [2],
"no-implied-eval": [2],
"eqeqeq": ["error", "smart"],
"no-eval": "error",
"no-global-assign": "error",
"no-implied-eval": "error",
"lines-around-comment": ["warn", {"beforeLineComment": true}],
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
"no-script-url": [2],
"no-throw-literal": [2],
"yoda": [1],
"no-path-concat": [2],
"eol-last": [1],
"no-nested-ternary": [1],
"no-tabs": [2],
"no-unneeded-ternary": [1],
"object-curly-spacing": [1],
"no-script-url": "error",
"no-throw-literal": "error",
"yoda": "warn",
"no-path-concat": "error",
"eol-last": "warn",
"no-nested-ternary": "warn",
"no-tabs": "error",
"no-unneeded-ternary": "warn",
"object-curly-spacing": "warn",
"space-infix-ops": ["error"],
"space-in-parens": ["error", "never"],
"space-unary-ops": ["error", {
"words": true,
"nonwords": false
}],
"no-const-assign": [2],
"no-duplicate-imports": [2],
"prefer-template": [1],
"no-const-assign": "error",
"no-duplicate-imports": "error",
"prefer-template": "warn",
"comma-spacing": ["error", {
"after": true
}],
"no-var": [2],
"no-lonely-if": [2],
"curly": [2],
"no-var": "error",
"no-lonely-if": "error",
"curly": "error",
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_|next",
"varsIgnorePattern": "^_"
@@ -61,6 +63,13 @@
}],
"newline-per-chained-call": ["error", {
"ignoreChainWithDepth": 2
}]
}],
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn"
}
}
+3
View File
@@ -35,5 +35,8 @@ plugins/*
!plugins/talk-plugin-sort-most-liked
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
**/node_modules/*
+2 -1
View File
@@ -1,4 +1,5 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
@@ -19,7 +20,7 @@ endpoint when the server is running with built assets.
- Blog: https://blog.coralproject.net
- Community Guides for Journalism: https://guides.coralproject.net/
- Community Guides for Journalism: https://guides.coralproject.net/
## License
+9 -5
View File
@@ -1,10 +1,15 @@
{
"name": "The Coral Project: Talk",
"env": {
"TALK_SESSION_SECRET": {
"TALK_JWT_SECRET": {
"description": "The session secret",
"generator": "secret"
},
"TALK_ROOT_URL": {
"description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.",
"value":"https://<COPY APP NAME HERE>.herokuapp.com",
"required": true
},
"TALK_FACEBOOK_APP_ID": {
"value": "",
"required": true
@@ -14,8 +19,7 @@
"required": true
},
"NODE_ENV": "production",
"TALK_SMTP_PORT": "2525",
"REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN",
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
"NPM_CONFIG_PRODUCTION": "false"
},
"addons": [{
@@ -25,8 +29,8 @@
"plan": "rediscloud:30",
"as": "REDIS"
}, {
"plan": "postmark:10k",
"as": "POSTMARK"
"plan": "mailgun:starter",
"as": "MAILGUN"
}],
"image": "heroku/nodejs",
"success_url": "/admin/install"
+19 -1
View File
@@ -4,8 +4,9 @@
* Module dependencies.
*/
// const util = require('./util');
const program = require('./commander');
const {head, map} = require('lodash');
const Matcher = require('did-you-mean');
program
.command('serve', 'serve the application')
@@ -23,6 +24,21 @@ program
)
.parse(process.argv);
// If the command wasn't found, output help.
const cmds = map(program.commands, '_name');
const cmd = head(program.args);
if (!cmds.includes(cmd)) {
const m = new Matcher(cmds);
const similarCMDs = m.list(cmd);
console.error(`cli '${cmd}' is not a talk cli command. See 'cli --help'.`);
if (similarCMDs.length > 0) {
const sc = similarCMDs.map(({value}) => `\t${value}\n`).join('');
console.error(`\nThe most similar commands are\n${sc}`);
}
process.exit(1);
}
/**
* When this provess exists, check to see if we have a running command, if we do
* check to see if it is still running. If it is, then kill it with a SIGINT
@@ -31,6 +47,8 @@ program
*/
process.once('exit', () => {
if (
// program.runningCommand &&
program.runningCommand.killed === false &&
program.runningCommand.exitCode === null
) {
+13 -8
View File
@@ -78,18 +78,23 @@ function rangeJobsByState(state = 'complete', limit) {
/**
* Cleans up the jobs that are in the queue.
*/
function cleanupJobs(options) {
async function cleanupJobs(options) {
const n = 100;
Promise.all([
rangeJobsByState('complete', n),
options.stuck ? rangeJobsByState('failed', n) : false
])
.then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs))
.then(() => {
try {
const joblists = await Promise.all([
rangeJobsByState('complete', n),
options.stuck ? rangeJobsByState('failed', n) : false
]);
await joblists.filter((jobs) => jobs).map(removeJobs);
util.shutdown();
console.log('Removed old jobs');
});
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
//==============================================================================
+1 -1
View File
@@ -53,7 +53,7 @@ function versionMatch(name, version) {
}
}
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
function reconcilePackages({quiet = false, upgradeRemote = false}) {
const fetchable = [];
+38 -45
View File
@@ -98,39 +98,32 @@ function getUserCreateAnswers(options) {
/**
* Prompts for input and registers a user based on those.
*/
function createUser(options) {
getUserCreateAnswers(options)
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
async function createUser(options) {
try {
const answers = await getUserCreateAnswers(options);
if (answers.password !== answers.confirmPassword) {
throw new Error('Passwords do not match');
}
return answers;
})
.then((answers) => {
return UsersService
.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim())
.then((user) => {
console.log(`Created user ${user.id}.`);
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
console.log(`Created user ${user.id}.`);
if (answers.roles.length > 0) {
return Promise.all(answers.roles.map((role) => {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the role ${role} to User ${user.id}.`);
});
}));
}
});
})
.then(() => {
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown();
});
if (answers.roles.length > 0) {
return Promise.all(answers.roles.map((role) => {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the role ${role} to User ${user.id}.`);
});
}));
}
util.shutdown();
} catch (err) {
console.error(err);
util.shutdown();
}
}
/**
@@ -169,21 +162,21 @@ function passwd(userID) {
validate: validateRequired('Confirm Password is required')
}
])
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
return Promise.reject(new Error('Password mismatch'));
}
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
throw new Error('Password mismatch');
}
return UsersService.changePassword(userID, answers.password);
})
.then(() => {
console.log('Password changed.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
return UsersService.changePassword(userID, answers.password);
})
.then(() => {
console.log('Password changed.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
+1 -1
View File
@@ -125,7 +125,7 @@ module.exports = async ({fix, limit, batch}) => {
// Check that the action summaries match the cached counts.
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
// Batch updates for those changes.
// Batch updates for those changes.
commentOperations.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
});
+1
View File
@@ -4,6 +4,7 @@
"es6": true,
"mocha": true
},
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
+6 -6
View File
@@ -52,16 +52,16 @@ export const newPage = () => ({
export const setRole = (id, role) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
};
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
// Ban User Dialog
+10 -10
View File
@@ -26,19 +26,19 @@ const validation = (formData, dispatch, next) => {
// Required Validation
const empty = validKeys
.filter((name) => {
const cond = !formData[name].length;
.filter((name) => {
const cond = !formData[name].length;
if (cond) {
if (cond) {
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (empty.length) {
dispatch(hasError());
+14 -14
View File
@@ -69,23 +69,23 @@ class AdminLogin extends React.Component {
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
location.href = location.href;
}}>
? <p className={styles.passwordRequestSuccess} onClick={() => {
location.href = location.href;
}}>
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
<Success />
</p>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
);
return (
<Layout fixedDrawer restricted={true}>
@@ -65,7 +65,7 @@ class SuspendUserDialog extends React.Component {
{t('suspenduser.title_suspend')}
</h1>
<p className={styles.description}>
{t('suspenduser.description_suspend', username)}
{t('suspenduser.description_suspend', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
@@ -103,7 +103,7 @@ class SuspendUserDialog extends React.Component {
{t('suspenduser.title_notify')}
</h1>
<p className={styles.description}>
{t('suspenduser.description_notify', username)}
{t('suspenduser.description_notify', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
+24 -24
View File
@@ -139,29 +139,29 @@ export default class UserDetail extends React.Component {
<hr/>
{
selectedCommentIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<Button
onClick={bulkAccept}
className={styles.bulkAction}
cStyle='approve'
icon='done'>
</Button>
<Button
onClick={bulkReject}
className={styles.bulkAction}
cStyle='reject'
icon='close'>
</Button>
{`${selectedCommentIds.length} comments selected`}
</div>
)
? (
<ul className={styles.commentStatuses}>
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<Button
onClick={bulkAccept}
className={styles.bulkAction}
cStyle='approve'
icon='done'>
</Button>
<Button
onClick={bulkReject}
className={styles.bulkAction}
cStyle='reject'
icon='close'>
</Button>
{`${selectedCommentIds.length} comments selected`}
</div>
)
}
<div>
@@ -190,7 +190,7 @@ export default class UserDetail extends React.Component {
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
/>
</Drawer>
</ClickOutside>
);
@@ -54,8 +54,8 @@ class UserDetailComment extends React.Component {
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
<div className={styles.badgeBar}>
@@ -121,10 +121,10 @@ class UserDetailComment extends React.Component {
</div>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
);
+30 -30
View File
@@ -13,9 +13,9 @@ const CoralHeader = ({
}) => (
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
<IndexLink
id='dashboardNav'
@@ -63,36 +63,36 @@ const CoralHeader = ({
</Navigation>
:
null
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank">
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank">
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</div>
</Header>
);
+4 -4
View File
@@ -55,10 +55,10 @@ class LayoutContainer extends Component {
toggleShortcutModal={toggleShortcutModal}
{...this.props}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{this.props.children}
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{this.props.children}
</Layout>
);
} else if (loggedIn) {
@@ -54,8 +54,9 @@ class SuspendUserDialogContainer extends Component {
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
settings {
organizationName
organizationName
}
}
`);
@@ -88,13 +88,13 @@ class UserDetailContainer extends React.Component {
});
}
})
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
this.isLoadingMore = false;
throw err;
});
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
this.isLoadingMore = false;
throw err;
});
};
componentWillReceiveProps(next) {
+27 -4
View File
@@ -1,10 +1,33 @@
import update from 'immutability-helper';
export default {
mutations: {
SetUserStatus: () => ({
refetchQueries: ['CoralAdmin_Community'],
SetUserStatus: ({variables: {status, userId}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
if (status !== 'APPROVED') {
return prev;
}
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
}
}),
RejectUsername: () => ({
refetchQueries: ['CoralAdmin_Community'],
RejectUsername: ({variables: {input: {id: userId}}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
}
}),
},
};
@@ -0,0 +1,4 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
@@ -1,10 +1,11 @@
import React from 'react';
import styles from './Community.css';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
// TODO: Needs refactoring.
const ActionButton = ({type = '', user, ...props}) => {
return (
<Button
@@ -1,299 +0,0 @@
@custom-media --big-viewport (min-width: 780px);
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.mainFlaggedContent {
width: 100%;
padding: 34px 14px;
box-sizing: border-box;
}
.roleButton {
display: block;
}
.searchBox {
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
i {
color: #A1A1A1
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
.list {
padding: 8px 0;
list-style: none;
display: block;
&.singleView .listItem {
display: none;
}
&.singleView .listItem.activeItem {
display: block;
height: 100%;
font-size: 1.5em;
line-height: 1.5em;
border: none;
.actions {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
margin: 0;
}
.actionButton {
transform: scale(1.4);
}
}
}
.listItem {
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: all 200ms;
padding: 10px 0 0;
min-height: 220px;
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
&:last-child {
border-bottom: none;
}
.context {
a {
color: #f36451;
text-decoration: underline;
float: right;
}
}
.sideActions {
position: absolute;
right: 0;
height: 100%;
top: 0;
padding: 50px 18px;
box-sizing: border-box;
}
.itemHeader {
display: flex;
align-items: center;
justify-content: space-between;
.author {
font-size: 16px;
font-weight: bold;
min-width: 230px;
display: flex;
align-items: center;
}
}
.itemBody {
display: flex;
justify-content: space-between;
}
.avatar {
margin-right: 16px;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: #757575;
font-size: 40px;
color: #fff;
}
.created {
color: #666;
font-size: 13px;
margin-left: 40px;
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
}
.flagged {
color: rgba(255, 0, 0, .5);
padding-top: 15px;
padding-left: 10px;
}
.flagCount{
font-size: 12px;
color: #d32f2f;
}
}
.empty {
color: #444;
margin-top: 50px;
text-align: center;
}
@media (--big-viewport) {
.listItem {
border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
&.activeItem {
border: 2px solid #333;
}
}
}
.hasLinks {
color: #f00;
text-align: right;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.banned {
color: #f00;
text-align: left;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.ban {
display: block;
text-align: center;
margin-top: 5px;
}
.banButton {
width: 114px;
letter-spacing: 1px;
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
}
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.flaggedByCount {
display: block;
text-align: left;
margin-top: 5px;
}
.flaggedByCount i {
font-size: 14px;
margin-right: 10px;
}
.flaggedBy {
display: inline;
padding: 3px;
font-size: 14px;
margin-left: 5px;
}
.flaggedByLabel {
font-weight: 600;
font-size: 14px;
}
.flaggedReasons {
margin-left: 24px;
margin-top: 10px;
}
p.flaggedByReason {
font-size: 1tpx;
margin: 0px;
line-height: 1.4;
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
vertical-align: text-bottom;
&:hover {
background-color: #E0E0E0;
}
}
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import CommunityMenu from './CommunityMenu';
import People from './People';
import FlaggedAccounts from './FlaggedAccounts';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import RejectUsernameDialog from './RejectUsernameDialog';
export default class Community extends Component {
@@ -54,7 +54,7 @@ export default class Community extends Component {
}
getTabContent(searchValue, props) {
const {community, root: {users}, viewUserDetail} = props;
const {community} = props;
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
if (activeTab === 'people') {
@@ -76,16 +76,7 @@ export default class Community extends Component {
return (
<div>
<FlaggedAccounts
commenters={users}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
showRejectUsernameDialog={props.showRejectUsernameDialog}
approveUser={props.approveUser}
rejectUsername={props.rejectUsername}
currentUser={this.props.currentUser}
viewUserDetail={viewUserDetail}
/>
<FlaggedAccounts />
<RejectUsernameDialog
open={community.rejectUsernameDialog}
handleClose={props.hideRejectUsernameDialog}
@@ -0,0 +1,21 @@
.tab {
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: border-bottom 200ms;
}
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 5px #F36451;
}
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './styles.css';
import styles from './CommunityMenu.css';
import t from 'coral-framework/services/i18n';
import {Link} from 'react-router';
@@ -0,0 +1,34 @@
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.mainFlaggedContent {
width: 100%;
padding: 34px 14px;
box-sizing: border-box;
}
.list {
margin: 0;
padding: 0;
}
.userLeave {
opacity: 1.0;
}
.userLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.userEnter {
opacity: 0;
}
.userEnterActive {
opacity: 1.0;
transition: opacity 800ms;
}
@@ -1,38 +1,74 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import styles from './Community.css';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import User from './User';
import LoadMore from '../../../components/LoadMore';
import FlaggedUser from '../containers/FlaggedUser';
import {CSSTransitionGroup} from 'react-transition-group';
import styles from './FlaggedAccounts.css';
const FlaggedAccounts = (props) => {
const {commenters} = props;
const hasResults = commenters && !!commenters.length;
class FlaggedAccounts extends React.Component {
render() {
const {
users,
loadMore,
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
approveUser,
me,
viewUserDetail,
} = this.props;
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{
hasResults
? commenters.map((commenter, index) => {
return <User
user={commenter}
key={index}
index={index}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
showRejectUsernameDialog={props.showRejectUsernameDialog}
approveUser={props.approveUser}
currentUser={props.currentUser}
viewUserDetail={props.viewUserDetail}
/>;
})
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{
hasResults
? <CSSTransitionGroup
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.userEnter,
enterActive: styles.userEnterActive,
leave: styles.userLeave,
leaveActive: styles.userLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{
users.nodes.map((user) => {
return (
<FlaggedUser
user={user}
key={user.id}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
viewUserDetail={viewUserDetail}
/>
);
})
}
</CSSTransitionGroup>
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
<LoadMore
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</div>
</div>
</div>
);
};
);
}
}
export default FlaggedAccounts;
@@ -0,0 +1,104 @@
@custom-media --big-viewport (min-width: 780px);
.root {
composes: mdl-shadow--2dp mdl-card from global;
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: box-shadow 200ms, margin-bottom 200ms;
padding: 10px 0 0;
min-height: 220px;
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
}
.rootSelected {
composes: mdl-shadow--16dp from global;
}
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.author {
display: flex;
justify-content: flex-start;
}
.body {
display: flex;
align-items: center;
justify-content: space-between;
}
.flaggedByCount {
display: block;
text-align: left;
margin-top: 5px;
}
.flagIcon {
font-size: 14px;
margin-right: 10px;
}
.flaggedBy {
display: inline;
padding: 3px;
font-size: 14px;
margin-left: 5px;
}
.flaggedByLabel {
font-weight: 600;
font-size: 14px;
}
.flaggedReasons {
margin-left: 24px;
margin-top: 10px;
}
.flaggedByReason {
font-size: 1tpx;
margin: 0px;
line-height: 1.4;
}
@media (--big-viewport) {
.root {
border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
}
&.rootSelected {
border: 2px solid #333;
}
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
vertical-align: text-bottom;
&:hover {
background-color: #E0E0E0;
}
}
@@ -0,0 +1,145 @@
import React from 'react';
import styles from './FlaggedUser.css';
import ActionButton from './ActionButton';
// TODO: Should not rely on plugin.
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
// TODO: Should work with custom flags too.
const shortReasons = {
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
[username.offensive]: t('community.offensive'),
[username.nolike]: t('community.dont_like_username'),
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
class User extends React.Component {
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
render() {
const {
user,
modActionButtons,
viewUserDetail,
selected,
approveUser,
showRejectUsernameDialog,
me,
className,
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
className={styles.button}>
{user.username}
</button>
{me.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
</div>
</div>
<div className={styles.body}>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
<span className={styles.flaggedByLabel}>
{t('community.flags')}({ user.actions.length })
</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user &&
<button onClick={() => {viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
}
return null;
}
)}
</div>;
}
)}
</div>
</div>
<div className={styles.sideActions}>
<div className={styles.actions}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={approveUser}
showRejectUsernameDialog={showRejectUsernameDialog}
/>
)}
</div>
</div>
</div>
</div>
</li>
);
}
}
export default User;
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './Community.css';
import styles from './styles.css';
import Table from '../containers/Table';
import {Pager, Icon} from 'coral-ui';
import EmptyCard from '../../../components/EmptyCard';
@@ -45,12 +45,12 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
<div className={styles.mainContent}>
{
hasResults
? <Table
? <Table
headers={tableHeaders}
commenters={commenters}
onHeaderClickHandler={props.onHeaderClickHandler}
/>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
}
<Pager
totalPages={props.totalPages}
@@ -50,9 +50,9 @@ class RejectUsernameDialog extends Component {
const next = () => this.setState({stage: stage + 1});
const suspend = () => {
rejectUsername({id: user.user.id, message: this.state.email})
.then(() => {
this.props.handleClose();
});
.then(() => {
this.props.handleClose();
});
};
const suspendModalActions = [
@@ -71,21 +71,21 @@ class RejectUsernameDialog extends Component {
const {stage} = this.state;
return <Dialog
className={styles.suspendDialog}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div className={styles.container}>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
</div>
{
stage === 1 &&
className={styles.suspendDialog}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div className={styles.container}>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
</div>
{
stage === 1 &&
<div className={styles.writeContainer}>
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
<div className={styles.emailContainer}>
@@ -96,16 +96,16 @@ class RejectUsernameDialog extends Component {
onChange={this.onEmailChange}/>
</div>
</div>
}
<div className={styles.modalButtons}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button key={i} onClick={this.onActionClick(stage, i)}>
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
</div>
</div>
</Dialog>;
}
<div className={styles.modalButtons}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button key={i} onClick={this.onActionClick(stage, i)}>
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
</div>
</div>
</Dialog>;
}
}
@@ -9,9 +9,9 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
<tr>
{headers.map((header, i) =>(
<th
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
@@ -1,117 +0,0 @@
import React from 'react';
import styles from './Community.css';
import ActionButton from './ActionButton';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import t from 'coral-framework/services/i18n';
const shortReasons = {
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
[username.offensive]: t('community.offensive'),
[username.nolike]: t('community.dont_like_username'),
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
const User = (props) => {
const {user, modActionButtons} = props;
let userStatus = user.status;
const showSuspenUserDialog = () => props.showSuspendUserDialog({
userId: user.id,
username: user.username,
});
const showBanUserDialog = () => props.showBanUserDialog({
userId: user.id,
username: user.username,
});
// Do not display unless the user status is 'pending' or 'banned'.
// This means that they have already been reviewed and approved.
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<button onClick={() => {props.viewUserDetail(user.id);}} className={styles.button}>{user.username}</button>
{props.currentUser.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={showSuspenUserDialog}>
Suspend User</ActionsMenuItem>
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
</div>
</div>
<div className={styles.itemBody}>
<div className={styles.body}>
<div className={styles.flaggedByCount}>
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{t('community.flags')}({ user.actions.length })</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user &&
<button onClick={() => {props.viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
}
return null;
}
)}
</div>;
}
)}
</div>
</div>
<div className={styles.sideActions}>
<div className={`actions ${styles.actions}`}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={props.approveUser}
showRejectUsernameDialog={props.showRejectUsernameDialog}
/>
)}
</div>
</div>
</div>
</div>
</li>;
};
export default User;
@@ -1,119 +1,56 @@
/**
* deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
@custom-media --big-viewport (min-width: 780px);
.listContainer {
max-width: 860px;
margin: 0 auto;
}
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
}
.tab {
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: border-bottom 200ms;
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 5px #F36451;
}
.active > span {
color: white;
}
.active:after {
background: transparent !important;
}
.showShortcuts {
position: absolute;
right: 130px;
.container {
padding: 10px;
display: flex;
align-items: center;
font-size: 13px;
span {
margin-left: 7px;
}
padding-bottom: 200px;
}
@media (--big-viewport) {
.tab {
flex: none;
}
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.approve {
margin-top: 10px;
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.notFound {
position: relative;
margin: 20px auto;
text-align: center;
padding: 68px 45px;
vertical-align: middle;
min-width: 500px;
a {
color: rgb(244, 126, 107);
font-weight: 500;
&.goToStreams {
position: absolute;
right: 10px;
bottom: 10px;
}
}
.roleButton {
display: block;
}
.header {
background-color: #2c2c2c;
color: white;
margin-bottom: -1px;
.searchBox {
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
.settingsButton {
i {
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
}
i {
color: #A1A1A1
}
.moderateAsset {
a {
text-align: center;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: opacity 200ms;
opacity: 1;
&:hover {
opacity: .8;
cursor: pointer;
}
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
@custom-media --big-viewport (min-width: 780px);
.list {
padding: 8px 0;
list-style: none;
@@ -134,15 +71,12 @@ span {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
margin: 0;
}
.actionButton {
transform: scale(1.4);
}
}
}
@@ -159,6 +93,7 @@ span {
min-height: 220px;
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
@@ -184,9 +119,9 @@ span {
right: 0;
height: 100%;
top: 0;
padding: 40px 18px;
padding: 50px 18px;
box-sizing: border-box;
}
}
.itemHeader {
display: flex;
@@ -194,6 +129,8 @@ span {
justify-content: space-between;
.author {
font-size: 16px;
font-weight: bold;
min-width: 230px;
display: flex;
align-items: center;
@@ -221,18 +158,12 @@ span {
margin-left: 40px;
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
}
.flagged {
@@ -245,11 +176,8 @@ span {
font-size: 12px;
color: #d32f2f;
}
}
.empty {
color: #444;
margin-top: 50px;
@@ -301,34 +229,14 @@ span {
margin-top: 5px;
}
.Comment {
.moderateArticle {
font-size: 12px;
a {
display: inline-block;
color: #679af3;
text-decoration: none;
font-size: 1em;
font-weight: 400;
letter-spacing: .5px;
font-size: 12px;
margin-left: 10px;
.banButton {
width: 114px;
letter-spacing: 1px;
&:hover {
text-decoration: underline;
opacity: .9;
cursor: pointer;
}
}
}
}
.flagBox {
max-width: 480px;
border-top: 1px solid rgba(66, 66, 66, 0.12);
h3 {
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
margin: 0;
font-weight: 500;
}
}
@@ -1,23 +1,15 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
import {compose} from 'react-apollo';
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {
fetchAccounts,
updateSorting,
newPage,
showRejectUsernameDialog,
hideRejectUsernameDialog
} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import Community from '../components/Community';
@@ -27,85 +19,26 @@ class CommunityContainer extends Component {
this.props.fetchAccounts({});
}
approveUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'APPROVED'});
}
banUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'BANNED'});
}
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (!('users' in this.props.root)) {
return <div><Spinner/></div>;
}
return (
<Community {...this.props} approveUser={this.approveUser} banUser={this.banUser}/>
<Community {...this.props} />
);
}
}
export const withCommunityQuery = withQuery(gql`
query CoralAdmin_Community($action_type: ACTION_TYPE) {
users(query:{action_type: $action_type}){
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
}
`, {
options: ({params: {action_type = 'FLAG'}}) => {
return {
variables: {
action_type: action_type
}
};
}
});
const mapStateToProps = (state) => ({
community: state.community,
currentUser: state.auth.user,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
fetchAccounts,
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
hideRejectUsernameDialog,
updateSorting,
newPage,
viewUserDetail,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withCommunityQuery,
withSetUserStatus,
withRejectUsername,
)(CommunityContainer);
@@ -0,0 +1,128 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
import {withSetUserStatus} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {showRejectUsernameDialog} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import {getDefinitionName} from 'coral-framework/utils';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
class FlaggedAccountsContainer extends Component {
constructor(props) {
super(props);
}
approveUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'APPROVED'});
}
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.users.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
const updated = update(previous, {
users: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
},
});
return updated;
},
});
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
}
return (
<FlaggedAccounts
showBanUserDialog={this.props.showBanUserDialog}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showRejectUsernameDialog={this.props.showRejectUsernameDialog}
viewUserDetail={this.props.viewUserDetail}
approveUser={this.approveUser}
loadMore={this.loadMore}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
me={this.props.root.me}
/>
);
}
}
const LOAD_MORE_QUERY = gql`
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
hasNextPage
endCursor
nodes {
__typename
...${getDefinitionName(FlaggedUser.fragments.user)}
}
}
}
${FlaggedUser.fragments.user}
`;
export const withFlaggedAccountsyQuery = withQuery(gql`
query TalkAdmin_FlaggedAccounts {
...${getDefinitionName(FlaggedUser.fragments.root)}
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
hasNextPage
endCursor
nodes {
__typename
...${getDefinitionName(FlaggedUser.fragments.user)}
}
}
me {
__typename
...${getDefinitionName(FlaggedUser.fragments.me)}
}
}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.user}
${FlaggedUser.fragments.me}
`, {
options: {
fetchPolicy: 'network-only',
},
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
viewUserDetail,
}, dispatch);
export default compose(
connect(null, mapDispatchToProps),
withFlaggedAccountsyQuery,
withSetUserStatus,
)(FlaggedAccountsContainer);
@@ -0,0 +1,42 @@
import {gql} from 'react-apollo';
import FlaggedUser from '../components/FlaggedUser';
import {withFragments} from 'plugin-api/beta/client/hocs';
export default withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
__typename
}
`,
me: gql`
fragment TalkAdminCommunity_FlaggedUser_me on User {
id
}
`,
user: gql`
fragment TalkAdminCommunity_FlaggedUser_user on User {
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
`
})(FlaggedUser);
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
.container {
display: flex;
@@ -111,20 +111,20 @@ export default class Configure extends Component {
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
<List onChange={this.changeSection} activeItem={activeSection}>
<Item itemId='stream' icon='speaker_notes'>
{t('configure.stream_settings')}
</Item>
<Item itemId='moderation' icon='thumbs_up_down'>
{t('configure.moderation_settings')}
</Item>
<Item itemId='tech' icon='code'>
{t('configure.tech_settings')}
</Item>
</List>
<div className={styles.saveBox}>
<div className={styles.container}>
<div className={styles.leftColumn}>
<List onChange={this.changeSection} activeItem={activeSection}>
<Item itemId='stream' icon='speaker_notes'>
{t('configure.stream_settings')}
</Item>
<Item itemId='moderation' icon='thumbs_up_down'>
{t('configure.moderation_settings')}
</Item>
<Item itemId='tech' icon='code'>
{t('configure.tech_settings')}
</Item>
</List>
<div className={styles.saveBox}>
{
showSave ?
<Button
@@ -136,25 +136,25 @@ export default class Configure extends Component {
>
{t('configure.save_changes')}
</Button>
:
:
<Button
raised
disabled
icon='check'
full
>
{t('configure.save_changes')}
</Button>
{t('configure.save_changes')}
</Button>
}
</div>
</div>
</div>
<div className={styles.mainContent}>
{ this.props.saveFetchingError }
{ this.props.fetchSettingsError }
{ section }
</div>
</div>
<div className={styles.mainContent}>
{ this.props.saveFetchingError }
{ this.props.fetchSettingsError }
{ section }
</div>
</div>
);
}
}
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import t from 'coral-framework/services/i18n';
import {join} from 'path';
import styles from './Configure.css';
import {Button, Card} from 'coral-ui';
import {BASE_URL} from 'coral-framework/constants/url';
@@ -25,7 +26,7 @@ class EmbedLink extends Component {
}
render () {
const coralJsUrl = [BASE_URL, '/embed.js'].join('');
const coralJsUrl = join(BASE_URL, '/embed.js');
const nonce = String(Math.random()).slice(2);
const streamElementId = `coral_talk_${nonce}`;
const embedText = `
@@ -93,15 +93,15 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
value={settings.charCount}
disabled={settings.charCountEnable ? '' : 'disabled'}
/>
<span>{t('configure.comment_count_text_post')}</span>
{
errors.charCount &&
<span>{t('configure.comment_count_text_post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{t('configure.comment_count_error')}
</span>
}
}
</p>
</div>
</Card>
@@ -14,19 +14,19 @@ const ActivityWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{asset.commentCount}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
? assets.map((asset) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{asset.commentCount}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
}
</div>
</div>
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
.Dashboard {
display: flex;
}
@@ -16,24 +16,24 @@ const FlagWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
}
? assets.map((asset) => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
}
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
}
</div>
</div>
@@ -15,20 +15,20 @@ const LikeWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
? assets.map((asset) => {
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
}
</div>
</div>
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
:root {
--row-height: 60px;
}
@@ -42,15 +42,15 @@ export const witDashboardQuery = withQuery(gql`
}
}
`, {
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
return {
variables: {
from: dashboardWindowStart,
to: dashboardWindowEnd
}
};
}
});
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
return {
variables: {
from: dashboardWindowStart,
to: dashboardWindowEnd
}
};
}
});
const mapStateToProps = (state) => {
return {
@@ -1,12 +1,12 @@
.Install {
.install {
max-width: 900px;
margin: 0 auto;
text-align: center;
padding: 50px 0;
}
h2 {
font-size: 2em;
font-weight: 500;
margin: 0;
}
.header {
font-size: 2em;
font-weight: 500;
margin: 0;
}
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
import styles from './style.css';
import styles from './Install.css';
import {Wizard, WizardNav} from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
@@ -37,11 +37,11 @@ export default class Install extends Component {
return (
<Layout restricted={true}>
<div className={styles.Install}>
<div className={styles.install}>
{
!install.alreadyInstalled ? (
<div>
<h2>Welcome to the Coral Project</h2>
<h2 className={styles.header}>Welcome to the Coral Project</h2>
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
<Wizard
currentStep={install.step}
@@ -19,7 +19,7 @@ const InitialStep = (props) => {
showErrors={install.showErrors}
errorMsg={install.errors.email}
noValidate
/>
/>
<TextField
className={styles.textField}
@@ -29,7 +29,7 @@ const InitialStep = (props) => {
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.username}
/>
/>
<TextField
className={styles.textField}
@@ -39,7 +39,7 @@ const InitialStep = (props) => {
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.password}
/>
/>
<TextField
className={styles.textField}
@@ -49,13 +49,13 @@ const InitialStep = (props) => {
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.confirmPassword}
/>
/>
{
!props.install.isLoading ?
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
:
<Spinner />
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
:
<Spinner />
}
{props.install.installRequest === 'FAILURE' && <div className={styles.error}>Error: {props.install.installRequestError}</div>}
</form>
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
.step {
padding: 20px 0;
@@ -67,7 +67,7 @@ class Comment extends React.Component {
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const commentType = getCommentType(comment);
let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const queryData = {root, comment, asset: comment.asset};
@@ -91,8 +91,8 @@ class Comment extends React.Component {
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{currentUserId !== comment.user.id &&
<ActionsMenu icon="not_interested">
@@ -192,10 +192,10 @@ class Comment extends React.Component {
/>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
);
@@ -97,7 +97,7 @@ class ModerationQueue extends React.Component {
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
currentUserId={this.props.currentUserId}
/>;
/>;
})
}
</CSSTransitionGroup>
@@ -110,7 +110,7 @@ class ModerationQueue extends React.Component {
<LoadMore
loadMore={this.loadMore}
showLoadMore={comments.length < commentCount}
/>
/>
</div>
);
}
@@ -61,20 +61,20 @@ const StorySearch = (props) => {
{
loading
? <Spinner />
: assets.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
? <Spinner />
: assets.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
return <Story
key={i}
id={story.id}
title={story.title}
createdAt={new Date(story.created_at).toISOString()}
open={storyOpen}
author={story.author}
goToStory={props.goToStory}
/>;
})
return <Story
key={i}
id={story.id}
title={story.title}
createdAt={new Date(story.created_at).toISOString()}
open={storyOpen}
author={story.author}
goToStory={props.goToStory}
/>;
})
}
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
@custom-media --big-viewport (min-width: 780px);
.listContainer {
@@ -444,20 +449,20 @@ span {
.commentLeave {
opacity: 1.0;
transition: opacity 800ms;
}
.commentLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.commentEnter {
opacity: 0;
transition: opacity 800ms;
}
.commentEnterActive {
opacity: 1.0;
transition: opacity 800ms;
}
.editedMarker {
@@ -208,17 +208,22 @@ class ModerationContainer extends Component {
// Still loading.
return <Spinner />;
}
} else if(asset !== undefined || !('premodCount' in root)) {
} else if (asset !== undefined || !('premodCount' in root)) {
// loading.
return <Spinner />;
}
const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation);
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
isPremod(settings.moderation);
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
if (premodEnabled) {
if (premodEnabled && root.newCount === 0) {
delete currentQueueConfig.new;
} else {
}
if (!premodEnabled && root.premodCount === 0) {
delete currentQueueConfig.premod;
}
@@ -355,8 +360,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
variables: {
asset_id: id,
sortOrder: props.moderation.sortOrder,
allAssets: id === null
}
allAssets: id === null,
},
fetchPolicy: 'network-only'
};
},
});
@@ -98,14 +98,14 @@ export const withAssetSearchQuery = withQuery(gql`
}
}
`, {
options: ({moderation: {storySearchString = ''}}) => {
return {
variables: {
value: storySearchString
}
};
}
});
options: ({moderation: {storySearchString = ''}}) => {
return {
variables: {
value: storySearchString
}
};
}
});
export default compose(
withRouter,
@@ -69,15 +69,15 @@ function getCommentQueues(comment, queueConfig) {
const queues = [];
Object.keys(queueConfig).forEach((key) => {
const {action_type, statuses, tags} = queueConfig[key];
let addToQueues = false;
if (statuses && statuses.indexOf(comment.status) >= 0) {
addToQueues = true;
let addToQueues = true;
if (statuses && statuses.indexOf(comment.status) === -1) {
addToQueues = false;
}
if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) {
addToQueues = true;
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
addToQueues = false;
}
if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) {
addToQueues = true;
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
addToQueues = false;
}
if (addToQueues) {
queues.push(key);
@@ -1,16 +1,16 @@
import t from 'coral-framework/services/i18n';
export default {
new: {
statuses: ['NONE'],
icon: 'question_answer',
name: t('modqueue.new'),
},
premod: {
statuses: ['PREMOD'],
icon: 'access_time',
name: t('modqueue.premod'),
},
new: {
statuses: ['NONE', 'PREMOD'],
icon: 'question_answer',
name: t('modqueue.new'),
},
reported: {
action_type: 'FLAG',
statuses: ['NONE', 'PREMOD'],
@@ -134,20 +134,20 @@ export default class Stories extends Component {
<Radio value='closed'>{t('streams.closed')}</Radio>
</RadioGroup>
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}
className={styles.radioGroup}
>
<Radio value='desc'>{t('streams.newest')}</Radio>
<Radio value='asc'>{t('streams.oldest')}</Radio>
</RadioGroup>
</div>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}
className={styles.radioGroup}
>
<Radio value='desc'>{t('streams.newest')}</Radio>
<Radio value='asc'>{t('streams.oldest')}</Radio>
</RadioGroup>
</div>
{
assetsIds.length
? <div className={styles.mainContent}>
? <div className={styles.mainContent}>
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
<TableHeader name="title" cellFormatter={this.renderTitle}>{t('streams.article')}</TableHeader>
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
@@ -162,7 +162,7 @@ export default class Stories extends Component {
page={this.state.page}
onNewPageHandler={this.onPageClick} />
</div>
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
}
</div>
);
@@ -56,13 +56,13 @@ export default ({handleChange, handleApply, changed, ...props}) => (
title: t('configure.enable_questionbox'),
description: t('configure.enable_questionbox_description')
}} />
{
props.questionBoxEnable && <QuestionBoxBuilder
questionBoxIcon={props.questionBoxIcon}
questionBoxContent={props.questionBoxContent}
handleChange={handleChange}
/>
}
{
props.questionBoxEnable && <QuestionBoxBuilder
questionBoxIcon={props.questionBoxIcon}
questionBoxContent={props.questionBoxContent}
handleChange={handleChange}
/>
}
</li>
</ul>
</div>
@@ -41,39 +41,39 @@ class QuestionBoxBuilder extends React.Component {
<ul className={styles.qbItemIconList}>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'default'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="default" >
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'default'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="default" >
<DefaultIcon className={styles.defaultIcon} />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'forum'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="forum" >
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'forum'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="forum" >
<Icon name="forum" />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'build'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="build" >
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'build'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="build" >
<Icon name="build" />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="format_quote" >
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="format_quote" >
<Icon name="format_quote" />
</li>
</ul>
@@ -115,7 +115,7 @@ class ConfigureStreamContainer extends Component {
/>
<hr />
<h3>{closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}</h3>
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
<CloseCommentsInfo
onClick={this.toggleStatus}
status={closedAt}
@@ -1,7 +1,6 @@
import React from 'react';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import {TransitionGroup} from 'react-transition-group';
import {forEachError} from 'coral-framework/utils';
@@ -63,7 +62,7 @@ class AllCommentsPane extends React.Component {
}
if (
prevComments && nextComments &&
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
) {
@@ -126,7 +125,6 @@ class AllCommentsPane extends React.Component {
root,
comments,
commentClassNames,
ignoreUser,
setActiveReplyBox,
activeReplyBox,
notify,
@@ -139,7 +137,6 @@ class AllCommentsPane extends React.Component {
loadNewReplies,
deleteAction,
showSignInDialog,
commentIsIgnored,
charCountEnable,
maxCharCount,
editComment,
@@ -157,34 +154,32 @@ class AllCommentsPane extends React.Component {
/>
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
commentClassNames={commentClassNames}
data={data}
root={root}
disableReply={disableReply}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
depth={0}
postComment={postComment}
asset={asset}
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
comment={comment}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
editComment={editComment}
emit={emit}
/>;
return (
<Comment
commentClassNames={commentClassNames}
data={data}
root={root}
disableReply={disableReply}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
depth={0}
postComment={postComment}
asset={asset}
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
comment={comment}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
editComment={editComment}
emit={emit}
/>
);
})}
</TransitionGroup>
<LoadMore
@@ -66,17 +66,6 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.topRight .popover {
margin-top: 1em;
right: 0px;
}
.topRight .link.active,
.topRight .active .link {
padding-bottom: 0.125em;
border-bottom: 2px solid currentColor;
}
.editCommentForm {
margin-bottom: 10px;
}
@@ -175,4 +164,4 @@
.username {
margin-right: 5px;
}
}
@@ -16,7 +16,6 @@ import mapValues from 'lodash/mapValues';
import LoadMore from './LoadMore';
import {getEditableUntilDate} from './util';
import {findCommentWithId} from '../graphql/utils';
import {TopRightMenu} from './TopRightMenu';
import CommentContent from './CommentContent';
import Slot from 'coral-framework/components/Slot';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
@@ -95,8 +94,7 @@ export default class Comment extends React.Component {
this.onClickEdit = this.onClickEdit.bind(this);
this.stopEditing = this.stopEditing.bind(this);
this.state = {
// Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment)
isEditable: commentIsStillEditable(props.comment),
isEditing: false,
replyBoxVisible: false,
loadingState: '',
@@ -108,7 +106,7 @@ export default class Comment extends React.Component {
const {comment: {replies: prevReplies}} = this.props;
const {comment: {replies: nextReplies}} = next;
if (
prevReplies && nextReplies &&
prevReplies && nextReplies &&
nextReplies.nodes.length < prevReplies.nodes.length
) {
@@ -185,12 +183,6 @@ export default class Comment extends React.Component {
})
}).isRequired,
// given a comment, return whether it should be rendered as ignored
commentIsIgnored: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: PropTypes.func,
@@ -217,9 +209,18 @@ export default class Comment extends React.Component {
}
}
commentIsIgnored(comment) {
const me = this.props.root.me;
return (
me &&
me.ignoredUsers &&
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
}
hasIgnoredReplies() {
return this.props.comment.replies &&
this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply));
this.props.comment.replies.nodes.some((reply) => this.commentIsIgnored(reply));
}
loadNewReplies = () => {
@@ -295,14 +296,12 @@ export default class Comment extends React.Component {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
// if still in the edit window, set a timeout to re-render once it expires
const msLeftToEdit = editWindowRemainingMs(this.props.comment);
if (msLeftToEdit > 0) {
// if still in the edit window, set a timeout to handle expiration.
if (this.state.isEditable) {
const msLeftToEdit = editWindowRemainingMs(this.props.comment);
this.editWindowExpiryTimeout = setTimeout(() => {
// re-render
this.setState(this.state);
}, msLeftToEdit);
this.setState({isEditable: false});
}, Math.max(msLeftToEdit, 0));
}
}
componentWillUnmount() {
@@ -320,7 +319,6 @@ export default class Comment extends React.Component {
comment,
postFlag,
parentId,
ignoreUser,
highlighted,
postComment,
currentUser,
@@ -335,15 +333,18 @@ export default class Comment extends React.Component {
charCountEnable,
showSignInDialog,
liveUpdates,
commentIsIgnored,
animateEnter,
emit,
commentClassNames = []
} = this.props;
if (this.commentIsIgnored(comment)) {
return <IgnoredCommentTombstone />;
}
const view = this.getVisibileReplies();
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
const isActive = isCommentActive(comment.status);
const {loadingState} = this.state;
@@ -447,12 +448,20 @@ export default class Comment extends React.Component {
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
<Slot
className={cn('talk-stream-comment-author-tags')}
fill="commentAuthorTags"
queryData={queryData}
{...slotProps}
inline
/>
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
</span>
@@ -468,23 +477,13 @@ export default class Comment extends React.Component {
/* User can edit/delete their own comment for a short window after posting */
<span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
this.state.isEditable &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
}
{ isActive && (currentUser && (comment.user.id !== currentUser.id)) &&
/* TopRightMenu allows currentUser to ignore other users' comments */
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
notify={notify} />
</span>
}
{ !isActive &&
<InactiveCommentLabel status={comment.status}/>
}
@@ -492,7 +491,7 @@ export default class Comment extends React.Component {
<div className={styles.content}>
{
this.state.isEditing
? <EditableCommentContent
? <EditableCommentContent
editComment={this.editComment}
notify={notify}
comment={comment}
@@ -501,14 +500,14 @@ export default class Comment extends React.Component {
maxCharCount={maxCharCount}
parentId={parentId}
stopEditing={this.stopEditing}
/>
: <div>
<Slot
fill="commentContent"
defaultComponent={CommentContent}
{...slotProps}
queryData={queryData}
/>
: <div>
<Slot
fill="commentContent"
defaultComponent={CommentContent}
{...slotProps}
queryData={queryData}
/>
</div>
}
</div>
@@ -562,23 +561,22 @@ export default class Comment extends React.Component {
{activeReplyBox === comment.id
? <ReplyBox
commentPostedHandler={this.commentPostedHandler}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
setActiveReplyBox={setActiveReplyBox}
parentId={(depth < THREADING_LEVEL) ? comment.id : parentId}
notify={notify}
postComment={postComment}
currentUser={currentUser}
assetId={asset.id}
/>
commentPostedHandler={this.commentPostedHandler}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
setActiveReplyBox={setActiveReplyBox}
parentId={(depth < THREADING_LEVEL) ? comment.id : parentId}
notify={notify}
postComment={postComment}
currentUser={currentUser}
assetId={asset.id}
/>
: null}
<TransitionGroup>
{view.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <CommentContainer
{view.map((reply) => {
return (
<CommentContainer
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
@@ -595,18 +593,17 @@ export default class Comment extends React.Component {
postFlag={postFlag}
deleteAction={deleteAction}
loadMore={loadMore}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
showSignInDialog={showSignInDialog}
commentIsIgnored={commentIsIgnored}
liveUpdates={liveUpdates}
reactKey={reply.id}
key={reply.id}
comment={reply}
emit={emit}
/>;
})}
/>
);
})}
</TransitionGroup>
<div className="talk-load-more-replies">
<LoadMore
@@ -136,15 +136,15 @@ export class EditableCommentContent extends React.Component {
<span className={styles.editWindowRemaining}>
{
this.isEditWindowExpired()
? <span>
? <span>
{t('edit_comment.edit_window_expired')}
{
typeof this.props.stopEditing === 'function'
? <span>&nbsp;<a className={styles.link} onClick={this.props.stopEditing}>{t('edit_comment.edit_window_expired_close')}</a></span>
: null
? <span>&nbsp;<a className={styles.link} onClick={this.props.stopEditing}>{t('edit_comment.edit_window_expired_close')}</a></span>
: null
}
</span>
: <span>
: <span>
<Icon name="timer" className={styles.timerIcon}/> {t('edit_comment.edit_window_timer_prefix')}
<CountdownSeconds
until={this.getEditableUntil()}
@@ -7,12 +7,12 @@ const NewCount = ({count, loadMore}) => {
return <div className='talk-new-comments talk-load-more'>
{
count ?
<Button onClick={loadMore}>
{count === 1
? t('framework.new_count', count, t('framework.comment'))
: t('framework.new_count', count, t('framework.comments'))}
</Button>
: null
<Button onClick={loadMore}>
{count === 1
? t('framework.new_count', count, t('framework.comment'))
: t('framework.new_count', count, t('framework.comments'))}
</Button>
: null
}
</div>;
};
@@ -13,7 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n';
import CommentBox from 'talk-plugin-commentbox/CommentBox';
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import {isCommentActive} from 'coral-framework/utils';
import {Spinner, Button, Tab, TabCount, TabPane} from 'coral-ui';
import {Button, Tab, TabCount, TabPane} from 'coral-ui';
import cn from 'classnames';
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
@@ -23,8 +23,6 @@ import StreamTabPanel from '../containers/StreamTabPanel';
import styles from './Stream.css';
const SpinnerWhenLoading = ({loading, children}) => loading ? <Spinner /> : <div>{children}</div>;
class Stream extends React.Component {
constructor(props) {
@@ -42,15 +40,6 @@ class Stream extends React.Component {
}
}
commentIsIgnored = (comment) => {
const me = this.props.root.me;
return (
me &&
me.ignoredUsers &&
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
};
renderHighlightedComment() {
const {
data,
@@ -66,7 +55,6 @@ class Stream extends React.Component {
postDontAgree,
deleteAction,
showSignInDialog,
ignoreUser,
loadNewReplies,
auth: {user},
emit,
@@ -92,7 +80,6 @@ class Stream extends React.Component {
data={data}
root={root}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
@@ -108,7 +95,6 @@ class Stream extends React.Component {
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={topLevelComment.id}
commentIsIgnored={this.commentIsIgnored}
comment={topLevelComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
@@ -135,7 +121,6 @@ class Stream extends React.Component {
postDontAgree,
deleteAction,
showSignInDialog,
ignoreUser,
activeStreamTab,
setActiveStreamTab,
loadNewReplies,
@@ -144,6 +129,7 @@ class Stream extends React.Component {
emit,
sortOrder,
sortBy,
loading,
} = this.props;
const slotProps = {data};
@@ -171,6 +157,7 @@ class Stream extends React.Component {
tabPaneSlot={'streamTabPanes'}
slotProps={slotProps}
queryData={slotQueryData}
loading={loading}
appendTabs={
<Tab tabId={'all'} key='all'>
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
@@ -183,7 +170,6 @@ class Stream extends React.Component {
root={root}
comments={comments}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
@@ -197,7 +183,6 @@ class Stream extends React.Component {
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
commentIsIgnored={this.commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
@@ -223,7 +208,6 @@ class Stream extends React.Component {
viewAllComments,
auth: {loggedIn, user},
editName,
loading,
} = this.props;
const {keepCommentBox} = this.state;
const open = !asset.isClosed;
@@ -257,16 +241,16 @@ class Stream extends React.Component {
{open
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<QuestionBox
content={asset.settings.questionBoxContent}
enable={asset.settings.questionBoxEnable}
icon={asset.settings.questionBoxIcon}
/>
{!banned &&
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<QuestionBox
content={asset.settings.questionBoxContent}
enable={asset.settings.questionBoxEnable}
icon={asset.settings.questionBoxIcon}
/>
{!banned &&
temporarilySuspended &&
<RestrictedMessageBox>
{t(
@@ -275,13 +259,13 @@ class Stream extends React.Component {
timeago(user.suspension.until)
)}
</RestrictedMessageBox>}
{banned &&
{banned &&
<SuspendedAccount
canEditName={user && user.canEditName}
editName={editName}
currentUsername={user.username}
/>}
{showCommentBox &&
{showCommentBox &&
<CommentBox
notify={notify}
postComment={postComment}
@@ -294,7 +278,7 @@ class Stream extends React.Component {
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>}
</div>
</div>
: <p>{asset.settings.closedMessage}</p>}
<Slot
@@ -310,12 +294,10 @@ class Stream extends React.Component {
/>
)}
<SpinnerWhenLoading loading={loading}>
{highlightedComment
? this.renderHighlightedComment()
: this.renderTabPanel()
}
</SpinnerWhenLoading>
{highlightedComment
? this.renderHighlightedComment()
: this.renderTabPanel()
}
</div>
);
}
@@ -325,9 +307,6 @@ Stream.propTypes = {
notify: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: React.PropTypes.func
};
@@ -0,0 +1,3 @@
.spinnerContainer {
margin-top: 16px;
}
@@ -1,19 +1,23 @@
import React from 'react';
import {TabBar, TabContent} from 'coral-ui';
import {Spinner, TabBar, TabContent} from 'coral-ui';
import PropTypes from 'prop-types';
import styles from './StreamTabPanel.css';
class StreamTabPanel extends React.Component {
render() {
const {activeTab, setActiveTab, tabs, tabPanes, sub} = this.props;
const {activeTab, setActiveTab, tabs, tabPanes, sub, loading} = this.props;
return (
<div>
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
{tabs}
</TabBar>
<TabContent activeTab={activeTab} sub={sub}>
{tabPanes}
</TabContent>
{loading
? <div className={styles.spinnerContainer}><Spinner /></div>
: <TabContent activeTab={activeTab} sub={sub}>
{tabPanes}
</TabContent>
}
</div>
);
}
@@ -44,40 +44,40 @@ class SuspendedAccount extends Component {
return <RestrictedMessageBox>
<span>{
canEditName ?
canEditName ?
t('framework.edit_name.msg')
:
<span>
<b>{t('framework.banned_account_header')}</b><br/> {t('framework.banned_account_body')}
</span>
}</span>
}</span>
{
canEditName ?
<div>
<div className={styles.alert}>
{alert}
</div>
<label
htmlFor='username'
className="screen-reader-text"
aria-hidden={true}>
{t('framework.edit_name.label')}
</label>
<input
type='text'
className={styles.editNameInput}
value={username}
placeholder={t('framework.edit_name.label')}
id='username'
onChange={(e) => this.setState({username: e.target.value})}
rows={3}/><br/>
<Button
onClick={this.onSubmitClick}>
{
t('framework.edit_name.button')
}
</Button>
</div> : null
<div>
<div className={styles.alert}>
{alert}
</div>
<label
htmlFor='username'
className="screen-reader-text"
aria-hidden={true}>
{t('framework.edit_name.label')}
</label>
<input
type='text'
className={styles.editNameInput}
value={username}
placeholder={t('framework.edit_name.label')}
id='username'
onChange={(e) => this.setState({username: e.target.value})}
rows={3}/><br/>
<Button
onClick={this.onSubmitClick}>
{
t('framework.edit_name.button')
}
</Button>
</div> : null
}
</RestrictedMessageBox>;
}
@@ -1,60 +0,0 @@
import React, {PropTypes} from 'react';
import {IgnoreUserWizard} from './IgnoreUserWizard';
import Toggleable from './Toggleable';
// TopRightMenu appears as a dropdown in the top right of the comment.
// when you click the down cehvron, it expands and shows IgnoreUserWizard
// when you click 'cancel' in the wizard, it closes the menu
export class TopRightMenu extends React.Component {
static propTypes = {
// comment on which this menu appears
comment: PropTypes.shape({
user: PropTypes.shape({
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired
}).isRequired
}).isRequired,
ignoreUser: PropTypes.func,
// show notification to the user (e.g. for errors)
notify: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
timesReset: 0
};
}
render() {
const {comment, ignoreUser, notify} = this.props;
// timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable)
const reset = () => this.setState({timesReset: this.state.timesReset + 1});
const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => {
// close menu
reset();
// ignore user
try {
await ignoreUser({id});
} catch (error) {
notify('error', 'Failed to ignore user');
throw error;
}
};
return (
<Toggleable key={this.state.timesReset} className="talk-stream-comment-chevron">
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
<IgnoreUserWizard
user={comment.user}
cancel={reset}
ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError}
/>
</div>
</Toggleable>
);
}
}
@@ -16,7 +16,8 @@ const slots = [
'commentContent',
'commentReactions',
'commentAvatar',
'commentAuthorName'
'commentAuthorName',
'commentAuthorTags'
];
/**
@@ -84,13 +85,24 @@ const singleCommentFragment = gql`
edited
editableUntil
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`;
const withCommentFragments = withFragments({
root: gql`
fragment CoralEmbedStream_Comment_root on RootQuery {
me {
ignoredUsers {
id
}
}
__typename
me {
ignoredUsers {
id
}
}
${getSlotFragmentSpreads(slots, 'root')}
}
`,
@@ -115,7 +127,6 @@ const withCommentFragments = withFragments({
endCursor
}
`, THREADING_LEVEL)}
${getSlotFragmentSpreads(slots, 'comment')}
}
${singleCommentFragment}
`
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree,
withDeleteAction, withIgnoreUser, withEditComment
withDeleteAction, withEditComment
} from 'coral-framework/graphql/mutations';
import * as authActions from 'coral-embed-stream/src/actions/auth';
@@ -84,6 +84,10 @@ class StreamContainer extends React.Component {
return prev;
}
// Newest top-level comments are only added when sorting by 'newest first'.
if (!commentAdded.parent && !this.isSortedByNewestFirst()) {
return prev;
}
return insertCommentIntoEmbedQuery(prev, commentAdded);
},
});
@@ -163,32 +167,9 @@ class StreamContainer extends React.Component {
}
componentWillReceiveProps(nextProps) {
const prevSortedNewest = this.isSortedByNewestFirst(this.props);
const nextSortedNewest = this.isSortedByNewestFirst(nextProps);
// When switching to 'Newest first' we refetch and subscribe so that
// we always have the newest comments.
if (!prevSortedNewest && nextSortedNewest) {
if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) {
nextProps.data.refetch();
this.subscribeToCommentsAdded();
}
// When switching away from 'Newest first' unsubscribe from newest comments.
if (prevSortedNewest && !nextSortedNewest) {
this.unsubscribeCommentsAdded();
}
}
shouldComponentUpdate(nextProps) {
const prevSortedNewest = this.isSortedByNewestFirst(this.props);
const nextSortedNewest = this.isSortedByNewestFirst(nextProps);
if (!prevSortedNewest && nextSortedNewest) {
// When switching to 'Newest first' we refetch => skip
// rendering this frame and wait for refetch to kick in.
return false;
}
return true;
}
userIsDegraged({auth: {user}} = this.props) {
@@ -393,7 +374,6 @@ export default compose(
withPostComment,
withPostFlag,
withPostDontAgree,
withIgnoreUser,
withDeleteAction,
withEditComment,
)(StreamContainer);
@@ -86,6 +86,7 @@ class StreamTabPanelContainer extends React.Component {
setActiveTab={this.props.setActiveTab}
tabs={this.getPluginTabElements().concat(this.props.appendTabs)}
tabPanes={this.getPluginTabPaneElements().concat(this.props.appendTabPanes)}
loading={this.props.loading}
sub={this.props.sub}
/>
);
@@ -110,6 +111,7 @@ StreamTabPanelContainer.propTypes = {
queryData: PropTypes.object,
className: PropTypes.string,
sub: PropTypes.bool,
loading: PropTypes.bool,
};
const mapStateToProps = (state) => ({
@@ -108,33 +108,6 @@ export default {
`,
},
mutations: {
IgnoreUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Embed: (previousData) => {
const ignoredUserId = variables.id;
const updated = update(previousData, {me: {ignoredUsers: {$push: [{
id: ignoredUserId,
__typename: 'User',
}]}}});
return updated;
}
}
}),
StopIgnoringUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Profile: (previousData) => {
const noLongerIgnoredUserId = variables.id;
// remove noLongerIgnoredUserId from ignoredUsers
const updated = update(previousData, {me: {ignoredUsers: {
$apply: (ignoredUsers) => {
return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId);
}
}}});
return updated;
}
}
}),
PostComment: ({
variables: {comment: {asset_id, body, parent_id, tags = []}},
state: {auth},
@@ -60,7 +60,7 @@ function findAndInsertComment(parent, comment) {
[connectionField]: {
nodes: {
$apply: (nodes) =>
nodes.map((node) => findAndInsertComment(node, comment))
nodes.map((node) => findAndInsertComment(node, comment))
},
},
});
@@ -176,7 +176,7 @@ function findAndInsertFetchedComments(parent, comments, parent_id) {
[connectionField]: {
nodes: {
$apply: (nodes) =>
nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id))
nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id))
},
},
});
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as default styles should be included in the components.
* Please remove this file when styles have been refactored.
*/
* {
font-family: inherit;
font-style: inherit;
+1 -1
View File
@@ -38,7 +38,7 @@ export default class Snackbar {
}
alert(message) {
const [type, text] = message.split('|');
const {type, text} = JSON.parse(message);
this.el.style.transform = 'translate(-50%, 20px)';
this.el.style.opacity = 0;
this.el.className = `coral-notif-${type}`;
@@ -1,4 +1,4 @@
import React from 'react';
import React, {Children} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import omit from 'lodash/omit';
@@ -28,18 +28,13 @@ class IfSlotIsEmpty extends React.Component {
}
render() {
const {className, component: Component = 'div', children} = this.props;
return (
<Component className={className}>
{this.isSlotEmpty() ? children : null}
</Component>
);
const {children} = this.props;
return this.isSlotEmpty() ? Children.only(children) : null;
}
}
IfSlotIsEmpty.propTypes = {
slot: PropTypes.string,
className: PropTypes.string,
};
const mapStateToProps = (state) => ({
@@ -1,4 +1,4 @@
import React from 'react';
import React, {Children} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import omit from 'lodash/omit';
@@ -28,18 +28,13 @@ class IfSlotIsNotEmpty extends React.Component {
}
render() {
const {className, component: Component = 'div', children} = this.props;
return (
<Component className={className}>
{this.isSlotEmpty() ? null : children}
</Component>
);
const {children} = this.props;
return this.isSlotEmpty() ? null : Children.only(children);
}
}
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.string,
className: PropTypes.string,
};
const mapStateToProps = (state) => ({
+13 -2
View File
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
export default class Popup extends Component {
ref = null;
detectCloseInterval = null;
resetCallbackInterval = null;
constructor(props) {
super(props);
@@ -41,16 +42,26 @@ export default class Popup extends Component {
this.ref.onunload = () => {
this.onUnload();
const interval = setInterval(() => {
if (this.resetCallbackInterval) {
clearInterval(this.resetCallbackInterval);
}
this.resetCallbackInterval = setInterval(() => {
if (this.ref && this.ref.onload === null) {
clearInterval(this.resetCallbackInterval);
this.resetCallbackInterval = null;
this.setCallbacks();
clearInterval(interval);
}
}, 50);
if (this.detectCloseInterval) {
clearInterval(this.detectCloseInterval);
}
this.detectCloseInterval = setInterval(() => {
if (!this.ref || this.ref.closed) {
clearInterval(this.detectCloseInterval);
this.detectCloseInterval = null;
this.onClose();
}
}, 50);
+1 -1
View File
@@ -293,7 +293,7 @@ export const withIgnoreUser = withMutation(
}
`, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
ignoreUser: (id) => {
return mutate({
variables: {
id,
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
email: (email) => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
email: (email) => (/^.+@.+\..+$/.test(email)),
password: (pass) => (/^(?=.{8,}).*$/.test(pass)),
confirmPassword: () => true,
username: (username) => (/^[a-zA-Z0-9_]+$/.test(username)),
+22 -22
View File
@@ -84,8 +84,8 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
const updateCallbacks =
[base.update || config.options.update]
.concat(...configs.map((cfg) => cfg.update))
.filter((i) => i);
.concat(...configs.map((cfg) => cfg.update))
.filter((i) => i);
const update = (proxy, result) => {
if (getResponseErrors(result)) {
@@ -101,28 +101,28 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
base.updateQueries || config.options.updateQueries,
...configs.map((cfg) => cfg.updateQueries)
]
.filter((i) => i)
.reduce((res, map) => {
Object.keys(map).forEach((key) => {
if (!(key in res)) {
res[key] = (prev, result) => {
if (getResponseErrors(result.mutationResult)) {
.filter((i) => i)
.reduce((res, map) => {
Object.keys(map).forEach((key) => {
if (!(key in res)) {
res[key] = (prev, result) => {
if (getResponseErrors(result.mutationResult)) {
// Do not run updates when we have mutation errors.
return prev;
}
return map[key](prev, result) || prev;
};
} else {
const existing = res[key];
res[key] = (prev, result) => {
const next = existing(prev, result);
return map[key](next, result) || next;
};
}
});
return res;
}, {});
return prev;
}
return map[key](prev, result) || prev;
};
} else {
const existing = res[key];
res[key] = (prev, result) => {
const next = existing(prev, result);
return map[key](next, result) || next;
};
}
});
return res;
}, {});
const wrappedConfig = {
variables,
+17 -17
View File
@@ -69,7 +69,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
const status = networkStatusToString(networkStatus);
const {root} = separateDataAndRoot(data);
this.context.eventEmitter.emit(`query.${name}.${status}`, {variables, data: root});
this.context.eventEmitter.emit(`query.${this.name}.${status}`, {variables, data: root});
}
nextData(data) {
@@ -121,7 +121,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
const resolvedDocument = this.resolveDocument(lmArgs.query);
const fetchName = getDefinitionName(resolvedDocument);
this.context.eventEmitter.emit(
`query.${name}.fetchMore.${fetchName}.begin`,
`query.${this.name}.fetchMore.${fetchName}.begin`,
{variables: lmArgs.variables});
// Resolve document fragments before passing it to `apollo-client`.
@@ -129,18 +129,18 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
...lmArgs,
query: resolvedDocument,
})
.then((res) => {
this.context.eventEmitter.emit(
`query.${name}.fetchMore.${fetchName}.success`,
{variables: lmArgs.variables, data: res.data});
return Promise.resolve(res);
})
.catch((err) => {
this.context.eventEmitter.emit(
`query.${name}.fetchMore.${fetchName}.error`,
{variables: lmArgs.variables, error: err});
throw err;
});
.then((res) => {
this.context.eventEmitter.emit(
`query.${this.name}.fetchMore.${fetchName}.success`,
{variables: lmArgs.variables, data: res.data});
return Promise.resolve(res);
})
.catch((err) => {
this.context.eventEmitter.emit(
`query.${this.name}.fetchMore.${fetchName}.error`,
{variables: lmArgs.variables, error: err});
throw err;
});
},
};
}
@@ -168,11 +168,11 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
const base = (typeof this.wrappedConfig.options === 'function')
? this.wrappedConfig.options(data)
: this.wrappedConfig.options;
const configs = this.graphqlRegistry.getQueryOptions(name);
const configs = this.graphqlRegistry.getQueryOptions(this.name);
const reducerCallbacks =
[base.reducer || ((i) => i)]
.concat(...configs.map((cfg) => cfg.reducer))
.filter((i) => i);
.concat(...configs.map((cfg) => cfg.reducer))
.filter((i) => i);
const reducer = withSkipOnErrors(
reducerCallbacks.reduce(
@@ -5,14 +5,14 @@
*/
export function createNotificationService(pym) {
return {
success(msg) {
pym.sendMessage('coral-alert', `success|${msg}`);
success(text) {
pym.sendMessage('coral-alert', JSON.stringify({kind: 'success', text}));
},
error(msg) {
pym.sendMessage('coral-alert', `error|${msg}`);
error(text) {
pym.sendMessage('coral-alert', JSON.stringify({kind: 'error', text}));
},
info(msg) {
pym.sendMessage('coral-alert', `info|${msg}`);
info(text) {
pym.sendMessage('coral-alert', JSON.stringify({kind: 'info', text}));
},
};
}
+6 -6
View File
@@ -81,11 +81,11 @@ class PluginsService {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
return flatten(this.plugins
// Filter out components that have slots and have been disabled in `plugin_config`
.filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components))
// Filter out components that have slots and have been disabled in `plugin_config`
.filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components))
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot])
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot])
)
.filter((component) => {
if(!component.isExcluded) {
@@ -114,8 +114,8 @@ class PluginsService {
config: pluginConfig,
...(
component.fragments
? pick(queryData, Object.keys(component.fragments))
: withWarnings(component, queryData)
? pick(queryData, Object.keys(component.fragments))
: withWarnings(component, queryData)
)
};
}
+14 -1
View File
@@ -26,7 +26,7 @@ export const getMyActionSummary = (type, comment) => {
.find((a) => a.current_user);
};
/**
/**
* getActionSummary
* retrieves the action summaries based on the type and the comment
* array could be length > 1, as in the case of FlagActionSummary
@@ -184,3 +184,16 @@ export function getShallowChanges(a, b) {
return union(Object.keys(a), Object.keys(b))
.filter((key) => a[key] !== b[key]);
}
// TODO: replace with something less fragile.
// NOT_REACTION_TYPES are the action summaries that are not reactions.
const NOT_REACTION_TYPES = [
'FlagActionSummary',
'DontAgreeActionSummary',
];
export function getTotalReactionsCount(actionSummaries) {
return actionSummaries
.filter(({__typename}) => !NOT_REACTION_TYPES.includes(__typename))
.reduce((total, {count}) => total + count, 0);
}
+1 -1
View File
@@ -1,4 +1,4 @@
/**
/**
* getReliability
* retrieves reliability value as string
*/
@@ -1,24 +0,0 @@
.ignoredUser {
display: table-row;
}
.ignoredUserList {
display: table;
}
.ignoredUser > * {
display: table-cell;
}
.stopListening {
color: #D0011B;
}
.link {
text-decoration: underline;
cursor: pointer;
}
.stopListening:before {
content: '\00a0\00a0\00a0\00a0';
}
@@ -1,43 +0,0 @@
import React, {Component, PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
import styles from './IgnoredUsers.css';
export class IgnoredUsers extends Component {
static propTypes = {
users: PropTypes.arrayOf(PropTypes.shape({
username: PropTypes.string,
id: PropTypes.string,
})).isRequired,
// accepts { id }
stopIgnoring: PropTypes.func.isRequired,
}
render() {
const {users, stopIgnoring} = this.props;
return (
<div>
{
users.length
? <p>{t('framework.because_you_ignored')}</p>
: null
}
<dl className={styles.ignoredUserList}>
{
users.map(({username, id}) => (
<span className={styles.ignoredUser} key={id}>
<dt key={id}>{ username }</dt>
<dd className={styles.stopListening}>
<a
onClick={() => stopIgnoring({id})}
className={styles.link}>{t('framework.stop_ignoring')}</a>
</dd>
</span>
))
}
</dl>
</div>
);
}
}
export default IgnoredUsers;
@@ -3,12 +3,10 @@ import {compose, gql} from 'react-apollo';
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {withQuery} from 'coral-framework/hocs';
import {withStopIgnoringUser} from 'coral-framework/graphql/mutations';
import Slot from 'coral-framework/components/Slot';
import {link} from 'coral-framework/services/pym';
import NotLoggedIn from '../components/NotLoggedIn';
import IgnoredUsers from '../components/IgnoredUsers';
import {Spinner} from 'coral-ui';
import CommentHistory from 'talk-plugin-history/CommentHistory';
@@ -55,7 +53,7 @@ class ProfileContainer extends Component {
};
render() {
const {auth, auth: {user}, showSignInDialog, stopIgnoringUser, root, data} = this.props;
const {auth, auth: {user}, showSignInDialog, root, data} = this.props;
const {me} = this.props.root;
const loading = this.props.data.loading;
@@ -77,15 +75,11 @@ class ProfileContainer extends Component {
<h2>{user.username}</h2>
{emailAddress ? <p>{emailAddress}</p> : null}
{me.ignoredUsers && me.ignoredUsers.length
? <div>
<h3>{t('framework.ignored_users')}</h3>
<IgnoredUsers
users={me.ignoredUsers}
stopIgnoring={stopIgnoringUser}
/>
</div>
: null}
<Slot
fill="profileSections"
data={data}
queryData={{root}}
/>
<hr />
@@ -98,8 +92,10 @@ class ProfileContainer extends Component {
}
}
// TODO: This Slot should be included in `talk-plugin-history` instead.
const slots = [
'profileSections',
// TODO: This Slot should be included in `talk-plugin-history` instead.
'commentContent',
];
@@ -108,6 +104,11 @@ const CommentFragment = gql`
nodes {
id
body
replyCount
action_summaries {
count
__typename
}
asset {
id
title
@@ -138,10 +139,6 @@ const withProfileQuery = withQuery(
query CoralEmbedStream_Profile {
me {
id
ignoredUsers {
id,
username,
}
comments(query: {limit: 10}) {
...TalkSettings_CommentConnectionFragment
}
@@ -160,6 +157,5 @@ const mapDispatchToProps = (dispatch) =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withStopIgnoringUser,
withProfileQuery
)(ProfileContainer);
+26 -26
View File
@@ -3,34 +3,34 @@ import styles from './CoralLogo.css';
const CoralLogo = ({className = ''}) => (
<svg className={`${styles.base} ${className}`} viewBox='0 0 381 391' version='1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink'>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g id='Wordmark-Round' className={styles.mark} transform='translate(-1833.000000, -411.000000)' strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
<g id='coralProjectLogo-2-Copy-2' transform='translate(1842.000000, 421.000000)'>
<g id='Layer_2' transform='translate(2.268750, 1.133903)'>
<rect id='Rectangle-1' fill='#F47E6B' x='0' y='0' width='358.4625' height='368.518519' rx='40'>
</rect>
<path d='M0.226875,105.679772 C13.7259375,122.688319 41.29125,131.986325 56.71875,116.792023 C67.0415625,106.586895 62.5040625,93.0934473 76.910625,63.3851852 C83.94375,48.7578348 90.523125,38.3259259 101.980313,37.5321937 C115.139063,36.6250712 131.814375,50.3452991 134.31,69.3948718 C137.826563,95.1344729 115.479375,105.339601 121.15125,123.822222 C127.390313,144.119088 157.110938,140.377208 166.52625,165.096296 C170.042813,174.394302 171.630938,190.382336 163.463438,198.319658 C149.170313,212.266667 120.584063,186.073504 80.7675,198.319658 C73.280625,200.587464 56.3784375,205.803419 51.500625,219.523647 C46.73625,233.130484 55.584375,251.046154 67.60875,260.797721 C93.245625,281.661538 119.79,254.674644 159.379688,271.909972 C181.840313,281.661538 203.053125,303.319088 208.725,330.305983 C211.674375,344.593162 209.6325,356.952707 207.704063,364.549858' id='Shape'>
</path>
</g>
<g id='Layer_3' transform='translate(43.106250, 289.145299)'>
<path d='M90.4096875,72.5698006 C78.8390625,41.3874644 64.9996875,31.4091168 54.1096875,28.234188 C41.8584375,24.7190883 30.7415625,29.0279202 17.8096875,21.2039886 C8.394375,15.4210826 3.403125,6.57663818 0.680625,0'id='Shape'></path>
</g><g id='Layer_4' transform='translate(220.068750, 209.772080)'>
<path d='M81.7884375,152.963533 C74.9821875,122.007977 61.8234375,104.77265 50.593125,94.5675214 C31.8759375,77.6723647 14.746875,77.1054131 5.218125,57.2621083 C4.4240625,55.5612536 -5.7853125,33.4501425 5.218125,17.008547 C14.52,3.06153846 33.350625,1.47407407 41.518125,0.907122507 C64.3190625,-0.907122507 73.280625,9.52478632 100.959375,13.039886 C117.180938,15.0809117 130.793438,13.4934473 139.30125,12.0193732'id='Shape'></path>
</g>
<g id='Layer_5' transform='translate(285.862500, 289.145299)'>
<path d='M74.415,2.04102564 C66.3609375,0.793732194 51.046875,-0.566951567 33.12375,5.1025641 C17.5828125,9.97834758 6.80625,18.0290598 0.9075,23.2450142'id='Shape'></path>
</g>
<g id='Layer_6' transform='translate(174.693750, 1.133903)'>
<path d='M184.109063,151.035897 C170.950313,174.507692 158.699063,180.290598 149.850938,181.311111 C133.85625,183.011966 129.091875,168.724786 104.475938,163.168661 C79.2928125,157.499145 72.2596875,169.745299 52.0678125,163.168661 C30.0609375,155.911681 18.7171875,134.821083 12.705,123.822222 C-1.588125,97.4022792 -0.3403125,71.6626781 0.5671875,51.2524217 C1.588125,29.4814815 6.125625,12.0193732 9.6421875,0.907122507'id='Shape'></path>
<path d='M183.541875,69.3948718 C170.496563,56.6951567 157.564688,45.8096866 149.28375,39.0062678 C143.385,34.1304843 134.990625,33.2233618 128.297813,36.8518519 C128.070938,36.9652422 127.844063,37.0786325 127.730625,37.1920228 C122.739375,40.1401709 119.449688,45.3561254 118.8825,51.1390313 C118.201875,59.0763533 117.0675,71.4358974 114.912188,82.8883191 C114.118125,87.0837607 107.085,103.865527 89.7290625,110.668946 C77.2509375,115.544729 62.0503125,110.668946 53.4290625,101.597721 C40.38375,87.7641026 44.8078125,66.9002849 47.416875,55.2210826 C53.5425,26.760114 73.3940625,9.07122507 82.6959375,1.81424501'id='Shape'></path>
</g>
<g id='Layer_7' transform='translate(3.403125, 179.156695)'>
<path d='M0.1134375,0.226780627 C2.949375,6.34985755 8.394375,16.1014245 18.2634375,25.3994302 C27.67875,34.2438746 37.3209375,39.0062678 43.4465625,41.5008547'id='Shape'></path>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g id='Wordmark-Round' className={styles.mark} transform='translate(-1833.000000, -411.000000)' strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
<g id='coralProjectLogo-2-Copy-2' transform='translate(1842.000000, 421.000000)'>
<g id='Layer_2' transform='translate(2.268750, 1.133903)'>
<rect id='Rectangle-1' fill='#F47E6B' x='0' y='0' width='358.4625' height='368.518519' rx='40'>
</rect>
<path d='M0.226875,105.679772 C13.7259375,122.688319 41.29125,131.986325 56.71875,116.792023 C67.0415625,106.586895 62.5040625,93.0934473 76.910625,63.3851852 C83.94375,48.7578348 90.523125,38.3259259 101.980313,37.5321937 C115.139063,36.6250712 131.814375,50.3452991 134.31,69.3948718 C137.826563,95.1344729 115.479375,105.339601 121.15125,123.822222 C127.390313,144.119088 157.110938,140.377208 166.52625,165.096296 C170.042813,174.394302 171.630938,190.382336 163.463438,198.319658 C149.170313,212.266667 120.584063,186.073504 80.7675,198.319658 C73.280625,200.587464 56.3784375,205.803419 51.500625,219.523647 C46.73625,233.130484 55.584375,251.046154 67.60875,260.797721 C93.245625,281.661538 119.79,254.674644 159.379688,271.909972 C181.840313,281.661538 203.053125,303.319088 208.725,330.305983 C211.674375,344.593162 209.6325,356.952707 207.704063,364.549858' id='Shape'>
</path>
</g>
<g id='Layer_3' transform='translate(43.106250, 289.145299)'>
<path d='M90.4096875,72.5698006 C78.8390625,41.3874644 64.9996875,31.4091168 54.1096875,28.234188 C41.8584375,24.7190883 30.7415625,29.0279202 17.8096875,21.2039886 C8.394375,15.4210826 3.403125,6.57663818 0.680625,0'id='Shape'></path>
</g><g id='Layer_4' transform='translate(220.068750, 209.772080)'>
<path d='M81.7884375,152.963533 C74.9821875,122.007977 61.8234375,104.77265 50.593125,94.5675214 C31.8759375,77.6723647 14.746875,77.1054131 5.218125,57.2621083 C4.4240625,55.5612536 -5.7853125,33.4501425 5.218125,17.008547 C14.52,3.06153846 33.350625,1.47407407 41.518125,0.907122507 C64.3190625,-0.907122507 73.280625,9.52478632 100.959375,13.039886 C117.180938,15.0809117 130.793438,13.4934473 139.30125,12.0193732'id='Shape'></path>
</g>
<g id='Layer_5' transform='translate(285.862500, 289.145299)'>
<path d='M74.415,2.04102564 C66.3609375,0.793732194 51.046875,-0.566951567 33.12375,5.1025641 C17.5828125,9.97834758 6.80625,18.0290598 0.9075,23.2450142'id='Shape'></path>
</g>
<g id='Layer_6' transform='translate(174.693750, 1.133903)'>
<path d='M184.109063,151.035897 C170.950313,174.507692 158.699063,180.290598 149.850938,181.311111 C133.85625,183.011966 129.091875,168.724786 104.475938,163.168661 C79.2928125,157.499145 72.2596875,169.745299 52.0678125,163.168661 C30.0609375,155.911681 18.7171875,134.821083 12.705,123.822222 C-1.588125,97.4022792 -0.3403125,71.6626781 0.5671875,51.2524217 C1.588125,29.4814815 6.125625,12.0193732 9.6421875,0.907122507'id='Shape'></path>
<path d='M183.541875,69.3948718 C170.496563,56.6951567 157.564688,45.8096866 149.28375,39.0062678 C143.385,34.1304843 134.990625,33.2233618 128.297813,36.8518519 C128.070938,36.9652422 127.844063,37.0786325 127.730625,37.1920228 C122.739375,40.1401709 119.449688,45.3561254 118.8825,51.1390313 C118.201875,59.0763533 117.0675,71.4358974 114.912188,82.8883191 C114.118125,87.0837607 107.085,103.865527 89.7290625,110.668946 C77.2509375,115.544729 62.0503125,110.668946 53.4290625,101.597721 C40.38375,87.7641026 44.8078125,66.9002849 47.416875,55.2210826 C53.5425,26.760114 73.3940625,9.07122507 82.6959375,1.81424501'id='Shape'></path>
</g>
<g id='Layer_7' transform='translate(3.403125, 179.156695)'>
<path d='M0.1134375,0.226780627 C2.949375,6.34985755 8.394375,16.1014245 18.2634375,25.3994302 C27.67875,34.2438746 37.3209375,39.0062678 43.4465625,41.5008547'id='Shape'></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</svg>
);
CoralLogo.propTypes = {
+1 -1
View File
@@ -56,7 +56,7 @@ export default class Dialog extends Component {
className={`mdl-dialog ${className}`}
{...rest}
>
{children}
{children}
</dialog>
);
}
+7 -7
View File
@@ -3,7 +3,7 @@ import styles from './Pager.css';
const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) =>
<li className={curr === i ? styles.current : ''}
key={i} onClick={() => onClickHandler(i + 1)}>
key={i} onClick={() => onClickHandler(i + 1)}>
{i + 1}
</li>
);
@@ -17,18 +17,18 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
onClick={() => onNewPageHandler(page - 1)}>
Prev
</li>
:
:
null
}
{Rows(page, totalPages, onNewPageHandler)}
{
(page < totalPages && totalPages > 1) ?
<li
onClick={() => onNewPageHandler(page + 1)}>
<li
onClick={() => onNewPageHandler(page + 1)}>
Next
</li>
:
null
</li>
:
null
}
</ul>
</div>

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