Compare commits

...
576 Commits
Author SHA1 Message Date
Kim Gardner 59c1656da3 Merge pull request #457 from coralproject/update-readme
Update readme
2017-03-31 16:06:55 -04:00
Kim Gardner f74d6071df update readme 2017-03-31 16:01:54 -04:00
Kim Gardner b93f1e2a7d Merge pull request #446 from coralproject/change-permalink-behavior
Change permalink behavior
2017-03-31 15:57:32 -04:00
Riley Davis 8196cb9c24 load all the replies in one go 2017-03-31 13:37:28 -06:00
riley ba4f39aecd Merge branch 'master' into change-permalink-behavior 2017-03-31 10:10:34 -06:00
Riley Davis ca4c79681b Merge pull request #456 from coralproject/prevent-collapse-from-css
prevent Talk from collapsing from custom css
2017-03-31 09:36:04 -06:00
Riley Davis 093c13c103 add polling to permalink page 2017-03-31 09:16:21 -06:00
Riley Davis 72c1774cf3 prevent Talk from collapsing when a height is set to the body on the parent page 2017-03-31 09:12:09 -06:00
Riley Davis a1bb0cd55f Merge branch 'master' into change-permalink-behavior 2017-03-30 12:59:06 -06:00
Riley Davis 87ff9306de Merge remote-tracking branch 'origin/change-permalink-behavior' into change-permalink-behavior 2017-03-30 10:51:16 -06:00
Riley Davis fd968b2070 position permalink popup given different comment scenarios 2017-03-30 10:50:58 -06:00
Kiwi b9f0633365 Merge pull request #454 from coralproject/markdown-infobox
Markdown InfoBox
2017-03-30 23:10:14 +07:00
Belén Curcio 0ec582e411 Merge branch 'master' into markdown-infobox 2017-03-30 13:04:07 -03:00
Chi Vinh Le 4c79f45556 Support Markdown in Infobox 2017-03-30 22:19:02 +07:00
Riley Davis f7f0a9d99e Merge pull request #453 from coralproject/upgrade-react
Upgrade React
2017-03-30 08:31:32 -06:00
David Erwin a22d7af8bb Merge branch 'master' into change-permalink-behavior 2017-03-30 10:20:58 -04:00
Chi Vinh Le 4d89db931e Integrate MarkdownEditor 2017-03-30 20:49:18 +07:00
Chi Vinh Le 2d0208046b Implement MarkdownEditor 2017-03-30 20:49:18 +07:00
Chi Vinh Le e878d1ebfd Upgrade React 2017-03-30 17:13:02 +07:00
Riley Davis f37c768662 only highlight the actual comment permalinked 2017-03-29 15:21:41 -06:00
Riley Davis 3007ba68f8 load more replies on a permalinked comment 2017-03-29 15:12:33 -06:00
David Erwin 1e80991614 Merge pull request #445 from coralproject/dynamic-email-templates
hot-load email templates during development
2017-03-29 15:38:42 -04:00
Wyatt Johnson ebcb3b4b02 Merge branch 'master' into dynamic-email-templates 2017-03-29 13:09:31 -06:00
Riley Davis f11a11cf67 show view more button on a permalinked comment if applicable 2017-03-29 13:04:28 -06:00
Kim Gardner c5d36a9f19 Merge branch 'master' into change-permalink-behavior 2017-03-29 14:49:21 -04:00
Kim Gardner f80f253b98 Merge pull request #452 from coralproject/bump-version-1-4-0
Bump version to 1.4.0
2017-03-29 14:49:02 -04:00
Kim Gardner 71d2baa010 Bump version to 1.4.0 2017-03-29 14:43:19 -04:00
Kim Gardner 55973859ef Merge branch 'master' into change-permalink-behavior 2017-03-29 14:29:16 -04:00
Kim Gardner 6548a76bfb Merge pull request #451 from coralproject/username-fix
Fixes issues with wordlist and usernames
2017-03-29 14:28:57 -04:00
riley 009b381744 Merge branch 'master' into dynamic-email-templates 2017-03-29 12:28:14 -06:00
riley 3e00a4e842 Merge branch 'master' into change-permalink-behavior 2017-03-29 12:27:52 -06:00
Kim Gardner 57c1e58bc3 Merge branch 'master' into username-fix 2017-03-29 14:10:24 -04:00
Kim Gardner 0726603a4e Merge pull request #447 from coralproject/total-counts
show the total comment count instead of the parent count
2017-03-29 14:10:05 -04:00
Kim Gardner 9d59c95cdf Merge branch 'master' into total-counts 2017-03-29 13:55:21 -04:00
Kiwi 771f99cee8 Merge pull request #450 from coralproject/rm_duplicate_css
Remove duplicate material.css
2017-03-30 00:52:28 +07:00
Chi Vinh Le 00a74f05cb Use standard mdl theme 2017-03-30 00:44:26 +07:00
Wyatt Johnson 8680d6cab8 Fixes issues with wordlist and usernames
- Ensure that the wordlist can't contain emoji's
- Ensure that username checks user the correct tokenizer
2017-03-29 10:57:12 -06:00
Riley Davis 184b6c00a6 Merge branch 'master' into total-counts 2017-03-29 10:06:41 -06:00
Riley Davis 870fcac3b1 Merge branch 'master' into dynamic-email-templates 2017-03-29 09:31:24 -06:00
Chi Vinh Le d80ed467f1 Remove duplicate material.css 2017-03-29 21:41:22 +07:00
Kim Gardner 8435358879 Merge pull request #448 from pablocubico/patch-1
Found some typos on the spanish side :)
2017-03-29 07:53:24 -04:00
Pablo Cúbico 5092b849b3 Found some typos on the spanish side :) 2017-03-28 23:22:12 -03:00
Riley Davis daa6ce60ab show the total comment count instead of the parent count 2017-03-28 15:41:27 -06:00
Riley Davis b7ce669cfc add translations 2017-03-28 14:46:01 -06:00
Riley Davis e75663f5b9 Merge branch 'master' into change-permalink-behavior 2017-03-28 14:31:53 -06:00
Riley Davis 4e60689fa4 view a permalink thread in isolation 2017-03-28 14:31:43 -06:00
Wyatt Johnson e7c0c5b638 adjusted path 2017-03-28 09:54:27 -06:00
Belén Curcio 4ff683b3af Merge branch 'master' into dynamic-email-templates 2017-03-28 11:20:22 -03:00
David Erwin 4f1f85d127 Merge pull request #443 from coralproject/community-flags
Community flags
2017-03-28 10:19:37 -04:00
Gabriela Rodríguez Berón 155be523ff Merge branch 'master' into community-flags 2017-03-28 07:14:30 -07:00
Riley Davis 3a47dd4c4b Merge branch 'master' into dynamic-email-templates 2017-03-27 12:44:52 -06:00
Riley Davis 2ec5b243fb hot-load email templates during development 2017-03-27 12:43:03 -06:00
Kim Gardner 0769a836d1 Merge pull request #444 from coralproject/fix-typo
Fix typo
2017-03-27 14:16:11 -04:00
Kim Gardner 68584cf2cf Fix typo 2017-03-27 13:59:45 -04:00
Kim Gardner 8a8700ebb2 Merge pull request #442 from coralproject/dep-upgrade
Package upgrades
2017-03-27 11:19:05 -04:00
Kim Gardner 542a261e5e Merge branch 'master' into dep-upgrade 2017-03-27 11:15:11 -04:00
Kim Gardner d539e23c00 Merge pull request #441 from coralproject/auto-close-comment-toggle
Auto close comment toggle
2017-03-27 11:07:40 -04:00
Belén Curcio a4dabd0308 Merge branch 'master' into dep-upgrade 2017-03-27 08:41:17 -03:00
Riley Davis c35043f799 Merge branch 'master' into auto-close-comment-toggle 2017-03-24 16:50:47 -06:00
gaba a6fa9a22c3 It refresh the page after editing name. 2017-03-24 13:57:10 -07:00
Kim Gardner a1f200de37 Merge pull request #436 from coralproject/translations
Translations
2017-03-24 16:46:21 -04:00
gaba a2551a499f Adds a few more font-sizes. 2017-03-24 12:58:45 -07:00
gaba 680919dd0e Making more sense. 2017-03-24 11:53:51 -07:00
Gabriela Rodríguez Berón c51a4ff228 Merge branch 'master' into translations 2017-03-24 11:33:16 -07:00
gaba b5d86a3380 Merge branch 'master' into community-flags 2017-03-24 11:19:30 -07:00
gaba 9eaed90318 Revert "Revert "Story 138187767 - Moderate Flag User Names""
This reverts commit 903aa23f19.
2017-03-24 11:15:42 -07:00
Riley Davis 2b90f2f588 Merge branch 'master' into auto-close-comment-toggle 2017-03-24 11:33:17 -06:00
Riley Davis 8114608626 put the mongoose update opts in the right place 2017-03-24 11:33:06 -06:00
Kim Gardner 596b7f538a Merge pull request #439 from coralproject/highlighter-wordz
highlight suspect, banned words and links
2017-03-24 13:19:26 -04:00
Wyatt Johnson 228fdeac4d Removed root .babelrc file, upgrades
- Upgraded packages
- Fixed sourcemaps
- Moved .babelrc config into client folder
- Created .babelrc in tests folder for client tests
2017-03-24 11:12:57 -06:00
Kim Gardner 67da144391 Merge branch 'master' into highlighter-wordz 2017-03-24 12:58:24 -04:00
Kim Gardner deb1d558fe Merge pull request #438 from coralproject/keep-on-keeping-on
always show moderation link to admins
2017-03-24 12:55:50 -04:00
riley 701994f8f7 pass lint 2017-03-24 10:42:31 -06:00
riley 5d5ad2585d autoclose an article toggle 2017-03-24 10:40:12 -06:00
Wyatt Johnson 05efbc4ded Updated lockfile 2017-03-24 10:15:03 -06:00
gaba 323749915a CSS alignments 2017-03-24 08:59:43 -07:00
gaba 84f1d3ece2 Use findONeAndUpdate and the moderation queue for account flags should only give back pending accounts. 2017-03-23 16:02:25 -07:00
riley 6b6d3d8bbb highlight suspect, banned words and links 2017-03-23 16:09:36 -06:00
gaba b9c816285d Fix params for moduserflaggedquery. 2017-03-23 14:48:15 -07:00
Riley Davis 7f1b993ea6 always show moderation link to admins 2017-03-23 13:04:37 -06:00
Riley Davis c1baccae5a Merge pull request #435 from coralproject/reset-password-redirect
redirect to the parent url when resetting your password
2017-03-23 12:59:31 -06:00
Riley Davis 984514d7eb check whitelist before redirecting 2017-03-23 12:46:20 -06:00
Riley Davis 67e04bf156 Merge branch 'master' into reset-password-redirect 2017-03-23 10:29:05 -06:00
David Erwin 7e65ba6371 Merge pull request #437 from coralproject/revert-370-story-138187767-mod-flag-names
Revert "Story 138187767  - Moderate Flag User Names"
2017-03-23 11:22:28 -04:00
David Erwin 903aa23f19 Revert "Story 138187767 - Moderate Flag User Names" 2017-03-23 11:18:55 -04:00
David Erwin 03aebd7840 Merge pull request #432 from coralproject/my-comments-ux
My Comments / Profile Tab UX
2017-03-23 10:57:40 -04:00
David Erwin 53ba313f57 Merge branch 'master' into my-comments-ux 2017-03-23 10:53:47 -04:00
David Erwin da8e632fd3 Merge pull request #433 from coralproject/design-more
Design UX - #141430305
2017-03-23 10:53:32 -04:00
David Erwin 6c83b2e60d Merge branch 'master' into design-more 2017-03-23 10:49:26 -04:00
David Erwin ae457f1951 Merge branch 'master' into my-comments-ux 2017-03-23 10:40:00 -04:00
David Erwin 907f7173fb Merge pull request #430 from coralproject/admin-ux
Admin UX
2017-03-23 10:39:18 -04:00
David Erwin a01c6f0f78 Merge branch 'master' into admin-ux 2017-03-23 09:04:36 -04:00
Belen Curcio 23833886bb missing tab name 2017-03-23 09:51:26 -03:00
Belen Curcio 464539d6a3 missing tab name 2017-03-23 09:50:18 -03:00
Riley Davis 375ff08584 Merge branch 'master' into reset-password-redirect 2017-03-22 18:45:40 -06:00
Gabriela Rodríguez Berón 849be3d5fd Merge branch 'master' into translations 2017-03-22 17:31:28 -07:00
gaba 35c8e1b5f9 Check spanish translations 2017-03-22 17:27:43 -07:00
gaba 4cbd6ebad2 Mooore translations. 2017-03-22 16:21:40 -07:00
Gabriela Rodríguez Berón ae96cb1e44 Merge branch 'master' into my-comments-ux 2017-03-22 16:18:45 -07:00
Riley Davis 4b0d0fed53 unused variable 2017-03-22 17:10:11 -06:00
Riley Davis fb2f8ef074 redirect to the parent url when resetting your password 2017-03-22 16:49:34 -06:00
Riley Davis c6401a2d08 Merge pull request #434 from coralproject/asset-url
asset_url and canonical link tags
2017-03-22 15:51:08 -06:00
Riley Davis 7af16471cd if the asset_url is not present, attempt to find the canonical link tag. if not, warn the user and strip the query params. 2017-03-22 15:41:06 -06:00
gaba 27a8c994a8 Fixing more translations 2017-03-22 13:19:08 -07:00
Belen Curcio 0bae6fe76c design tweaks 2017-03-22 17:06:41 -03:00
Belén Curcio 1e54a997d6 Merge branch 'master' into my-comments-ux 2017-03-22 16:25:06 -03:00
Belen Curcio 85d235bf37 Merge branch 'my-comments-ux' of github.com:coralproject/talk into my-comments-ux 2017-03-22 16:23:00 -03:00
Riley Davis d8e92968a5 Merge branch 'master' into admin-ux 2017-03-22 13:22:21 -06:00
Belen Curcio 4f50e8f19c Removing tests for now until we find a better solution for this 2017-03-22 16:19:05 -03:00
gaba 78e7951ad7 Merge branch 'master' into translations 2017-03-22 12:11:52 -07:00
Gabriela Rodríguez Berón 82068cd20e Merge pull request #370 from coralproject/story-138187767-mod-flag-names
Story 138187767  - Moderate Flag User Names
2017-03-22 12:10:43 -07:00
gaba 72e714a42c Translations into json 2017-03-22 11:59:33 -07:00
gaba 54015691fe Merge branch 'master' into translations 2017-03-22 11:46:09 -07:00
gaba d9af09d2a3 Email templates 2017-03-22 11:43:03 -07:00
Belén Curcio ae672d038c Merge branch 'master' into my-comments-ux 2017-03-22 15:37:48 -03:00
Belen Curcio 5729b03656 my-comments-ux 2017-03-22 15:35:40 -03:00
gaba 046cea5043 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-22 11:33:25 -07:00
Gabriela Rodríguez Berón d7ccfa4eda Merge pull request #431 from coralproject/remove-express-dep
remove express dep from mailer service
2017-03-22 11:32:36 -07:00
gaba ac6a116896 Only the name for the template. 2017-03-22 11:21:41 -07:00
David Erwin 49e4486b75 Merge pull request #426 from coralproject/no-reply-closed-streams
hide reply button if stream is closed
2017-03-22 11:08:33 -07:00
Riley Davis 555b4517d8 move email templates into services/mailer. remove express app dep to render files 2017-03-22 12:00:30 -06:00
Belen Curcio 71415877b3 comment tweaks 2017-03-22 14:04:08 -03:00
gaba 9782cf0b9d Mail SendSimple will not use app in the future but render from ejs. 2017-03-22 10:02:59 -07:00
Belen Curcio cd27ffe86a wip 2017-03-22 13:37:44 -03:00
Belen Curcio 6a5f667336 hover, menu order 2017-03-22 13:20:26 -03:00
Riley Davis b51cbebe1d Merge branch 'master' into story-138187767-mod-flag-names 2017-03-22 09:57:29 -06:00
David Erwin 2016546ca3 Merge pull request #426 from coralproject/no-reply-closed-streams
hide reply button if stream is closed
2017-03-22 10:53:15 -04:00
David Erwin 63fca6ba4c Merge branch 'master' into no-reply-closed-streams 2017-03-22 10:45:02 -04:00
gaba 7522bd9cd6 Sends a mail to the suspended user. 2017-03-21 15:08:46 -07:00
David Erwin 9cf974d098 Merge pull request #427 from coralproject/view-more-no-worky
Comment Stream Bugfix
2017-03-21 17:59:40 -04:00
David Erwin 072d332027 Merge branch 'master' into view-more-no-worky 2017-03-21 17:54:37 -04:00
Riley Davis 80ff090c45 better tracking of new/old comments 2017-03-21 15:52:26 -06:00
Riley Davis a61b784d2b Merge pull request #425 from coralproject/reCAPTCHA-docs
Added reCAPTCHA docs
2017-03-21 15:03:42 -06:00
Riley Davis 8804b5b63f Merge branch 'master' into reCAPTCHA-docs 2017-03-21 14:59:59 -06:00
David Erwin d7adf0c792 Merge pull request #423 from coralproject/stream-sorting
sort the stories by date
2017-03-21 16:47:27 -04:00
Riley Davis 442e384298 Merge branch 'master' into no-reply-closed-streams 2017-03-21 13:21:01 -06:00
Riley Davis afaf9dd26d Merge branch 'master' into stream-sorting 2017-03-21 13:20:53 -06:00
Riley Davis 94619ca291 actually pass the asset_id instead of undefined 2017-03-21 12:30:27 -06:00
Belén Curcio b9fc1a6186 Merge branch 'master' into view-more-no-worky 2017-03-21 15:08:27 -03:00
David Erwin 481c22b7dd Merge pull request #429 from coralproject/documentation-fix
Fixed plugin examples
2017-03-21 13:44:57 -04:00
Gabriela Rodríguez Berón b9960c3cc7 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-21 10:42:48 -07:00
gaba a09d41053f It uses Button instead of FabButton from coral-ui. 2017-03-21 10:36:21 -07:00
Riley Davis c8dc3c5ad2 disable replies too 2017-03-21 10:27:11 -06:00
Wyatt Johnson e5a2d627df Fixed plugin examples 2017-03-21 10:03:24 -06:00
Riley Davis ffc5379e11 Merge branch 'master' into stream-sorting 2017-03-21 09:12:04 -06:00
Riley Davis f5d1e0df8f Merge branch 'master' into no-reply-closed-streams 2017-03-21 09:11:54 -06:00
Riley Davis 894ea634c9 Merge branch 'master' into view-more-no-worky 2017-03-21 09:09:57 -06:00
Wyatt Johnson 033502626c Merge pull request #428 from coralproject/load-more-fix
Fixed bug in loading more comments
2017-03-21 08:07:00 -06:00
Riley Davis 9bfc4b5815 how did this get duplicated? 2017-03-20 16:26:52 -06:00
Riley Davis 546dd63d8c why did only this get changed? 2017-03-20 16:21:01 -06:00
gaba e5bac88bb5 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-20 15:02:38 -07:00
Wyatt Johnson 20348d5f05 Fixed bug in loading more comments 2017-03-20 15:58:33 -06:00
Riley Davis 0383d86021 load more replies if there are currently no replies 2017-03-20 15:09:45 -06:00
riley fb503798e8 use the reply id so we don't get weird behavior. fix a console warning 2017-03-20 14:46:59 -06:00
gaba be2ecf0838 Adds user and message to flagaction to display in FE. 2017-03-20 13:38:11 -07:00
gaba 9e76ca5d27 Fix CSS. 2017-03-20 12:37:21 -07:00
riley d7e554a055 hide reply button if stream is closed 2017-03-20 13:01:38 -06:00
Wyatt Johnson eceddf109a Added reCAPTCHA docs 2017-03-20 12:35:22 -06:00
gaba ac8c73c4e9 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-20 10:47:04 -07:00
David Erwin 9ccdaec063 Merge pull request #422 from coralproject/plugin-patch
Update Dockerfile
2017-03-20 10:47:03 -07:00
Wyatt Johnson 69ad11adf5 Update Dockerfile 2017-03-20 10:47:03 -07:00
David Erwin af93953b17 Merge pull request #421 from coralproject/plugins
Server Graph Plugins
2017-03-20 10:47:02 -07:00
Riley Davis a7908f3399 Merge pull request #418 from coralproject/recaptcha-fix
Added new pageData action/reducer
2017-03-20 10:47:01 -07:00
David Erwin 17deea5b96 Merge branch 'master' into plugins 2017-03-20 10:47:01 -07:00
Riley Davis 09ec2dba94 combine destructuring 2017-03-20 10:47:00 -07:00
Wyatt Johnson 46ecf0c06f Renamed pageData -> config 2017-03-20 10:46:59 -07:00
Wyatt Johnson b35170484e Merge branch 'master' into recaptcha-fix 2017-03-20 10:46:59 -07:00
Wyatt Johnson 8eac8d6890 Merge branch 'master' into recaptcha-fix 2017-03-20 10:46:58 -07:00
Wyatt Johnson dfffbbe619 bug fix 2017-03-20 10:46:58 -07:00
Wyatt Johnson 5768d30a7a Added new pageData action/reducer 2017-03-20 10:46:57 -07:00
gaba 2fff47c753 Merge branch 'master' into plugins 2017-03-20 10:46:30 -07:00
Kim Gardner 74a561a770 Merge pull request #420 from coralproject/view-more-bug
show different copy per # of replies
2017-03-20 10:45:32 -07:00
Riley Davis 9d64639f8e differentiate between top- and comment-level LoadMore 2017-03-20 10:45:31 -07:00
Riley Davis 316aa12481 add spanish translations 2017-03-20 10:45:31 -07:00
Kim Gardner 369d867e77 Merge pull request #419 from coralproject/dashboard-reload-bugfix
dashboard refresh fix
2017-03-20 10:45:30 -07:00
Riley Davis 73c59cb137 show different copy per # of replies 2017-03-20 10:45:30 -07:00
Riley Davis 517ede1977 dashboard reload fix 2017-03-20 10:45:29 -07:00
Kim Gardner 5514f45ab3 Merge pull request #412 from coralproject/recaptcha-support
recaptcha + RL support
2017-03-20 10:45:28 -07:00
gaba 2a66a78d41 Merge branch 'master' into recaptcha-support 2017-03-20 10:37:18 -07:00
Kim Gardner 61a2f1ed36 Merge pull request #417 from coralproject/dashboard-bug
move CountdownTimer to its own component
2017-03-20 10:27:31 -07:00
Riley Davis d2e2eef046 do a null check. 2017-03-20 10:27:31 -07:00
gaba a2833538a7 move CountdownTimer to its own component 2017-03-20 10:26:12 -07:00
Kim Gardner ecb1c51a9f Merge pull request #415 from coralproject/config-shuffle
move pre-mod links
2017-03-20 10:21:59 -07:00
Kim Gardner 1a83ffe482 Merge branch 'master' into config-shuffle 2017-03-20 10:21:59 -07:00
Kim Gardner 2c2cbb720b Merge pull request #416 from coralproject/login-error-fix
correctly handle error when not logged in
2017-03-20 10:21:58 -07:00
Riley Davis 92055c7814 correctly handle error when not logged in 2017-03-20 10:21:58 -07:00
riley e975d6d809 move pre-mod links 2017-03-20 10:21:57 -07:00
Kim Gardner 8459c31b42 Merge pull request #414 from coralproject/streams-to-stories
change Stream > Story sometimes
2017-03-20 10:21:56 -07:00
Riley Davis 63c3d02329 change Stream > Story sometimes 2017-03-20 10:21:56 -07:00
Riley Davis dedcb87435 Merge branch 'master' into recaptcha-support 2017-03-20 10:21:55 -07:00
Kim Gardner 22753e1e14 Merge pull request #411 from coralproject/remove-like-widget
remove like widget for now
2017-03-20 10:21:54 -07:00
gaba 7232d09eb3 Merge branch 'master' into remove-like-widget 2017-03-20 07:47:37 -07:00
Riley Davis 5c767b6aa6 Merge branch 'master' into recaptcha-support 2017-03-20 07:42:31 -07:00
riley 1ff5ab2e3e remove like widget for now 2017-03-20 07:42:31 -07:00
David Erwin 75cb83014b Merge pull request #406 from coralproject/asset-comment-metrics
Asset comment metrics
2017-03-20 07:42:30 -07:00
David Erwin abd0b51441 Removing log 2017-03-20 07:42:29 -07:00
David Erwin c55b4d22d7 Language update 2017-03-20 07:42:29 -07:00
gaba a71b8a4ae4 Merge branch 'master' into asset-comment-metrics 2017-03-20 07:41:12 -07:00
Kim Gardner 4ff6c08d4d Merge pull request #407 from coralproject/hide-dashboard-cta
save dismissed timer note to localStorage
2017-03-20 07:39:31 -07:00
Kim Gardner 6af8ef7838 Merge branch 'master' into hide-dashboard-cta 2017-03-20 07:39:31 -07:00
Kim Gardner 465e2012c5 Merge pull request #410 from coralproject/firefox-mod-buttons
button text was wrapping in firefox
2017-03-20 07:39:30 -07:00
Kim Gardner 01db845814 Merge branch 'master' into firefox-mod-buttons 2017-03-20 07:39:29 -07:00
Kim Gardner aba9488aab Merge pull request #413 from coralproject/reporting-messaging
indentation and flag -> report
2017-03-20 07:39:28 -07:00
Riley Davis 337b6accec remove fakeurl 2017-03-20 07:39:28 -07:00
riley 39e69f3bdc button text was wrapping in firefox 2017-03-20 07:39:27 -07:00
riley 21acb0d1d7 indentation and flag -> report 2017-03-20 07:39:27 -07:00
Riley Davis 2124ca0f34 save dismissed timer note to localStorage 2017-03-20 07:39:26 -07:00
gaba 6f3cb34817 Merge branch 'master' into asset-comment-metrics 2017-03-20 07:39:00 -07:00
David Erwin 1d061ce359 Merge branch 'master' into asset-comment-metrics 2017-03-20 07:36:23 -07:00
Riley Davis f005907894 add some translations 2017-03-20 07:36:23 -07:00
Riley Davis 2c4bf52f6b add Activity widget 2017-03-20 07:36:22 -07:00
riley 8348e1e462 Merge branch 'master' into asset-comment-metrics 2017-03-20 07:36:22 -07:00
Riley Davis c8a0ded4ad try to just add recaptcha manually
use react-recaptcha

remove recaptcha file
2017-03-20 07:36:21 -07:00
riley ca07406b1a Merge branch 'master' into recaptcha-support 2017-03-20 07:36:20 -07:00
Riley Davis 6b332fbde1 Merge pull request #404 from coralproject/keyboard-shortcut-scroll
Scrolling to keep selected comments in view.
2017-03-20 07:36:19 -07:00
Riley Davis f8b3c5752e Merge branch 'master' into keyboard-shortcut-scroll 2017-03-20 07:36:19 -07:00
Riley Davis 46e8d30e22 scrollIntoView will work in more browsers 2017-03-20 07:36:18 -07:00
David Erwin 28b24ebb09 Fix bug in example 2017-03-20 07:36:17 -07:00
David Jay ae4bae6cd2 Scrolling to keep selected comments in view. 2017-03-20 07:36:17 -07:00
Wyatt Johnson a7c946f9af Fixed bug in docs 2017-03-20 07:36:16 -07:00
Wyatt Johnson 32af28bfdc Fixed typo 2017-03-20 07:36:16 -07:00
Wyatt Johnson 86f0ed80ea Added docs and added new context based plugins. 2017-03-20 07:36:15 -07:00
Wyatt Johnson 7bf1510a8b Changed before/after to pre/post 2017-03-20 07:36:15 -07:00
gaba 258fd8b25a Initial implementation at plugins 2017-03-20 07:36:06 -07:00
David Erwin 2ee6335328 Merge pull request #409 from coralproject/revert-version-pin
Removed the version pin
2017-03-20 07:33:00 -07:00
Wyatt Johnson 894395bc65 Resolved #357 2017-03-20 07:32:59 -07:00
David Erwin ebf675bf2c Merge pull request #405 from coralproject/asset-comment-total-count
Added support for total comment count
2017-03-20 07:32:58 -07:00
David Erwin 30380ed5d7 Merge branch 'master' into asset-comment-total-count 2017-03-20 07:32:58 -07:00
David Erwin 408a74ad4d Merge pull request #408 from coralproject/widen-notifications
make notifications wider
2017-03-20 07:32:57 -07:00
Riley Davis 09e7f483d1 Merge pull request #403 from coralproject/bugfix-ban-dialog-language
Hiding rejected note on ban dialog for rejected comments.
2017-03-20 07:32:56 -07:00
David Erwin fe6e6fbd2f Merge branch 'master' into widen-notifications 2017-03-20 07:32:56 -07:00
Riley Davis eab5832457 Merge branch 'master' into bugfix-ban-dialog-language 2017-03-20 07:32:55 -07:00
David Jay a243310ecd Merge branch 'master' into bugfix-ban-dialog-language 2017-03-20 07:32:54 -07:00
gaba be351e46fd Merge branch 'master' into bugfix-ban-dialog-language 2017-03-20 07:32:06 -07:00
Riley Davis e5c108f8fb make notifications wider 2017-03-20 07:29:09 -07:00
David Erwin 2f86b9eecf Merge branch 'master' into bugfix-ban-dialog-language 2017-03-20 07:29:09 -07:00
Wyatt Johnson fcc93963c4 Added support for total comment count 2017-03-20 07:29:08 -07:00
Riley Davis 117b73adf7 Merge pull request #398 from coralproject/widget-auto-update
Auto-update Dashboard
2017-03-20 07:29:07 -07:00
Riley Davis 587ee82787 Merge branch 'master' into widget-auto-update 2017-03-20 07:29:07 -07:00
Riley Davis 66efbf89d4 Merge pull request #401 from coralproject/remove-notification-warning
incorrectly using dispatch now that we do pym directly
2017-03-20 07:29:06 -07:00
David Jay 88538d7b64 Merge branch 'master' into remove-notification-warning 2017-03-20 07:29:05 -07:00
David Jay 2d968126ab Merge branch 'master' into remove-notification-warning 2017-03-20 07:29:05 -07:00
Riley Davis db38bcb477 modify to light grey 2017-03-20 07:29:04 -07:00
gaba b2234bc3c9 Merge branch 'master'
Conflicts:
	client/coral-admin/src/containers/Dashboard/Widget.css
2017-03-20 07:28:17 -07:00
Riley Davis d71ca8d3f9 Merge pull request #396 from coralproject/open-closed-stream-buttons
fix some firefox display things
2017-03-20 07:27:20 -07:00
Riley Davis e0d3ad32e2 Merge branch 'master' into open-closed-stream-buttons 2017-03-20 07:27:20 -07:00
David Erwin 5f5d33931e Merge branch 'master' into open-closed-stream-buttons 2017-03-20 07:27:19 -07:00
Riley Davis b94401a8cf make only the title clickable 2017-03-20 07:27:19 -07:00
David Jay 1fd74068cd Merge branch 'master' into open-closed-stream-buttons 2017-03-20 07:27:18 -07:00
Riley Davis 1dd0b79a3e Merge pull request #392 from coralproject/mod-queue-load-more
Load More button in mod queue
2017-03-20 07:27:17 -07:00
Riley Davis 90298436c6 Merge branch 'master' into widget-auto-update 2017-03-20 07:27:17 -07:00
David Jay 203776d07c Merge branch 'mod-queue-load-more' of github.com:coralproject/talk into mod-queue-load-more 2017-03-20 07:27:16 -07:00
David Jay a5e6689f81 Merge pull request #402 from coralproject/autoreject-on-ban
Autoreject on ban
2017-03-20 07:27:15 -07:00
David Jay a78648fc0d Merge branch 'master' into mod-queue-load-more 2017-03-20 07:27:15 -07:00
Belén Curcio 6d43969e3b Merge branch 'master' into autoreject-on-ban 2017-03-20 07:27:14 -07:00
Riley Davis aec0f678c0 Merge branch 'master' into autoreject-on-ban 2017-03-20 07:27:13 -07:00
gaba ff71908a10 Merge branch 'master' into mod-queue-load-more 2017-03-20 07:26:00 -07:00
David Erwin a430ca65c2 Merge branch 'master' into widget-auto-update 2017-03-20 07:24:17 -07:00
David Erwin 96cd4056c3 Merge pull request #400 from coralproject/flag-username-reset
Resetting report menu on close.
2017-03-20 07:24:16 -07:00
David Erwin ede6c44c93 Merge branch 'master' into flag-username-reset 2017-03-20 07:24:15 -07:00
David Erwin 8b0f01f2aa Merge pull request #394 from coralproject/bugfix-mod-queue-refresh
Preventing admin panel refresh on accept and reject
2017-03-20 07:24:14 -07:00
Riley Davis 065ccfbfc4 Merge branch 'master' into flag-username-reset 2017-03-20 07:24:13 -07:00
Riley Davis 43119e5e15 sort the stories by date 2017-03-17 14:30:11 -06:00
David Erwin 471a55c3eb Merge pull request #422 from coralproject/plugin-patch
Update Dockerfile
2017-03-17 12:50:17 -04:00
Wyatt Johnson c79d1d40da Update Dockerfile 2017-03-17 10:46:45 -06:00
David Erwin dbc3c265a3 Merge pull request #421 from coralproject/plugins
Server Graph Plugins
2017-03-17 12:35:51 -04:00
David Erwin b3d80b2888 Merge branch 'master' into plugins 2017-03-17 12:31:40 -04:00
Riley Davis cb3572acc5 Merge pull request #418 from coralproject/recaptcha-fix
Added new pageData action/reducer
2017-03-17 10:24:22 -06:00
Riley Davis c48ce8323d combine destructuring 2017-03-17 10:18:07 -06:00
Wyatt Johnson df4e07d39c Renamed pageData -> config 2017-03-17 08:37:58 -06:00
Wyatt Johnson 6426663103 Merge branch 'master' into plugins 2017-03-17 08:26:42 -06:00
Wyatt Johnson edeedfaa71 Merge branch 'master' into recaptcha-fix 2017-03-17 08:01:47 -06:00
Kim Gardner 42beac9a39 Merge pull request #420 from coralproject/view-more-bug
show different copy per # of replies
2017-03-17 09:05:24 -04:00
Riley Davis a7cdce3579 differentiate between top- and comment-level LoadMore 2017-03-16 15:35:38 -06:00
Riley Davis 528e2ff851 add spanish translations 2017-03-16 14:54:09 -06:00
Riley Davis c61b249a37 show different copy per # of replies 2017-03-16 14:37:29 -06:00
David Erwin 4b8fb48f63 Fix bug in example 2017-03-16 14:32:59 -04:00
Wyatt Johnson 4a416d9477 Fixed bug in docs 2017-03-16 12:31:25 -06:00
Wyatt Johnson b277f3be75 Merge branch 'master' into recaptcha-fix 2017-03-16 11:59:56 -06:00
Kim Gardner 4219520002 Merge pull request #419 from coralproject/dashboard-reload-bugfix
dashboard refresh fix
2017-03-16 13:55:17 -04:00
Riley Davis 39596399ec dashboard reload fix 2017-03-16 11:45:10 -06:00
Wyatt Johnson 681bdb1d92 bug fix 2017-03-16 11:11:24 -06:00
Wyatt Johnson 598fcc4cb0 Added new pageData action/reducer 2017-03-16 11:07:38 -06:00
Kim Gardner 3277923a8c Merge pull request #412 from coralproject/recaptcha-support
recaptcha + RL support
2017-03-16 12:16:50 -04:00
Riley Davis 736ac11782 Merge branch 'master' into recaptcha-support 2017-03-16 10:12:28 -06:00
Kim Gardner 080895c70b Merge pull request #417 from coralproject/dashboard-bug
move CountdownTimer to its own component
2017-03-16 12:07:49 -04:00
Riley Davis 1043a37eef do a null check. 2017-03-16 09:28:17 -06:00
Wyatt Johnson d0ec7bbad1 Fixed typo 2017-03-15 16:54:07 -06:00
Wyatt Johnson 19e8f5133f Added docs and added new context based plugins. 2017-03-15 16:52:04 -06:00
Riley Davis 378e855c4c remove logs 2017-03-15 14:20:27 -06:00
Riley Davis 9ddc3aa0b7 move CountdownTimer to its own component 2017-03-15 14:13:18 -06:00
Kim Gardner 8d87af2115 Merge pull request #415 from coralproject/config-shuffle
move pre-mod links
2017-03-15 14:12:24 -04:00
Kim Gardner babd1b3d8d Merge branch 'master' into config-shuffle 2017-03-15 13:30:43 -04:00
Kim Gardner 9e890101fa Merge pull request #416 from coralproject/login-error-fix
correctly handle error when not logged in
2017-03-15 13:28:45 -04:00
Riley Davis 9beff74862 correctly handle error when not logged in 2017-03-15 11:16:13 -06:00
riley 3071b3783e move pre-mod links 2017-03-15 10:15:47 -06:00
Kim Gardner 8a84587e54 Merge pull request #414 from coralproject/streams-to-stories
change Stream > Story sometimes
2017-03-15 11:28:11 -04:00
Wyatt Johnson dcb673f072 Changed before/after to pre/post 2017-03-15 09:21:24 -06:00
Wyatt Johnson fff9d84303 Initial implementation at plugins 2017-03-14 17:46:04 -06:00
Riley Davis 04298aca7d change Stream > Story sometimes 2017-03-14 16:30:02 -06:00
Riley Davis dbd1366c26 Merge branch 'master' into recaptcha-support 2017-03-14 15:56:20 -06:00
Kim Gardner d8ce015b98 Merge pull request #411 from coralproject/remove-like-widget
remove like widget for now
2017-03-14 17:45:26 -04:00
Riley Davis b612bbd96c Merge branch 'master' into remove-like-widget 2017-03-14 15:35:07 -06:00
Riley Davis 099cc36b89 Merge branch 'master' into recaptcha-support 2017-03-14 15:33:18 -06:00
Riley Davis 7bbf8abd3d try to just add recaptcha manually
use react-recaptcha

remove recaptcha file
2017-03-14 15:32:16 -06:00
David Erwin 66d04ab5ca Merge pull request #406 from coralproject/asset-comment-metrics
Asset comment metrics
2017-03-14 17:16:15 -04:00
David Erwin 6758a178eb Removing log 2017-03-14 17:10:37 -04:00
David Erwin 13dad0c895 Language update 2017-03-14 17:07:48 -04:00
Riley Davis b4d236df36 Merge branch 'master' into asset-comment-metrics 2017-03-14 15:00:27 -06:00
Kim Gardner 8876c10254 Merge pull request #407 from coralproject/hide-dashboard-cta
save dismissed timer note to localStorage
2017-03-14 13:29:45 -04:00
Kim Gardner c50d25c0e9 Merge branch 'master' into hide-dashboard-cta 2017-03-14 13:19:23 -04:00
Kim Gardner 1330ffd394 Merge pull request #410 from coralproject/firefox-mod-buttons
button text was wrapping in firefox
2017-03-14 13:17:45 -04:00
Kim Gardner d85473e20c Merge branch 'master' into firefox-mod-buttons 2017-03-14 13:01:24 -04:00
Kim Gardner 13e2766ae9 Merge pull request #413 from coralproject/reporting-messaging
indentation and flag -> report
2017-03-14 13:00:49 -04:00
Riley Davis fa66bab937 Merge branch 'master' into asset-comment-metrics 2017-03-14 10:46:21 -06:00
Riley Davis f0eaf01ccc remove fakeurl 2017-03-13 13:33:31 -06:00
riley 147345505f indentation and flag -> report 2017-03-13 13:23:00 -06:00
riley b1f426e70d Merge branch 'master' into recaptcha-support 2017-03-13 12:27:19 -06:00
riley 7bfa718830 remove like widget for now 2017-03-13 12:15:38 -06:00
riley 801555e66e button text was wrapping in firefox 2017-03-13 12:08:57 -06:00
Riley Davis bf31f07053 Merge pull request #404 from coralproject/keyboard-shortcut-scroll
Scrolling to keep selected comments in view.
2017-03-13 11:52:08 -06:00
Riley Davis 6d3bb6af03 Merge branch 'master' into keyboard-shortcut-scroll 2017-03-13 11:14:04 -06:00
Riley Davis f999be6fbf scrollIntoView will work in more browsers 2017-03-13 11:13:28 -06:00
David Erwin 15e962c985 Merge pull request #409 from coralproject/revert-version-pin
Removed the version pin
2017-03-13 12:01:18 -04:00
Wyatt Johnson 8e6c2e8075 Resolved #357 2017-03-13 09:43:47 -06:00
David Erwin eb00320a0c Merge pull request #405 from coralproject/asset-comment-total-count
Added support for total comment count
2017-03-13 11:32:35 -04:00
David Erwin 33062ca43e Merge branch 'master' into asset-comment-total-count 2017-03-13 11:14:36 -04:00
David Erwin cf753632bf Merge branch 'master' into asset-comment-metrics 2017-03-13 10:18:57 -04:00
David Erwin 5ceaa24217 Merge pull request #408 from coralproject/widen-notifications
make notifications wider
2017-03-13 10:18:28 -04:00
David Erwin 9f5648b4c3 Merge branch 'master' into widen-notifications 2017-03-13 10:14:51 -04:00
Riley Davis 087851f694 Merge pull request #403 from coralproject/bugfix-ban-dialog-language
Hiding rejected note on ban dialog for rejected comments.
2017-03-10 15:08:27 -07:00
Riley Davis e159435293 Merge branch 'master' into bugfix-ban-dialog-language 2017-03-10 14:50:49 -07:00
Riley Davis 9d9d336e06 make notifications wider 2017-03-10 13:54:33 -07:00
Riley Davis 9e7b710faa save dismissed timer note to localStorage 2017-03-10 13:48:14 -07:00
Riley Davis 4a10122f34 add some translations 2017-03-10 12:16:20 -07:00
Wyatt Johnson 93221de563 Added support for total comment count 2017-03-09 17:07:07 -07:00
David Jay 255807858d Scrolling to keep selected comments in view. 2017-03-09 18:06:44 -05:00
Riley Davis f6af847bfb add Activity widget 2017-03-09 16:05:09 -07:00
riley 25fe268872 Merge branch 'master' into asset-comment-metrics 2017-03-09 13:20:51 -07:00
Riley Davis 5893e0cbde Merge pull request #398 from coralproject/widget-auto-update
Auto-update Dashboard
2017-03-09 13:19:39 -07:00
Riley Davis 0a88d3da97 Merge branch 'master' into widget-auto-update 2017-03-09 13:15:56 -07:00
Riley Davis d46b06da1c Merge pull request #401 from coralproject/remove-notification-warning
incorrectly using dispatch now that we do pym directly
2017-03-09 13:01:37 -07:00
Riley Davis cb10de6c7a modify to light grey 2017-03-09 12:58:57 -07:00
Riley Davis 98a87f4595 Merge branch 'master'
Conflicts:
	client/coral-admin/src/containers/Dashboard/Widget.css
2017-03-09 12:22:20 -07:00
David Jay 1884ad6437 Merge branch 'master' into bugfix-ban-dialog-language 2017-03-09 13:51:51 -05:00
David Jay 3c296ea4e9 Merge branch 'master' into remove-notification-warning 2017-03-09 13:51:21 -05:00
Riley Davis 82f47e27ac Merge pull request #396 from coralproject/open-closed-stream-buttons
fix some firefox display things
2017-03-09 11:50:28 -07:00
David Jay 3232c04997 Merge branch 'master' into remove-notification-warning 2017-03-09 13:50:09 -05:00
Riley Davis e6dfb3f64a Merge branch 'master' into open-closed-stream-buttons 2017-03-09 11:44:47 -07:00
Riley Davis fb7895b02f Merge branch 'master' into widget-auto-update 2017-03-09 11:44:16 -07:00
Riley Davis a152b9f1d3 Merge pull request #392 from coralproject/mod-queue-load-more
Load More button in mod queue
2017-03-09 11:43:27 -07:00
David Jay 542ccc6190 Merge branch 'master' into bugfix-ban-dialog-language 2017-03-09 13:40:44 -05:00
David Jay 8f988c898d Merge branch 'mod-queue-load-more' of github.com:coralproject/talk into mod-queue-load-more 2017-03-09 13:36:59 -05:00
David Jay 66adf19b97 Merge branch 'master' into mod-queue-load-more 2017-03-09 13:36:37 -05:00
David Jay f24eb74745 Merge pull request #402 from coralproject/autoreject-on-ban
Autoreject on ban
2017-03-09 13:35:10 -05:00
David Jay a0953c0689 Adding proptypes to loadmore component. 2017-03-09 13:33:00 -05:00
Belén Curcio c3749c5e31 Merge branch 'master' into autoreject-on-ban 2017-03-09 13:27:30 -05:00
David Erwin ebdf4b5ec7 Merge branch 'master' into open-closed-stream-buttons 2017-03-09 13:15:45 -05:00
David Erwin 53043051f3 Merge branch 'master' into bugfix-ban-dialog-language 2017-03-09 13:13:10 -05:00
David Erwin a195879556 Merge branch 'master' into widget-auto-update 2017-03-09 13:11:17 -05:00
David Erwin b0ca4d0785 Merge pull request #400 from coralproject/flag-username-reset
Resetting report menu on close.
2017-03-09 13:10:31 -05:00
David Jay 39386a9fcf Merge branch 'master' into mod-queue-load-more 2017-03-09 13:07:41 -05:00
Riley Davis 0db92974c4 make only the title clickable 2017-03-09 11:05:18 -07:00
David Erwin f64063ba14 Merge branch 'master' into flag-username-reset 2017-03-09 12:57:09 -05:00
David Erwin 91dff157aa Merge pull request #394 from coralproject/bugfix-mod-queue-refresh
Preventing admin panel refresh on accept and reject
2017-03-09 12:55:24 -05:00
David Jay fd9f838948 Merge branch 'master' into open-closed-stream-buttons 2017-03-09 12:39:54 -05:00
Riley Davis db5df1699c Merge branch 'master' into autoreject-on-ban 2017-03-09 10:30:10 -07:00
Riley Davis c064bee4f6 Merge branch 'master' into flag-username-reset 2017-03-09 10:27:27 -07:00
Riley Davis 895d3c31fc Merge branch 'master' into bugfix-mod-queue-refresh 2017-03-09 10:24:25 -07:00
Riley Davis 070ed13495 Merge branch 'master' into mod-queue-load-more 2017-03-09 10:15:08 -07:00
Gabriela Rodríguez Berón 491694b9c1 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-09 17:20:00 +01:00
Belén Curcio 6a72f4d066 Merge pull request #397 from coralproject/long-titles
Adding fix for handling long titles in the Moderation Queue
2017-03-09 11:17:11 -05:00
Belén Curcio 9bfc7d2037 Merge branch 'master' into long-titles 2017-03-09 11:05:08 -05:00
Belen Curcio 438877d58f Merge branch 'long-titles' of github.com:coralproject/talk into long-titles 2017-03-09 11:03:37 -05:00
Belen Curcio ce7a2d54d0 Font Size 2017-03-09 10:59:28 -05:00
Wyatt Johnson 63220cb6a7 Some fixes to make it work 2017-03-08 19:14:02 -07:00
Wyatt Johnson feaad2d6ca Added loader, graphql 2017-03-08 19:01:08 -07:00
David Jay 9b53930861 Merge branch 'master' into flag-username-reset 2017-03-08 17:49:59 -05:00
riley 2cdf2bb332 add moderate button 2017-03-08 15:26:45 -07:00
David Jay 7bf5285a1d Merge branch 'master' into bugfix-ban-dialog-language 2017-03-08 17:10:14 -05:00
David Jay a8d090d5a2 Merge branch 'master' into autoreject-on-ban 2017-03-08 17:07:26 -05:00
David Jay 377563c798 Hiding rejected note on ban dialog for rejected comments. 2017-03-08 17:02:47 -05:00
Riley Davis f0b1460aa8 Merge pull request #399 from coralproject/mod-squad
update dashboard links
2017-03-08 15:01:00 -07:00
Riley Davis 7b5721e0c6 Merged branch open-closed-stream-buttons into open-closed-stream-buttons 2017-03-08 15:00:09 -07:00
Riley Davis b34758e8cc unused vars 2017-03-08 14:59:55 -07:00
Riley Davis ee84b821fc Merge branch 'master' into mod-squad 2017-03-08 14:57:19 -07:00
Riley Davis c9e5b91b18 Merge branch 'master' into open-closed-stream-buttons 2017-03-08 14:53:24 -07:00
Riley Davis eaf035acd6 simplify dashboard table rendering 2017-03-08 14:53:07 -07:00
David Jay fac3d26c22 Closing ban dialog after ban is complete. 2017-03-08 16:39:53 -05:00
Riley Davis 5a50ae0d10 incorrectly using dispatch now that we do pym directly 2017-03-08 14:16:20 -07:00
David Erwin c467460580 Merge pull request #393 from coralproject/banned-optimize-bug
rejected comment optimize bug
2017-03-08 16:16:06 -05:00
David Jay d203a9afe0 Auto-rejecting comments on ban. 2017-03-08 16:06:34 -05:00
Riley Davis e4cdc73bf8 fix for react-mdn-selectfield in FF 2017-03-08 13:53:39 -07:00
David Jay 3262d136b7 Resetting report menu on close. 2017-03-08 15:44:19 -05:00
David Jay 3c28f68d0a Merge branch 'master' into mod-squad 2017-03-08 15:32:48 -05:00
David Jay a13e0205de Merge branch 'master' into open-closed-stream-buttons 2017-03-08 15:22:08 -05:00
David Jay be3212e144 Merge branch 'master' into banned-optimize-bug 2017-03-08 15:05:12 -05:00
David Jay 510ee2c28e Merge branch 'master' into bugfix-mod-queue-refresh 2017-03-08 15:04:45 -05:00
David Jay 3861d14a07 Merge branch 'master' into mod-queue-load-more 2017-03-08 15:04:24 -05:00
Gabriela Rodríguez Berón dae2e7d9d6 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-08 20:09:50 +01:00
gaba 69916a6ef2 I could not figure out how to send the parameter to the graphql query through the client. 2017-03-08 19:39:28 +01:00
Riley Davis 4d69ecc425 Merge branch 'master' into widget-auto-update 2017-03-08 11:12:53 -07:00
Riley Davis ec634fa3c0 add translations 2017-03-08 11:12:44 -07:00
Riley Davis 8989f3bfc2 Merge pull request #389 from coralproject/keyboard-shortcuts
Keyboard shortcuts
2017-03-08 11:10:24 -07:00
David Jay 15193e97a8 Preventing keys from scrolling past the end of the comment queue. 2017-03-08 13:01:37 -05:00
Riley Davis 0863ca26ff update dashboard links 2017-03-08 09:56:36 -07:00
Riley Davis 097fc30ac9 Merge branch 'master' into bugfix-mod-queue-refresh 2017-03-08 09:35:05 -07:00
Riley Davis bc1fc15572 Merge branch 'master' into banned-optimize-bug 2017-03-08 09:34:55 -07:00
Riley Davis 8c67acf609 Merge branch 'master' into mod-queue-load-more 2017-03-08 09:34:49 -07:00
Riley Davis 68d64665a9 Merge branch 'master' into keyboard-shortcuts 2017-03-08 09:34:38 -07:00
Riley Davis 47f6f4cb6c styles and auto-update Dashboard 2017-03-07 16:51:22 -07:00
Samantha Hankins 9d2d878790 Merge branch 'master' into long-titles 2017-03-07 17:17:44 -05:00
Samantha Hankins 02767cf060 Merge pull request #395 from coralproject/user-facing
User facing Styles Updates
2017-03-07 17:13:19 -05:00
Belen Curcio cceeafadec Indent 2017-03-07 16:53:42 -05:00
Belen Curcio af4a69a677 Changing icon and link 2017-03-07 16:50:36 -05:00
Belen Curcio f563bcf119 Adding fix for long text 2017-03-07 16:35:01 -05:00
Belen Curcio d1119604e0 ícon styling for infobox 2017-03-07 15:59:05 -05:00
Riley Davis e578f901b2 fix some firefox display things 2017-03-07 13:38:01 -07:00
Belen Curcio eb06f3892d changes 2017-03-07 15:27:43 -05:00
Belen Curcio ee30f549a1 Lato all the things 2017-03-07 15:04:35 -05:00
Belen Curcio dc96b550a6 Button should inherit font 2017-03-07 14:56:28 -05:00
Riley Davis d66dcd8548 Merge branch 'master' into mod-queue-load-more 2017-03-07 12:33:04 -07:00
riley c1d6a4dc7e Merged master into banned-optimize-bug 2017-03-07 12:31:42 -07:00
Riley Davis 9d13eedede Merge pull request #387 from coralproject/notification-warning
move notifications to parent
2017-03-07 12:30:02 -07:00
Riley Davis c4f2d876ee Merge branch 'master' into notification-warning 2017-03-07 12:22:42 -07:00
David Jay 7e55ecab59 Merge pull request #388 from coralproject/reply-box-focus
Focusing on reply box when reply is clicked.
2017-03-07 14:19:37 -05:00
David Jay 95aa89faed Switching premod status update to updateQueries function. 2017-03-07 14:16:44 -05:00
Riley Davis 87ef7ffb67 Merge branch 'master' into reply-box-focus 2017-03-07 11:54:55 -07:00
Riley Davis 162091a6d0 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-07 11:44:10 -07:00
David Jay d0d0766ca6 Merge branch 'master' into notification-warning 2017-03-07 13:33:24 -05:00
David Jay 1e34ffd075 Merge branch 'master' into banned-optimize-bug 2017-03-07 13:31:12 -05:00
David Jay 2b947bebf8 Merge pull request #383 from coralproject/premod-links
Premod links
2017-03-07 13:30:20 -05:00
Riley Davis 21003ca994 Merge branch 'master' into mod-queue-load-more 2017-03-07 11:29:59 -07:00
Riley Davis e15e4b004f Merge branch 'master' into keyboard-shortcuts 2017-03-07 11:26:58 -07:00
Riley Davis 37dad96fee Merge branch 'master'
Conflicts:
	client/coral-admin/src/containers/Configure/StreamSettings.js
2017-03-07 11:23:22 -07:00
David Jay 78a2a64671 Not displaying premod or rejected comments. 2017-03-07 13:13:21 -05:00
Gabriela Rodríguez Berón 0b280d0de2 Merge branch 'master' into story-138187767-mod-flag-names 2017-03-07 19:12:45 +01:00
David Jay 3d4521ab1d Merge pull request #384 from coralproject/global-info-bug
Guidelines info box has a word wrapping issue
2017-03-07 13:11:39 -05:00
David Jay ae5d35273b Merge branch 'master' into global-info-bug 2017-03-07 12:58:25 -05:00
David Jay 9bf1246d3a Merge pull request #386 from coralproject/configure-bugs
Configure Styles and UX Polish
2017-03-07 12:58:02 -05:00
Riley Davis 978bf05128 Merge branch 'master' into premod-links 2017-03-07 10:57:05 -07:00
David Jay 7379924e21 Merge branch 'master' into global-info-bug 2017-03-07 12:56:19 -05:00
Riley Davis d46795bbb7 Merge branch 'master' into mod-queue-load-more 2017-03-07 10:55:28 -07:00
David Jay bf1d46d60d Merge branch 'master' into configure-bugs 2017-03-07 12:52:57 -05:00
David Jay 2c8678f9e1 Merge pull request #391 from coralproject/favicon
Favicon Support
2017-03-07 12:52:28 -05:00
Riley Davis 5e4c251e83 style the pending comment correctly 2017-03-07 10:47:51 -07:00
David Jay 2a6236b618 Moving ModerationMenu to a stateless compenent. 2017-03-07 12:37:12 -05:00
gaba f8bc1037d2 Adds the reason. 2017-03-07 18:36:27 +01:00
David Jay 54aa3bc8fa Styling load more button. 2017-03-07 12:31:43 -05:00
Riley Davis 3a31ca8d38 blur the comment until it's ready 2017-03-07 10:06:03 -07:00
gaba 62423e620e Remove edge on user. 2017-03-07 18:04:50 +01:00
David Jay d5b46f219e Adding load more button. 2017-03-07 11:54:44 -05:00
gaba 54354c3cc6 Translation. 2017-03-07 16:27:12 +01:00
gaba 564b8d8eaa Merge branch 'master' into story-138187767-mod-flag-names 2017-03-07 13:04:28 +01:00
gaba d06a5d37f0 Moves to the loader and work on the user query to add pagination and action type. 2017-03-07 07:29:32 +01:00
Wyatt Johnson d82f3d0f0d Added initial recaptcha + RL support 2017-03-06 15:26:44 -07:00
Riley Davis 09556c9bef indention 2017-03-06 14:06:32 -07:00
Belen Curcio d0e67592c0 Adding favicon :) 2017-03-06 16:05:56 -05:00
Riley Davis d9d086c61e remove a banned comment after it's confirmed as rejected 2017-03-06 13:53:56 -07:00
Riley Davis 832f3c8abc Merge branch 'master' into configure-bugs 2017-03-06 13:52:54 -07:00
Riley Davis aca2af44e2 Merge branch 'master' into notification-warning 2017-03-06 13:51:25 -07:00
Riley Davis b7fe4c6802 Merge pull request #390 from coralproject/password-reset-view-fix
Fix password reset view render path
2017-03-06 13:21:37 -07:00
Wyatt Johnson 1c81d0a04f Update index.js 2017-03-06 13:00:22 -07:00
David Jay 450f1fdbac Focusing on reply box when reply is clicked. 2017-03-06 13:25:01 -05:00
Riley Davis 5cb7172bbe Merge branch 'master'
Conflicts:
	client/coral-embed-stream/src/Embed.js
2017-03-06 11:01:37 -07:00
Riley Davis 000833509f remove notification reducer. have parent show notifications 2017-03-06 10:59:12 -07:00
Belen Curcio e80d6e7b0a Configure UX Style Polish! 2017-03-06 12:12:26 -05:00
David Jay 77386fb21d Merge pull request #385 from coralproject/version-endpoint-fix
Moved to correct route
2017-03-06 12:04:19 -05:00
Wyatt Johnson 56350ffe1a Moved to correct route 2017-03-06 09:58:21 -07:00
Belen Curcio 6cd75b57dc border box 2017-03-03 16:18:58 -05:00
David Jay 3280ed9ae5 Merge branch 'master' into premod-links 2017-03-03 16:17:40 -05:00
Belen Curcio ec4fc566c1 props 2017-03-03 16:15:30 -05:00
David Jay be8332da66 Switching link action from flag to premod. 2017-03-03 16:12:53 -05:00
David Jay b9488dc36c Updating moderation. 2017-03-03 16:09:34 -05:00
David Jay b912b60428 Removing unecessary log statement. 2017-03-03 16:01:53 -05:00
Belen Curcio 1f0d102115 new components!! textarea stuff and configure bugs 2017-03-03 15:53:53 -05:00
David Jay 2e5d453ea7 Updating link search and adding settings in admin panel. 2017-03-03 15:44:03 -05:00
Belen Curcio a1a45a7969 Styles and disable and enable things 2017-03-03 15:10:15 -05:00
David Jay cb88f0a9b7 Flagging links when settings are set appropriately. 2017-03-03 14:32:57 -05:00
Kim Gardner 6c4282128e Merge pull request #382 from coralproject/my_history_comments_reverse
Reverse chronological order for My Comment History
2017-03-03 13:45:08 -05:00
Kim Gardner 1d1aa9c30e Merge branch 'master' into my_history_comments_reverse 2017-03-03 13:41:18 -05:00
Belen Curcio bb05fa5325 Reverse chronos 2017-03-03 13:37:10 -05:00
Kim Gardner f8d7a94b76 Merge pull request #379 from coralproject/140895223
Stories and Moderate on individual steam comment components
2017-03-03 13:35:11 -05:00
Belén Curcio 740aa88958 Merge branch 'master' into 140895223 2017-03-03 13:30:39 -05:00
Kim Gardner 6b4c303012 Merge pull request #381 from coralproject/star
Best Comments - Heart to Star and Coral Icon added
2017-03-03 13:29:39 -05:00
Kim Gardner 74d9b5814f Merge branch 'master' into star 2017-03-03 13:24:32 -05:00
Belen Curcio 79102de9d2 Star and Coral UI added :D 2017-03-03 13:21:42 -05:00
Belén Curcio a21bdff12c Merge branch 'master' into 140895223 2017-03-03 13:14:10 -05:00
Wyatt Johnson 3a581941e9 Merge pull request #380 from coralproject/version-endpoint
Added Version endpoint
2017-03-03 11:13:57 -07:00
Belén Curcio 853a0ef8b0 Merge branch 'master' into 140895223 2017-03-03 13:12:40 -05:00
Wyatt Johnson 3fc349dbe4 Fixed endpoint... 2017-03-03 11:08:59 -07:00
Wyatt Johnson 840bfad06f Simplfied endpoint 2017-03-03 11:04:46 -07:00
Wyatt Johnson 646d1aa585 Added version endpoint 2017-03-03 11:03:11 -07:00
Belen Curcio 5da1af8387 Styles ready! 2017-03-03 13:00:22 -05:00
David Jay 45208b096f Correcting bug in prev keyboard shortcut. 2017-03-03 12:52:20 -05:00
David Jay 3ecb93365a Correcting bug in keyboard shortcut next and previous. 2017-03-03 12:50:39 -05:00
Wyatt Johnson 5e8ee8ccb6 Merge pull request #378 from coralproject/bump-version-110
Update for major release to include Best Comments
2017-03-03 10:04:58 -07:00
Belen Curcio d218979ee6 Show Story, show and hide Moderate 2017-03-03 11:46:09 -05:00
Kim Gardner d65fb64b7e Update for major release to include Best Comments 2017-03-03 11:28:22 -05:00
Kim Gardner 8ebf6cb35f Merge pull request #377 from coralproject/bump-version-101
Bump version for 1.0.1 release
2017-03-03 10:08:26 -05:00
Kim Gardner 9079756ec2 Merge branch 'master' into bump-version-101 2017-03-03 10:04:39 -05:00
Kim Gardner 03a4f246f5 Merge pull request #376 from coralproject/keyboard-shortcuts
Keyboard shortcuts
2017-03-03 10:03:52 -05:00
Kim Gardner b5653eca2d Merge branch 'master' into keyboard-shortcuts 2017-03-03 09:59:45 -05:00
Kim Gardner 65c9bc550e Merge branch 'master' into bump-version-101 2017-03-03 09:59:15 -05:00
Kim Gardner f40211031b Merge pull request #350 from gobengo/138617379_best_comments
Best Comments
2017-03-03 09:56:03 -05:00
Belén Curcio f30275e375 Merge branch 'master' into 138617379_best_comments 2017-03-03 09:51:25 -05:00
Kim Gardner edd694cfa5 Bump version for 1.0.1 release 2017-03-03 06:55:55 -05:00
gaba 0fe456f574 Fix suspend button. 2017-03-02 16:43:52 -08:00
gaba 2fa832a3f4 adds suspendUser func and mutation. 2017-03-02 15:54:50 -08:00
David Jay b92ea72b78 Re-enabling premod links in settings. 2017-03-02 16:44:36 -05:00
Riley Davis e3f6327b86 messages show up 2017-03-02 14:43:26 -07:00
Kim Gardner f7d19dcd68 Merge pull request #371 from coralproject/bugfix-e2e-permalink
Fix e2e
2017-03-02 16:34:50 -05:00
Kim Gardner da6d5d58b3 Merge branch 'master' into bugfix-e2e-permalink 2017-03-02 16:26:54 -05:00
David Jay 57a7000e59 Animating transition in mod queue. 2017-03-02 16:08:24 -05:00
David Jay d554bf5211 Adding single view and restyling selected comment. 2017-03-02 16:03:18 -05:00
David Jay 1146f22613 Accepting and rejecting comments w/ keystrokes. 2017-03-02 15:41:51 -05:00
David Jay 78be17ec6d Adding keyboard selection of comments. 2017-03-02 15:19:33 -05:00
David Jay 7be3b05f22 Re-enabling keyboard shortcut menu. 2017-03-02 14:51:51 -05:00
Riley Davis 51b8f660b2 Merge pull request #373 from coralproject/node-gyp-fix
node-gyp fix
2017-03-02 12:47:52 -07:00
Wyatt Johnson 3b21026526 Merge branch 'master' into node-gyp-fix 2017-03-02 12:38:38 -07:00
Wyatt Johnson 6ae7a89642 Merge pull request #375 from coralproject/un-jank
only trigger a re-flow when the height actually changes
2017-03-02 12:38:25 -07:00
Riley Davis a6c7fe85b9 remove querySelector call 2017-03-02 12:34:10 -07:00
Riley Davis 2246728c80 only trigger a re-flow when the height actually changes 2017-03-02 12:27:31 -07:00
Wyatt Johnson 6c4db2d4ca Merge branch 'master' into node-gyp-fix 2017-03-02 12:10:39 -07:00
Wyatt Johnson d04b230b3f Merge pull request #372 from coralproject/session-timeout-extension
Adjusted session timeouts
2017-03-02 12:10:29 -07:00
Wyatt Johnson daf0d38c26 fixes #362 2017-03-02 12:07:42 -07:00
David Jay 06f9c12f5e Updating comment count. 2017-03-02 13:11:16 -05:00
David Jay 609af767e8 Merge branch 'master' of github.com:coralproject/talk into bugfix-e2e-permalink 2017-03-02 13:04:32 -05:00
David Jay acda388d4e Updating e2e tests. 2017-03-02 13:00:54 -05:00
Wyatt Johnson 21783edbfe Adjusted session details 2017-03-02 10:59:19 -07:00
gaba 1899dcbb7e Suspend User dialog. 2017-03-02 09:27:53 -08:00
gaba 3626d2678e Accept button working. Add suspend user dialog. 2017-03-01 23:43:25 -08:00
Wyatt Johnson ec09f1ca44 Merge branch 'master' into 138617379_best_comments 2017-03-01 16:47:03 -07:00
gaba acc9b2d9bf Approve and Reject buttons. Still misssing suspending 2017-03-01 14:21:49 -08:00
gaba f5ef33d24e Ban Button with graphql mutation. 2017-03-01 12:57:12 -08:00
Wyatt Johnson eb8b733ac7 Merge branch 'master' into 138617379_best_comments 2017-03-01 11:46:04 -07:00
Wyatt Johnson 69deba245a Merge branch 'master' into 138617379_best_comments 2017-03-01 10:32:57 -07:00
Wyatt Johnson a1e2077ef1 Merge branch 'master' into 138617379_best_comments 2017-03-01 09:43:44 -07:00
Benjamin Goering 5cb522c419 Merge branch 'master' into 138617379_best_comments 2017-03-01 20:49:04 +08:00
Benjamin Goering ab70ac38fd If fail to addremov best comment tag, show user an error 2017-03-01 16:07:25 +08:00
Benjamin Goering 90730e52be Make all comment action buttons use cursor:pointer and nowrap (sam said ok) 2017-03-01 15:02:49 +08:00
Benjamin Goering 5e0d01a649 lift findById out of add,removeCommentTag mutators and into root_mutation.js defs 2017-03-01 14:15:31 +08:00
gaba 6dfd3fc4ef Fix the people tag. 2017-02-28 22:10:14 -08:00
Benjamin Goering b0036ef778 Remove findById in error conditions of CommentsService@addTag,removeTag 2017-03-01 14:09:03 +08:00
Benjamin Goering 4b2cc72e1a Remove unnecessary defensive code in CommentsService 2017-03-01 14:01:12 +08:00
Benjamin Goering 405dac292c Only transform-async-to-generator for client code. Allows async to work with mocha tests (if proper node version) 2017-03-01 13:43:39 +08:00
gaba da3c812dc1 Adds graphql for users flagged in the server and client. 2017-02-28 21:33:01 -08:00
Benjamin Goering 388c3182ea Change add,removeTag in CommentsService to use simpler query then elemMatch 2017-03-01 13:08:36 +08:00
Benjamin Goering 378622f7e8 commentTag mutation tests await SettingsService 2017-03-01 12:46:35 +08:00
Benjamin Goering d6167e10ba Revert ALLOWED_TAGS declaration to be arr of objects, not strings 2017-02-28 23:57:35 +08:00
Benjamin Goering 32152fee7e add newline that eslint wants 2017-02-28 23:56:53 +08:00
Benjamin Goering 3a2f893c24 eslintrc.json doesn't use babel-eslint parser, but does set ecmaVersion to 2017 2017-02-28 23:46:02 +08:00
Benjamin Goering 97ebb66f8c Remove @TODOs I don't care about 2017-02-28 20:09:24 +08:00
Benjamin Goering d9df38fc3a fix e2e tests with .comment selector that avoids hidden dialog 2017-02-28 20:00:41 +08:00
Benjamin Goering f8479a8661 fix e2e tests with .comment selector that avoids hidden dialog 2017-02-28 20:00:41 +08:00
Benjamin Goering fe8cf2c612 Style multiple tag labels and material-icon therein (edited dist/../default.css before :/) 2017-02-28 19:59:35 +08:00
Benjamin Goering 96d1447602 remove debugging code from embedStreamPage 2017-02-28 19:59:35 +08:00
Benjamin Goering af23717988 Indicate best comments. Add some aria labels 2017-02-28 19:59:34 +08:00
Benjamin Goering c2d199a21c Remove BestButton copy for now 2017-02-28 19:59:34 +08:00
Benjamin Goering c6df602b94 Remove cursor: pointer from BestButton for now. And remove unneeded container 2017-02-28 19:59:34 +08:00
Benjamin Goering 51452a6b33 Comment component passes addTag,removeTag to its replies' Comments 2017-02-28 19:59:34 +08:00
Benjamin Goering 7e3c5820dc BestButton is disabled while its saving 2017-02-28 19:59:33 +08:00
Benjamin Goering bce84f24fd Embed can remove best tag 2017-02-28 19:59:33 +08:00
Benjamin Goering 98f3033326 Add permission checking to addCommentTag,removeCommentTag 2017-02-28 19:59:33 +08:00
Benjamin Goering 30ee80b12c basic working removeCommentTag mutation 2017-02-28 19:59:33 +08:00
Benjamin Goering 4ff6bce8ec Add ComentsService#removeTag 2017-02-28 19:59:33 +08:00
Benjamin Goering 1bb20923a2 Fix Commenter/BestCommentTest to postComment before checking things. 2017-02-28 19:59:32 +08:00
Benjamin Goering 6e338290a1 addCommentTag mutation works 2017-02-28 19:59:32 +08:00
Benjamin Goering fc9c67b05c BestButton sends mutations to backend, but they aren't implemented yet 2017-02-28 19:59:32 +08:00
Benjamin Goering 907c4526da BestButton only shows if currentUser can use it (and e2e) 2017-02-28 19:57:50 +08:00
Benjamin Goering a26a60cc67 minial best button uionly and e2e 2017-02-28 19:57:50 +08:00
gaba ca4b03872b Merge branch 'master' into story-138187767-mod-flag-names 2017-02-27 09:39:52 -08:00
gaba 714b496c9d Merge branch 'master' into story-138187767-mod-flag-names 2017-02-27 07:46:23 -08:00
gaba 7557592dc4 Get more data into the store. 2017-02-24 16:06:18 -08:00
gaba ad1e640b87 Adds emptystate. 2017-02-23 16:44:30 -08:00
gaba b01db3297d Adds menu for Community. Rename community to people. Adds account flagged tabs. 2017-02-23 16:37:16 -08:00
238 changed files with 7356 additions and 1961 deletions
+3
View File
@@ -4,6 +4,9 @@
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"indent": ["error",
2
+6
View File
@@ -12,3 +12,9 @@ dump.rdb
*.cfg
.idea/
coverage/
.tags
.tags1
# remove plugin folders
plugins
plugins.json
+2 -5
View File
@@ -1,7 +1,4 @@
FROM node:7.6
# Add node-gyp for bcrypt build support
RUN yarn global add node-gyp
FROM node:7
# Create app directory
RUN mkdir -p /usr/src/app
@@ -20,4 +17,4 @@ RUN yarn install --production
# Bundle app source
COPY . /usr/src/app
CMD [ "yarn", "start" ]
CMD ["yarn", "start"]
+265
View File
@@ -0,0 +1,265 @@
# Talk Plugins
Plugins for Talk can take various forms, currently we are only supporting server
side plugins.
## Plugin Registration: `plugins.json`
All plugins must be registered in the root file `plugins.json`.
The format for this file is thus:
```js
{
"server": [
"people"
]
}
```
Where we have a `server` key with an array of plugins that match the folder
name in the `plugins/` folder. For example, the above `plugins.json` would
require a plugin from `plugins/people`, which must provide a `index.js` file
that returns an object that matches the Plugin Specification.
## Server Plugins
### Specification
Each plugin should export a single object with all hooks available on it.
_**Note: You will have access to the whole core and other plugin's typeDefs,
context, loaders, mutators, resolvers, hooks. This is intentional, as it
encourages composing plugins to merge functionality, like a Slack plugin which
provides a Slack notify context function as well as having the loader for
comments.**_
The following are the hooks available:
#### Field: `typeDefs`
```graphql
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
```
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
`typeDefs` should be a string that will be _merged_ with the existing type
definitions. `enum`'s will be appended to, types will be appended, and new types
will be added.
#### Field: `context`
```js
{
Slack: (context) => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
}
```
Any property provided here will be added to the context parameter available
inside all resolvers, loaders, mutators, and of course, other context based
plugins.
The top level item must accept a context for the request which it should use to
configure the context plugin before it would be mounted at `context.plugins`.
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
#### Field: `loaders`
```js
(context) => ({
People: {
load: () => db.people.find({user: context.user})
}
})
```
Loaders should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `mutators`
```js
(context) => ({
People: {
create: (name) => {
return db.people.insert({user: context.user, name});
}
}
})
```
Mutators should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `resolvers`
```js
{
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
}
```
Should return a resolver map as described in the
[Apollo Docs](http://dev.apollodata.com/tools/graphql-tools/resolvers.html#Resolver-map).
This will merge with the existing resolvers in core and from previous plugins.
#### Field: `hooks`
```js
{
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
}
```
Hooks here are pretty special, for each resolver field, you can specify a
pre/post hook that will execute pre and post field resolution.
If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
### Full Example
Contents of `plugins.json`:
```json
{
"server": [
"people"
]
}
```
Located in `plugins/people/index.js`:
```js
module.exports = {
typeDefs: `
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
`,
context: {
Slack: () => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
},
loaders: ({user}) => ({
People: {
load: () => db.people.find({user})
}
}),
mutators: ({user}) => ({
People: {
create: (name) => {
return db.people.insert({user, name});
}
}
}),
resolvers: {
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
},
hooks: {
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) => {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
}
};
```
+3 -1
View File
@@ -1,6 +1,6 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
Talk is currently in Beta! [Read more about Talk here.](https://coralproject.net/products/talk.html)
Third party licenses are available via the `/client/3rdpartylicenses.txt`
endpoint when the server is running with built assets.
@@ -41,6 +41,8 @@ Facebook Login enabled app.
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
- `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
- `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
+4 -4
View File
@@ -34,6 +34,7 @@ app.use(helmet({
}));
app.use(bodyParser.json());
app.use('/client', express.static(path.join(__dirname, 'dist')));
app.use('/public', express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
@@ -45,16 +46,15 @@ const session_opts = {
secret: process.env.TALK_SESSION_SECRET,
httpOnly: true,
rolling: true,
saveUninitialized: false,
resave: false,
saveUninitialized: true,
resave: true,
unset: 'destroy',
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 36000000, // 1 hour for expiry.
maxAge: 8.64e+7, // 24 hours for session token expiry
},
store: new RedisStore({
ttl: 1800,
client: redis.createClient(),
})
};
+1 -2
View File
@@ -1,15 +1,14 @@
{
"sourceMaps": true,
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-async-to-generator",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
+17 -2
View File
@@ -1,14 +1,17 @@
import React from 'react';
import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
import Streams from 'containers/Streams/Streams';
import Stories from 'containers/Stories/Stories';
import Configure from 'containers/Configure/Configure';
import LayoutContainer from 'containers/LayoutContainer';
import InstallContainer from 'containers/Install/InstallContainer';
import CommunityLayout from 'containers/Community/CommunityLayout';
import CommunityContainer from 'containers/Community/CommunityContainer';
import ModerationLayout from 'containers/ModerationQueue/ModerationLayout';
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
import Dashboard from 'containers/Dashboard/Dashboard';
const routes = (
@@ -18,9 +21,21 @@ const routes = (
<IndexRoute component={Dashboard} />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='streams' component={Streams} />
<Route path='stories' component={Stories} />
<Route path='dashboard' component={Dashboard} />
{/* Community Routes */}
<Route path='community' component={CommunityLayout}>
<Route path='flagged' components={CommunityContainer}>
<Route path=':id' components={CommunityContainer} />
</Route>
<Route path='people' components={CommunityContainer}>
<Route path=':id' components={CommunityContainer} />
</Route>
<IndexRedirect to='flagged' />
</Route>
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
+26 -9
View File
@@ -2,15 +2,28 @@ import * as actions from '../constants/auth';
import coralApi from 'coral-framework/helpers/response';
// Log In.
export const handleLogin = (email, password) => dispatch => {
export const handleLogin = (email, password, recaptchaResponse) => dispatch => {
dispatch({type: actions.LOGIN_REQUEST});
return coralApi('/auth/local', {method: 'POST', body: {email, password}})
.then(result => {
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
const params = {method: 'POST', body: {email, password}};
if (recaptchaResponse) {
params.headers = {'X-Recaptcha-Response': recaptchaResponse};
}
return coralApi('/auth/local', params)
.then(({user}) => {
if (!user) {
return dispatch(checkLoginFailure('not logged in'));
}
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(user, isAdmin));
})
.catch(error => {
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({type: actions.LOGIN_MAXIMUM_EXCEEDED, message: error.translation_key});
} else {
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
}
});
};
@@ -34,9 +47,13 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
return coralApi('/auth')
.then(result => {
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
.then(({user}) => {
if (!user) {
return dispatch(checkLoginFailure('not logged in'));
}
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(user, isAdmin));
})
.catch(error => {
console.error(error);
+21 -8
View File
@@ -7,28 +7,33 @@ import {
SORT_UPDATE,
COMMENTERS_NEW_PAGE,
SET_ROLE,
SET_COMMENTER_STATUS
SET_COMMENTER_STATUS,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_SUSPENDUSER_DIALOG,
HIDE_SUSPENDUSER_DIALOG
} from '../constants/community';
import coralApi from '../../../coral-framework/helpers/response';
export const fetchCommenters = (query = {}) => dispatch => {
dispatch(requestFetchCommenters());
export const fetchAccounts = (query = {}) => dispatch => {
dispatch(requestFetchAccounts());
coralApi(`/users?${qs.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
.then(({result, page, count, limit, totalPages}) =>{
dispatch({
type: FETCH_COMMENTERS_SUCCESS,
commenters: result,
accounts: result,
page,
count,
limit,
totalPages
})
)
});
})
.catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
};
const requestFetchCommenters = () => ({
const requestFetchAccounts = () => ({
type: FETCH_COMMENTERS_REQUEST
});
@@ -55,3 +60,11 @@ export const setCommenterStatus = (id, status) => (dispatch) => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
// Ban User Dialog
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
// Suspend User Dialog
export const showSuspendUserDialog = (user) => ({type: SHOW_SUSPENDUSER_DIALOG, user});
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPENDUSER_DIALOG});
+7
View File
@@ -0,0 +1,7 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => dispatch => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({type: CONFIG_UPDATED, data});
};
+1 -1
View File
@@ -4,5 +4,5 @@ export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
// Ban User Dialog
export const showBanUserDialog = (user, commentId) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId});
export const showBanUserDialog = (user, commentId, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, showRejectedNote});
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
@@ -4,6 +4,7 @@ import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
import Recaptcha from 'react-recaptcha';
const lang = new I18n(translations);
class AdminLogin extends React.Component {
@@ -18,13 +19,22 @@ class AdminLogin extends React.Component {
this.props.handleLogin(this.state.email, this.state.password);
}
onRecaptchaLoad = () => {
// do something?
}
onRecaptchaVerify = (recaptchaResponse) => {
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
}
handleRequestPassword = e => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
render () {
const {errorMessage} = this.props;
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
const signInForm = (
<form onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{lang.t(`errors.${errorMessage}`)}</Alert>}
@@ -49,6 +59,15 @@ class AdminLogin extends React.Component {
this.setState({requestPassword: true});
}}>Request a new one.</a>
</p>
{
loginMaxExceeded &&
<Recaptcha
sitekey={recaptchaPublic}
render='explicit'
theme='dark'
onloadCallback={this.onRecaptchaLoad}
verifyCallback={this.onRecaptchaVerify} />
}
</form>
);
const requestPasswordForm = (
@@ -84,9 +103,11 @@ class AdminLogin extends React.Component {
}
AdminLogin.propTypes = {
loginMaxExceeded: PropTypes.bool.isRequired,
handleLogin: PropTypes.func.isRequired,
passwordRequestSuccess: PropTypes.string,
loginError: PropTypes.string
loginError: PropTypes.string,
recaptchaPublic: PropTypes.string
};
export default AdminLogin;
@@ -5,7 +5,7 @@
i {
vertical-align: middle;
margin-right: 10px;
margin-right: 5px;
font-size: 14px;
}
}
@@ -8,7 +8,14 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
const onBanClick = (userId, commentId, handleBanUser, rejectComment, handleClose) => (e) => {
e.preventDefault();
handleBanUser({userId})
.then(handleClose)
.then(() => rejectComment({commentId}));
};
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, showRejectedNote}) => (
<Dialog
className={styles.dialog}
id="banuserDialog"
@@ -23,13 +30,13 @@ const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
</div>
<div className={styles.separator}>
<h3>{lang.t('bandialog.are_you_sure', user.name)}</h3>
<i>{lang.t('bandialog.note')}</i>
<i>{showRejectedNote && lang.t('bandialog.note')}</i>
</div>
<div className={styles.buttons}>
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
{lang.t('bandialog.cancel')}
</Button>
<Button cStyle="black" className={styles.ban} onClick={() => handleBanUser({userId: user.id})} raised>
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, handleBanUser, rejectComment, handleClose)} raised>
{lang.t('bandialog.yes_ban_user')}
</Button>
</div>
@@ -40,6 +47,8 @@ const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
BanUserDialog.propTypes = {
handleBanUser: PropTypes.func.isRequired,
handleClose: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
commentId: PropTypes.string,
user: PropTypes.object.isRequired,
};
@@ -1,71 +0,0 @@
import React from 'react';
import timeago from 'timeago.js';
import Linkify from 'react-linkify';
import styles from './ModerationList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import Highlighter from 'react-highlight-words';
import {Icon} from 'coral-ui';
import ActionButton from './ActionButton';
const linkify = new Linkify();
// Render a single comment for the list
const Comment = props => {
const {comment, author} = props;
let authorStatus = author.status;
const links = linkify.getMatches(comment.body);
return (
<li tabIndex={props.index} className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>{author.username || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(comment.createdAt || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{comment.flagged ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
</div>
<div className={styles.sideActions}>
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{props.modActions.map(
(action, i) =>
<ActionButton
option={action}
key={i}
type='COMMENTS'
comment={comment}
user={author}
menuOptionsMap={props.menuOptionsMap}
onClickAction={props.onClickAction}
onClickShowBanDialog={props.onClickShowBanDialog}/>
)}
</div>
{authorStatus === 'banned' ?
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
</div>
</div>
<div className={styles.itemBody}>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter
searchWords={props.suspectWords}
textToHighlight={comment.body} />
</Linkify>
</span>
</div>
</li>
);
};
export default Comment;
const linkStyles = {
backgroundColor: 'rgb(255, 219, 135)',
padding: '1px 2px'
};
const lang = new I18n(translations);
@@ -0,0 +1,83 @@
import React, {PropTypes} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
import styles from 'coral-admin/src/containers/Dashboard/Dashboard.css';
import {Icon} from 'coral-ui';
const lang = new I18n(translations);
const refreshIntervalSeconds = 60 * 5;
class CountdownTimer extends React.Component {
static propTypes = {
handleTimeout: PropTypes.func.isRequired
}
constructor (props) {
super(props);
try {
if (window.localStorage.getItem('coral:dashboardNote') === null) {
window.localStorage.setItem('coral:dashboardNote', 'show');
}
} catch (e) {
// above will fail in Private Mode in some browsers.
}
this.state = {
secondsUntilRefresh: refreshIntervalSeconds,
dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show'
};
}
componentWillMount () {
this.interval = setInterval(() => { // the countdown timer
let nextCount = this.state.secondsUntilRefresh - 1;
if (nextCount < 0) {
nextCount = refreshIntervalSeconds;
return this.props.handleTimeout();
}
this.setState({secondsUntilRefresh: nextCount});
}, 1000);
}
componentWillUnmount () {
window.clearInterval(this.interval);
}
formatTime = () => {
const minutes = Math.floor(this.state.secondsUntilRefresh / 60);
let seconds = (this.state.secondsUntilRefresh % 60).toString();
if (seconds.length < 2) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
}
dismissNote = () => {
try {
window.localStorage.setItem('coral:dashboardNote', 'hide');
} catch (e) {
// when setItem fails in Safari Private mode
this.setState({dashboardNote: 'hide'});
}
}
render () {
const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' ||
this.state.dashboardNote === 'hide'; // for Safari Incognito
return (
<p
style={{display: hideReloadNote ? 'none' : 'block'}}
className={styles.autoUpdate}
onClick={this.dismissNote}>
<b>×</b>
<Icon name='timer' /> <strong>{lang.t('dashboard.next-update', this.formatTime())}</strong> {lang.t('dashboard.auto-update')}
</p>
);
}
}
export default CountdownTimer;
@@ -0,0 +1,622 @@
$minHeight: 200px;
$fullscreenZIndex: 10;
.wrapper {
/* Fix blockquote styling of MDL: https://github.com/google/material-design-lite/issues/2037 */
blockquote {
> p:first-child {
padding-top: 16px;
}
> p:last-child {
margin-bottom: 0;
}
&::after {
margin-left: -0.5em;
}
}
}
.iconBold {
composes: material-icons from global;
&::before {
content: 'format_bold';
}
}
.iconItalic {
composes: material-icons from global;
&::before {
content: 'format_italic';
}
}
.iconTitle {
composes: material-icons from global;
&::before {
content: 'title';
}
}
.iconQuote {
composes: material-icons from global;
&::before {
content: 'format_quote';
}
}
.iconUnorderedList {
composes: material-icons from global;
&::before {
content: 'format_list_bulleted';
}
}
.iconOrderedList {
composes: material-icons from global;
&::before {
content: 'format_list_numbered';
}
}
.iconLink {
composes: material-icons from global;
&::before {
content: 'link';
}
}
.iconImage {
composes: material-icons from global;
&::before {
content: 'insert_photo';
}
}
.iconPreview {
composes: material-icons from global;
&::before {
content: 'remove_red_eye';
}
}
.iconSideBySide {
composes: material-icons from global;
&::before {
content: 'chrome_reader_mode';
}
}
.iconFullscreen {
composes: material-icons from global;
&::before {
content: 'fullscreen';
}
}
.iconGuide {
composes: material-icons from global;
&::before {
content: 'help_outline';
}
}
/*
* These are modified styles taken from https://github.com/NextStepWebs/simplemde-markdown-editor and
* put through http://sebastianpontow.de/css2compass/.
*/
/*
* simplemde v1.11.2
* Copyright Next Step Webs, Inc.
* @link https://github.com/NextStepWebs/simplemde-markdown-editor
* @license MIT
*/
:global {
.CodeMirror {
font-family: monospace;
color: black;
position: relative;
overflow: hidden;
background: white;
height: auto;
min-height: $minHeight;
border: 1px solid #ddd;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 10px;
font: inherit;
z-index: 1;
pre {
padding: 0 4px;
border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
font-variant-ligatures: none;
}
span {
/*TODO: vertical-align: text-bottom;*/
}
.CodeMirror-code {
.cm-tag {
color: #63a35c;
}
.cm-attribute {
color: #795da3;
}
.cm-string {
color: #183691;
}
.cm-header-1 {
font-size: 200%;
line-height: 200%;
}
.cm-header-2 {
font-size: 160%;
line-height: 160%;
}
.cm-header-3 {
font-size: 125%;
line-height: 125%;
}
.cm-header-4 {
font-size: 110%;
line-height: 110%;
}
.cm-comment {
background: rgba(0, 0, 0, .05);
border-radius: 2px;
}
.cm-link {
color: #7f8c8d;
}
.cm-url {
color: #aab2b3;
}
.cm-strikethrough {
text-decoration: line-through;
}
.cm-tab {
display: inline-block;
text-decoration: inherit;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
position: absolute;
}
.cm-header {
font-weight: bold;
}
.cm-strong {
font-weight: bold;
}
.cm-em {
font-style: italic;
}
.cm-link {
text-decoration: underline;
}
.cm-strikethrough {
text-decoration: line-through;
}
.cm-invalidchar {
color: #f00;
}
}
.CodeMirror-selected {
background: #d9d9d9;
}
.CodeMirror-placeholder {
opacity: .5;
}
div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) {
background: rgba(255, 0, 0, .15);
}
}
.CodeMirror-lines {
padding: 4px 0;
cursor: text;
min-height: 1px;
}
.CodeMirror-scrollbar-filler {
background-color: white;
position: absolute;
z-index: 6;
display: none;
right: 0;
bottom: 0;
}
.CodeMirror-gutter-filler {
background-color: white;
position: absolute;
z-index: 6;
display: none;
left: 0;
bottom: 0;
}
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
position: absolute;
left: 0;
top: 0;
min-height: 100%;
z-index: 3;
box-sizing: content-box;
}
.CodeMirror-guttermarker {
color: black;
}
.CodeMirror-guttermarker-subtle {
color: #999;
}
.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
position: absolute;
}
@keyframes blink {
0% {
}
50% {
background-color: transparent;
}
100% {
}
}
.CodeMirror-scroll {
overflow: scroll !important;
margin-bottom: -30px;
margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none;
position: relative;
min-height: $minHeight;
box-sizing: content-box;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
box-sizing: content-box;
}
.CodeMirror-vscrollbar {
position: absolute;
z-index: 6;
display: none;
right: 0;
top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
position: absolute;
z-index: 6;
display: none;
bottom: 0;
left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
*zoom: 1;
*display: inline;
box-sizing: content-box;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
user-select: none;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0;
bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-code {
outline: none;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
pre {
position: static;
}
}
.CodeMirror-focused {
.CodeMirror-selected {
background: #d7d4f0;
}
div.CodeMirror-cursors {
visibility: visible;
}
}
.CodeMirror-selected {
background: #d9d9d9;
}
.CodeMirror-line::selection {
background: #d7d4f0;
}
.CodeMirror-line {
> span::selection {
background: #d7d4f0;
}
> span {
> span::selection {
background: #d7d4f0;
}
}
}
@media print {
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
.CodeMirror-fullscreen {
background: #fff;
position: fixed !important;
top: 50px;
left: 0;
right: 0;
bottom: 0;
height: auto;
z-index: $fullscreenZIndex;
}
.CodeMirror-sided {
width: 50% !important;
}
.editor-toolbar {
position: relative;
opacity: .6;
user-select: none;
padding: 0 10px;
border-top: 1px solid #bbb;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
&:after {
display: block;
content: ' ';
height: 1px;
margin-top: 8px;
}
&:before {
display: block;
content: ' ';
height: 1px;
margin-bottom: 8px;
}
&:hover {
opacity: .8;
}
&.fullscreen {
width: 100%;
height: 50px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding-top: 10px;
padding-bottom: 10px;
box-sizing: border-box;
background: #fff;
border: 0;
position: fixed;
top: 0;
left: 0;
opacity: 1;
z-index: $fullscreenZIndex;
}
&.fullscreen::before {
width: 20px;
height: 50px;
background: linear-gradient(to right, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%);
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0;
}
&.fullscreen::after {
width: 20px;
height: 50px;
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
position: fixed;
top: 0;
right: 0;
margin: 0;
padding: 0;
}
a {
display: inline-block;
text-align: center;
text-decoration: none!important;
color: #2c3e50!important;
width: 30px;
height: 30px;
margin: 0;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
outline: 0;
margin-right: 2px;
&.active {
background: #fcfcfc;
border-color: #95a5a6;
}
&:hover {
background: #fcfcfc;
border-color: #95a5a6;
}
&:active {
background: #eee;
}
&:before {
line-height: 30px;
}
}
i.separator {
display: inline-block;
width: 0;
border-left: 1px solid #d9d9d9;
border-right: 1px solid #fff;
color: transparent;
text-indent: -10px;
margin: 0 6px;
}
&.disabled-for-preview a:not(.no-disable) {
pointer-events: none;
background: #fff;
border-color: transparent;
text-shadow: inherit;
}
}
@media only screen and(max-width: 700px) {
.editor-toolbar a.no-mobile {
display: none;
}
}
.editor-statusbar {
padding: 8px 10px;
font-size: 12px;
color: #959694;
text-align: right;
span {
display: inline-block;
min-width: 4em;
margin-left: 1em;
}
.lines:before {
content: 'lines: ';
}
.words:before {
content: 'words: ';
}
.characters:before {
content: 'characters: ';
}
}
.editor-preview {
padding: 10px;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #fafafa;
z-index: 7;
overflow: auto;
display: none;
box-sizing: border-box;
> p {
margin-top: 0;
}
pre {
background: #eee;
margin-bottom: 10px;
}
table {
td {
border: 1px solid #ddd;
padding: 5px;
}
th {
border: 1px solid #ddd;
padding: 5px;
}
}
}
.editor-preview-side {
padding: 10px;
position: fixed;
bottom: 0;
width: 50%;
top: 50px;
right: 0;
background: #fafafa;
z-index: $fullscreenZIndex;
overflow: auto;
display: none;
box-sizing: border-box;
border: 1px solid #ddd;
> p {
margin-top: 0;
}
pre {
background: #eee;
margin-bottom: 10px;
}
table {
td {
border: 1px solid #ddd;
padding: 5px;
}
th {
border: 1px solid #ddd;
padding: 5px;
}
}
}
.editor-preview-active-side {
display: block;
}
.editor-preview-active {
display: block;
}
.CodeMirror-overwrite .CodeMirror-cursor {
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.cm-tab-wrap-hack:after {
content: '';
}
span.CodeMirror-selectedtext {
background: none;
}
.editor-wrapper input.title {
&:focus {
opacity: .8;
}
&:hover {
opacity: .8;
}
}
}
@@ -0,0 +1,147 @@
import React, {Component, PropTypes} from 'react';
import SimpleMDE from 'simplemde';
import cn from 'classnames';
import noop from 'lodash/noop';
import styles from './MarkdownEditor.css';
const config = {
status: false,
// Do not download fontAwesome icons as we replace them with
// material icons.
autoDownloadFontAwesome: false,
// Disable built-in spell checker as it is very rudimentary.
spellChecker: false,
toolbar: [
{
name: 'bold',
action: SimpleMDE.toggleBold,
className: styles.iconBold,
title: 'Bold',
},
{
name: 'italic',
action: SimpleMDE.toggleItalic,
className: styles.iconItalic,
title: 'Italic',
},
{
name: 'title',
action: SimpleMDE.toggleHeadingSmaller,
className: styles.iconTitle,
title: 'Title, Subtitle, Heading',
},
'|',
{
name: 'quote',
action: SimpleMDE.toggleBlockquote,
className: styles.iconQuote,
title: 'Quote',
},
{
name: 'unordered-list',
action: SimpleMDE.toggleUnorderedList,
className: styles.iconUnorderedList,
title: 'Generic List',
},
{
name: 'ordered-list',
action: SimpleMDE.toggleOrderedList,
className: styles.iconOrderedList,
title: 'Numbered List',
},
'|',
{
name: 'link',
action: SimpleMDE.drawLink,
className: styles.iconLink,
title: 'Create Link',
},
{
name: 'image',
action: SimpleMDE.drawImage,
className: styles.iconImage,
title: 'Insert Image',
},
'|',
{
name: 'preview',
action: SimpleMDE.togglePreview,
className: cn(styles.iconPreview, 'no-disable'),
title: 'Toggle Preview',
},
{
name: 'side-by-side',
action: SimpleMDE.toggleSideBySide,
className: cn(styles.iconSideBySide, 'no-disable'),
title: 'Toggle Side by Side',
},
{
name: 'fullscreen',
action: SimpleMDE.toggleFullScreen,
className: cn(styles.iconFullscreen, 'no-disable'),
title: 'Toggle Fullscreen',
},
'|',
{
name: 'guide',
action: 'https://simplemde.com/markdown-guide',
className: styles.iconGuide,
title: 'Markdown Guide',
},
],
};
export default class MarkdownEditor extends Component {
textarea = null
editor = null
onRef = (ref) => this.textarea = ref
componentDidMount() {
this.editor = new SimpleMDE({
...config,
element: this.textarea,
});
this.editor.codemirror.on('change', this.onChange);
}
componentWillReceiveProps(nextProps) {
if (this.props.value !== nextProps.value && nextProps.value !== this.editor.value()) {
this.editor.value(nextProps.value);
}
}
componentDidUpdate() {
// Workaround empty render issue.
// https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313
this.editor.codemirror.refresh();
}
componentWillUnmount() {
this.editor.toTextArea();
}
onChange = () => {
if (this.props.onChange) {
this.props.onChange(this.editor.value());
}
}
render() {
return (
<div className={styles.wrapper}>
<textarea ref={this.onRef} {...this.props} onChange={noop} />
</div>
);
}
}
MarkdownEditor.propTypes = {
onChange: PropTypes.func,
value: PropTypes.string,
};
@@ -1,4 +1,3 @@
import React from 'react';
import {Button, Icon} from 'react-mdl';
import styles from './Modal.css';
@@ -178,8 +178,13 @@
}
}
.selected {
border-radius: 10px;
}
.actionButton {
transform: scale(.8);
margin: 0;
width: 140px;
}
@@ -1,106 +0,0 @@
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import React, {Component, PropTypes} from 'react';
import Modal from 'components/Modal';
import styles from './SuspendUserModal.css';
import {Button} from 'coral-ui';
const stages = [
{
title: 'suspenduser.title_0',
description: 'suspenduser.description_0',
options: {
'j': 'suspenduser.no_cancel',
'k': 'suspenduser.yes_suspend'
}
},
{
title: 'suspenduser.title_1',
description: 'suspenduser.description_1',
options: {
'j': 'bandialog.cancel',
'k': 'suspenduser.send'
}
}
];
class SuspendUserModal extends Component {
state = {email: '', stage: 0}
static propTypes = {
stage: PropTypes.number,
actionType: PropTypes.string,
onClose: PropTypes.func.isRequired,
suspendUser: PropTypes.func.isRequired
}
componentDidMount() {
const about = lang.t('suspenduser.username');
this.setState({email: lang.t('suspenduser.email', about)});
}
/*
* When an admin clicks to suspend a user a dialog is shown, this function
* handles the possible actions for that dialog.
*/
onActionClick = (stage, menuOption) => () => {
const {suspendUser, action} = this.props;
const {stage, email} = this.state;
const cancel = this.props.onClose;
const next = () => this.setState({stage: stage + 1});
const suspend = () => suspendUser(action.item_id, lang.t('suspenduser.email_subject'), email)
.then(this.props.onClose);
const suspendModalActions = [
[ cancel, next ],
[ cancel, suspend ]
];
return suspendModalActions[stage][menuOption]();
}
onEmailChange = (e) => this.setState({email: e.target.value})
render () {
const {action, onClose} = this.props;
if (!action) {
return null;
}
const {stage} = this.state;
const actionType = action.actionType;
const about = actionType === 'flag_bio' ? lang.t('suspenduser.bio') : lang.t('suspenduser.username');
return <Modal open={true} onClose={onClose}>
<div className={styles.title}>{lang.t(stages[stage].title, about)}</div>
<div className={styles.container}>
<div className={styles.description}>
{lang.t(stages[stage].description, about)}
</div>
{
stage === 1 &&
<div className={styles.writeContainer}>
<div className={styles.emailMessage}>{lang.t('suspenduser.write_message')}</div>
<div className={styles.emailContainer}>
<textarea
rows={5}
className={styles.emailInput}
value={this.state.email}
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)}>
{lang.t(stages[stage].options[key], about)}
</Button>
))}
</div>
</div>
</Modal>;
}
}
export default SuspendUserModal;
const lang = new I18n(translations);
-50
View File
@@ -1,50 +0,0 @@
import React from 'react';
import styles from './ModerationList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import {Icon} from 'react-mdl';
import ActionButton from './ActionButton';
// Render a single comment for the list
const User = props => {
const {action, user} = props;
let userStatus = user.status;
// 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 mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>{user.username}</span>
</div>
<div className={styles.sideActions}>
<div className={`actions ${styles.actions}`}>
{props.modActions.map(
(action, i) =>
<ActionButton
type={action.toUpperCase()}
key={i}
user={user}
menuOptionsMap={props.menuOptionsMap}
onClickAction={props.onClickAction}
onClickShowBanDialog={props.onClickShowBanDialog}/>
)}
</div>
</div>
<div>
{userStatus === 'banned' ?
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
</div>
</div>
<div className={styles.flagCount}>
{`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
</div>
</li>;
};
export default User;
const lang = new I18n(translations);
@@ -23,9 +23,9 @@ const CoralDrawer = ({handleLogout, restricted = false}) => (
{lang.t('configure.moderate')}
</Link>
<Link className={styles.navLink}
to="/admin/streams"
to="/admin/stories"
activeClassName={styles.active}>
{lang.t('configure.streams')}
{lang.t('configure.stories')}
</Link>
<Link className={styles.navLink}
to="/admin/community"
@@ -74,8 +74,12 @@
background-color: transparent;
transition: background-color 200ms;
&:hover {
background-color: #232323;
}
&.active {
background-color: #232323;
background-color: #232323;
}
}
+11 -4
View File
@@ -14,28 +14,35 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
<div>
<Navigation className={styles.nav}>
<IndexLink
id='dashboardNav'
className={styles.navLink}
to="/admin/dashboard"
activeClassName={styles.active}>
{lang.t('configure.dashboard')}
</IndexLink>
<Link
id='moderateNav'
className={styles.navLink}
to="/admin/moderate"
activeClassName={styles.active}>
{lang.t('configure.moderate')}
</Link>
<Link className={styles.navLink}
to="/admin/streams"
<Link
id='streamsNav'
className={styles.navLink}
to="/admin/stories"
activeClassName={styles.active}>
{lang.t('configure.streams')}
{lang.t('configure.stories')}
</Link>
<Link className={styles.navLink}
<Link
id='communityNav'
className={styles.navLink}
to="/admin/community"
activeClassName={styles.active}>
{lang.t('configure.community')}
</Link>
<Link
id='configureNav'
className={styles.navLink}
to="/admin/configure"
activeClassName={styles.active}>
+1
View File
@@ -11,6 +11,7 @@ export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export const LOGIN_MAXIMUM_EXCEEDED = 'LOGIN_MAXIMUM_EXCEEDED';
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
+2 -1
View File
@@ -1,6 +1,7 @@
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
@@ -5,3 +5,13 @@ export const SORT_UPDATE = 'SORT_UPDATE';
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
export const SET_ROLE = 'SET_ROLE';
export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS';
export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST';
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS';
export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE';
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
@@ -3,4 +3,3 @@ export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS';
export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE';
export const USER_EMAIL_FAILURE = 'USER_EMAIL_FAILURE';
export const USERNAME_ENABLE_FAILURE = 'USERNAME_ENABLE_FAILURE';
export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
@@ -1,3 +1,5 @@
@custom-media --big-viewport (min-width: 780px);
.container {
padding: 10px;
display: flex;
@@ -15,6 +17,12 @@
box-sizing: border-box;
}
.mainFlaggedContent {
width: 100%;
padding: 34px 14px;
box-sizing: border-box;
}
.roleButton {
display: block;
}
@@ -94,3 +102,232 @@
cursor: pointer;
}
}
.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: 40px 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;
}
.flaggedBy {
display: inline;
padding: 3px;
font-size: 16px;
}
.flaggedByLabel {
font-weight: bold;
font-size: 14px;
}
.flaggedReasons {
padding-top: 15px;
margin-left: 24px;
}
.flaggedByReason {
font-size: 1tpx;
}
@@ -1,14 +1,29 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries';
import {banUser, setUserStatus, suspendUser} from 'coral-admin/src/graphql/mutations';
import {
fetchCommenters,
fetchAccounts,
updateSorting,
newPage,
showBanUserDialog,
hideBanUserDialog,
showSuspendUserDialog,
hideSuspendUserDialog
} from '../../actions/community';
import Community from './Community';
import CommunityMenu from './components/CommunityMenu';
import BanUserDialog from './components/BanUserDialog';
import SuspendUserDialog from './components/SuspendUserDialog';
import People from './People';
import FlaggedAccounts from './FlaggedAccounts';
class CommunityContainer extends Component {
constructor(props) {
super(props);
@@ -22,6 +37,10 @@ class CommunityContainer extends Component {
this.onNewPageHandler = this.onNewPageHandler.bind(this);
}
componentWillMount() {
this.props.fetchAccounts({});
}
onKeyDownHandler(e) {
if (e.key === 'Enter') {
e.preventDefault();
@@ -38,16 +57,13 @@ class CommunityContainer extends Component {
search(query = {}) {
const {community} = this.props;
this.props.dispatch(fetchCommenters({
this.props.fetchAccounts({
value: this.state.searchValue,
field: community.field,
asc: community.asc,
field: community.fieldPeople,
asc: community.ascPeople,
...query
}));
}
});
componentDidMount() {
this.search();
}
onHeaderClickHandler(sort) {
@@ -60,19 +76,66 @@ class CommunityContainer extends Component {
this.search({page});
}
getTabContent(searchValue, props) {
const {community, data} = props;
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
if (activeTab === 'people') {
return (
<People
isFetching={community.isFetchingPeople}
commenters={community.accounts}
searchValue={searchValue}
error={community.errorPeople}
totalPages={community.totalPagesPeople}
page={community.pagePeople}
onKeyDown={this.onKeyDownHandler}
onChange={this.onChangeHandler}
onHeaderClickHandler={this.onHeaderClickHandler}
onNewPageHandler={this.onNewPageHandler}
/>
);
}
return (
<div>
<FlaggedAccounts
commenters={data.users}
isFetching={data.loading}
error={data.error}
showBanUserDialog={props.showBanUserDialog}
approveUser={props.approveUser}
suspendUser={props.suspendUser}
showSuspendUserDialog={props.showSuspendUserDialog}
/>
<BanUserDialog
open={community.banDialog}
user={community.user}
handleClose={props.hideBanUserDialog}
handleBanUser={props.banUser}
/>
<SuspendUserDialog
open={community.suspendDialog}
handleClose={props.hideSuspendUserDialog}
user={community.user}
suspendUser={props.suspendUser}
/>
</div>
);
}
render() {
const {searchValue} = this.state;
const {community} = this.props;
const tab = this.getTabContent(searchValue, this.props);
return (
<Community
searchValue={searchValue}
commenters={community.commenters}
isFetching={community.isFetching}
error={community.error}
totalPages={community.totalPages}
page={community.page}
{...this}
/>
<div>
<CommunityMenu />
<div>
{ tab }
</div>
</div>
);
}
}
@@ -81,4 +144,18 @@ const mapStateToProps = state => ({
community: state.community.toJS()
});
export default connect(mapStateToProps)(CommunityContainer);
const mapDispatchToProps = dispatch => ({
fetchAccounts: query => dispatch(fetchAccounts(query)),
showBanUserDialog: (user) => dispatch(showBanUserDialog(user)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
showSuspendUserDialog: (user) => dispatch(showSuspendUserDialog(user)),
hideSuspendUserDialog: () => dispatch(hideSuspendUserDialog())
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
modUserFlaggedQuery,
banUser,
setUserStatus,
suspendUser
)(CommunityContainer);
@@ -0,0 +1,9 @@
import React from 'react';
const CommunityLayout = props => (
<div>
{props.children}
</div>
);
export default CommunityLayout;
@@ -0,0 +1,44 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
import styles from './Community.css';
import Loading from './Loading';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import User from './components/User';
const FlaggedAccounts = ({...props}) => {
const {commenters, isFetching} = props;
const hasResults = !isFetching && commenters && !!commenters.length;
// if (commenter.status === 'PENDING' && commenter.actions.length > 0) {
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{ isFetching && <Loading /> }
{
hasResults
? commenters.map((commenter, index) => {
return <User
user={commenter}
key={index}
index={index}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
approveUser={props.approveUser}
suspendUser={props.suspendUser}
/>;
})
: <EmptyCard>{lang.t('community.no-flagged-accounts')}</EmptyCard>
}
</div>
</div>
);
};
export default FlaggedAccounts;
@@ -29,7 +29,7 @@ const tableHeaders = [
}
];
const Community = ({isFetching, commenters, ...props}) => {
const People = ({isFetching, commenters, ...props}) => {
const hasResults = !isFetching && !!commenters.length;
return (
<div className={styles.container}>
@@ -58,7 +58,7 @@ const Community = ({isFetching, commenters, ...props}) => {
hasResults
? <Table
headers={tableHeaders}
data={commenters}
commenters={commenters}
onHeaderClickHandler={props.onHeaderClickHandler}
/>
: <EmptyCard>{lang.t('community.no-results')}</EmptyCard>
@@ -73,4 +73,4 @@ const Community = ({isFetching, commenters, ...props}) => {
);
};
export default Community;
export default People;
@@ -77,4 +77,4 @@ class Table extends Component {
}
}
export default connect(state => ({commenters: state.community.get('commenters')}))(Table);
export default connect(state => ({commenters: state.community.get('accounts')}))(Table);
@@ -0,0 +1,24 @@
import React from 'react';
import styles from '../Community.css';
import BanUserButton from '../../../components/BanUserButton';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap';
const ActionButton = ({type = '', user, ...props}) => {
if (type === 'BAN') {
return <BanUserButton icon='not interested' user={user} onClick={() => props.showBanUserDialog(user)} />;
}
return (
<Button
className={`${type.toLowerCase()} ${styles.actionButton}`}
cStyle={type.toLowerCase()}
icon={menuActionsMap[type].icon}
onClick={() => {
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showSuspendUserDialog({user: user});
}}
>{menuActionsMap[type].text}</Button>
);
};
export default ActionButton;
@@ -0,0 +1,164 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 500px;
top: 50%;
transform: translateY(-50%);
height: 184px;
padding: 20px;
h2 {
color: black;
font-size: 1.76em;
font-weight: 500;
margin: 0;
}
h3 {
color: black;
font-size: 1.4em;
font-weight: 500;
margin: 0;
}
}
.textField {
margin-top: 15px;
}
.textField label {
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
}
.textField input {
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
}
.footer {
margin: 20px auto 10px;
text-align: center;
}
.footer span {
display: block;
margin-bottom: 5px;
}
.footer a {
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
}
.socialConnections {
margin-bottom: 20px;
}
.signInButton {
margin-top: 10px;
}
.close {
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
}
.close:hover {
color: #6b6b6b;
}
input.error{
border: solid 2px #f44336;
}
.errorMsg, .hint {
color: grey;
font-weight: 600;
padding: 3px 0 16px;
}
.alert {
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
.alert--success {
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
}
.alert--error {
background: #FFEBEE;
color: #B71C1C;
}
.userBox a {
color: #2c69b6;
cursor: pointer;
margin: 0px;
}
.attention {
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
}
.action {
margin-top: 15px;
}
.passwordRequestSuccess {
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
}
.passwordRequestFailure {
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
}
.cancel {
margin-right: 10px;
width: 47%;
}
.ban {
width: 47%;
}
.buttons {
margin: 20px 0;
}
@@ -0,0 +1,54 @@
import React, {PropTypes} from 'react';
import {Dialog} from 'coral-ui';
import styles from './BanUserDialog.css';
import Button from 'coral-ui/components/Button';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
<Dialog
className={styles.dialog}
id="banuserDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={lang.t('community.ban_user')}>
<span className={styles.close} onClick={handleClose}>×</span>
<div>
<div className={styles.header}>
<h2>{lang.t('community.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{lang.t('community.are_you_sure', user.username)}</h3>
<i>{lang.t('community.note')}</i>
</div>
<div className={styles.buttons}>
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
{lang.t('community.cancel')}
</Button>
<Button
cStyle="black" className={styles.ban}
onClick={() => {
handleBanUser({userId: user.id}).then(() => {
handleClose();
});
}}
raised>
{lang.t('community.yes_ban_user')}
</Button>
</div>
</div>
</Dialog>
);
BanUserDialog.propTypes = {
handleBanUser: PropTypes.func.isRequired,
handleClose: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
};
export default BanUserDialog;
@@ -0,0 +1,31 @@
import React from 'react';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
import {Link} from 'react-router';
const lang = new I18n(translations);
const CommunityMenu = () => {
const flaggedPath = '/admin/community/flagged';
const peoplePath = '/admin/community/people';
return (
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div>
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
{lang.t('community.flaggedaccounts')}
</Link>
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
{lang.t('community.people')}
</Link>
</div>
</div>
</div>
);
};
export default CommunityMenu;
@@ -0,0 +1,115 @@
import React, {Component, PropTypes} from 'react';
import {Dialog, Button} from 'coral-ui';
import styles from './SuspendUserDialog.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
const stages = [
{
title: 'suspenduser.title_0',
description: 'suspenduser.description_0',
options: {
'j': 'suspenduser.no_cancel',
'k': 'suspenduser.yes_suspend'
}
},
{
title: 'suspenduser.title_1',
description: 'suspenduser.description_1',
options: {
'j': 'bandialog.cancel',
'k': 'suspenduser.send'
}
}
];
class SuspendUserDialog extends Component {
state = {email: '', stage: 0}
static propTypes = {
stage: PropTypes.number,
handleClose: PropTypes.func.isRequired,
suspendUser: PropTypes.func.isRequired
}
componentDidMount() {
this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')});
}
/*
* When an admin clicks to suspend a user a dialog is shown, this function
* handles the possible actions for that dialog.
*/
onActionClick = (stage, menuOption) => () => {
const {suspendUser, user} = this.props;
const {stage} = this.state;
const cancel = this.props.handleClose;
const next = () => this.setState({stage: stage + 1});
const suspend = () => {
suspendUser({userId: user.user.id, message: this.state.email})
.then(() => {
this.props.handleClose();
});
};
const suspendModalActions = [
[ cancel, next ],
[ cancel, suspend ]
];
return suspendModalActions[stage][menuOption]();
}
onEmailChange = (e) => {
this.setState({email: e.target.value});
}
render () {
const {open, handleClose} = this.props;
const {stage} = this.state;
return <Dialog
className={styles.suspendDialog}
id="suspendUserDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={lang.t('suspenduser.title')}>
<div className={styles.title}>
{lang.t(stages[stage].title, lang.t('suspenduser.username'))}
</div>
<div className={styles.container}>
<div className={styles.description}>
{lang.t(stages[stage].description, lang.t('suspenduser.username'))}
</div>
{
stage === 1 &&
<div className={styles.writeContainer}>
<div className={styles.emailMessage}>{lang.t('suspenduser.write_message')}</div>
<div className={styles.emailContainer}>
<textarea
rows={5}
className={styles.emailInput}
value={this.state.email}
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)}>
{lang.t(stages[stage].options[key], lang.t('suspenduser.username'))}
</Button>
))}
</div>
</div>
</Dialog>;
}
}
export default SuspendUserDialog;
@@ -0,0 +1,90 @@
import React from 'react';
import styles from '../Community.css';
import ActionButton from './ActionButton';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../../translations.json';
const lang = new I18n(translations);
// Render a single user for the list
const User = props => {
const {user, modActionButtons} = props;
let userStatus = user.status;
// 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}>
<span>
{user.username}
</span>
<ActionButton
className={styles.banButton}
type='BAN'
user={user}
showBanUserDialog={props.showBanUserDialog}
/>
</div>
</div>
<div className={styles.itemBody}>
<div className={styles.body}>
<div className={styles.flaggedByCount}>
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{lang.t('community.flags')}({ user.actions.length })</span>:
{ user.action_summaries.map(
(action, i ) => {
return <span className={styles.flaggedBy} key={i}>
{lang.t(`community.${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}>
{lang.t(`community.${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 && action.user.username}: {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}
suspendUser={props.suspendUser}
showSuspendUserDialog={props.showSuspendUserDialog}
/>
)}
</div>
</div>
</div>
</div>
</li>;
};
export default User;
@@ -0,0 +1,339 @@
@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;
display: flex;
align-items: center;
font-size: 13px;
span {
margin-left: 7px;
}
}
@media (--big-viewport) {
.tab {
flex: none;
}
}
.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;
}
}
}
.header {
background-color: #2c2c2c;
color: white;
margin-bottom: -1px;
.settingsButton {
i {
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
}
}
.moderateAsset {
a {
-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;
}
&:first-child {
text-align: left;
}
&:nth-child(2) {
text-align: center;
}
&:last-child {
text-align: right;
}
}
}
}
@custom-media --big-viewport (min-width: 780px);
.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%;
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 {
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: 40px 18px;
box-sizing: border-box;
}
.itemHeader {
display: flex;
align-items: center;
justify-content: space-between;
.author {
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;
font-weight: 300;
}
.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;
}
.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;
&:hover {
text-decoration: underline;
opacity: .9;
cursor: pointer;
}
}
}
}
.flagBox {
max-width: 480px;
border-top: 1px solid rgba(66, 66, 66, 0.12);
h3 {
font-size: 14px;
margin: 0;
font-weight: 500;
}
}
@@ -24,10 +24,30 @@
margin-bottom: 20px;
align-items: flex-start;
min-height: 100px;
max-width: 600px;
h3 {
margin: 0;
}
.actions {
display: inline-block;
width: 100%;
.copiedText {
display: inline-block;
color: #00796b;
padding: 12px;
font-size: 14px;
float: right;
}
.copyButton {
display: inline-block;
width: 200px;
float: right;
}
}
}
.settingsError {
@@ -41,7 +61,9 @@
.settingsHeader {
margin-top: 3px;
margin-bottom: 10px;
margin-bottom: 7px;
font-size: 18px;
font-weight: 500;
}
.disabledSettingText {
@@ -51,18 +73,12 @@
.configSettingInfoBox {
min-height: 100px;
margin-bottom: 20px;
cursor: pointer;
width: auto;
height: auto;
text-align: left;
overflow: visible;
}
.configSettingInfoBox p {
font-size: 12px;
bottom: 0;
}
.configSettingEmbed {
border: 1px solid #ccc;
border-radius: 4px;
@@ -73,6 +89,11 @@
.configTimeoutSelect {
display: inline-block;
margin-left: 20px;
i { /* fix for firefox and react-mdl-selectfield@0.2.0 */
padding: 20px 0;
vertical-align: top;
}
}
.charCountTexfield {
@@ -81,6 +102,8 @@
border-color: #ccc;
border-style: solid;
border-width: 0px 0px 1px 0px;
font-size: 14px;
text-align: center;
}
.charCountTexfieldEnabled {
@@ -96,29 +119,18 @@
color: white;
}
.copiedText {
color: #00796b;
float: right;
padding: 12px;
font-size: 14px;
}
.copyButton {
float: right;
width: 200px;
}
.embedInput {
border-radius: 3px;
border: 1px solid #ccc;
width: 100%;
display: block;
width: 90%;
vertical-align: middle;
margin-bottom: 10px;
color: #555;
padding: 14px;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
min-height: 175px;
font-size: 14px;
letter-spacing: 0.03em;
resize: none;
}
#bannedWordlist, #suspectWordlist {
@@ -167,6 +179,28 @@
.content {
display: inline-block;
padding-left: 30px;
padding: 0px 30px;
box-sizing: border-box;
width: 100%;
}
}
.Configure {
p {
line-height: 1.2;
max-width: 550px;
}
.wrapper {
width: 100%;
}
.descriptionBox {
margin-top: 15px;
input {
height: 150px;
}
}
}
@@ -10,15 +10,19 @@ const lang = new I18n(translations);
const Domainlist = ({domains, onChangeDomainlist}) => {
return (
<Card id={styles.domainlist} className={styles.configSetting}>
<h3>{lang.t('configure.domain-list-title')}</h3>
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
<div className={styles.wrapper}>
<div className={styles.settingsHeader}>{lang.t('configure.domain-list-title')}</div>
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
<div className={styles.wrapper}>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
</div>
</div>
</Card>
);
};
@@ -45,13 +45,19 @@ class EmbedLink extends Component {
`.trim();
return (
<Card shadow="2" className={styles.configSetting}>
<h3>Embed Comment Stream</h3>
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
<div className={styles.wrapper}>
<div className={styles.settingsHeader}>Embed Comment Stream</div>
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<div className={styles.actions}>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>
{this.state.copied && 'Copied!'}
</div>
</div>
</div>
</Card>
);
}
@@ -16,6 +16,11 @@ const updateEmailConfirmation = (updateSettings, verify) => () => {
updateSettings({requireEmailConfirmation: !verify});
};
const updatePremodLinksEnable = (updateSettings, premodLinks) => () => {
const premodLinksEnable = !premodLinks;
updateSettings({premodLinksEnable});
};
const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
// just putting this here for shorthand below
@@ -23,7 +28,7 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
const off = styles.disabledSetting;
return (
<div>
<div className={styles.Configure}>
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
<div className={styles.action}>
<Checkbox
@@ -50,6 +55,19 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
</p>
</div>
</Card>
<Card className={`${styles.configSetting} ${settings.premodLinksEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updatePremodLinksEnable(updateSettings, settings.premodLinksEnable)}
checked={settings.premodLinksEnable} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.enable-premod-links')}</div>
<p>
{lang.t('configure.enable-premod-links-text')}
</p>
</div>
</Card>
<Wordlist
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
@@ -3,8 +3,9 @@ import {SelectField, Option} from 'react-mdl-selectfield';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import styles from './Configure.css';
import {Textfield, Checkbox} from 'react-mdl';
import {Card, Icon} from 'coral-ui';
import {Checkbox, Textfield} from 'react-mdl';
import {Card, Icon, TextArea} from 'coral-ui';
import MarkdownEditor from 'coral-admin/src/components/MarkdownEditor';
const TIMESTAMPS = {
weeks: 60 * 60 * 24 * 7,
@@ -32,11 +33,15 @@ const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
updateSettings({infoBoxEnable});
};
const updateInfoBoxContent = (updateSettings) => (event) => {
const infoBoxContent = event.target.value;
const updateInfoBoxContent = (updateSettings) => (value) => {
const infoBoxContent = value;
updateSettings({infoBoxContent});
};
const updateAutoClose = (updateSettings, autoCloseStream) => () => {
updateSettings({autoCloseStream});
};
const updateClosedMessage = (updateSettings) => (event) => {
const closedMessage = event.target.value;
updateSettings({closedMessage});
@@ -64,7 +69,7 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
const off = styles.disabledSetting;
return (
<div>
<div className={styles.Configure}>
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
@@ -79,7 +84,9 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
value={settings.charCount}
disabled={settings.charCountEnable ? '' : 'disabled'}
/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
@@ -92,41 +99,46 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
</p>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</div>
<div className={styles.content}>
{lang.t('configure.include-comment-stream')}
<p>
<div className={styles.settingsHeader}>
{lang.t('configure.include-comment-stream')}
</div>
<p className={settings.infoBoxEnable ? '' : styles.disabledSettingText}>
{lang.t('configure.include-comment-stream-desc')}
</p>
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<div className={styles.content}>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</div>
<MarkdownEditor
className={styles.descriptionBox}
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
/>
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<div className={styles.content}>
{lang.t('configure.closed-comments-desc')}
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
<div className={styles.wrapper}>
<div className={styles.settingsHeader}>{lang.t('configure.closed-stream-settings')}</div>
<p>{lang.t('configure.closed-comments-desc')}</p>
<div>
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
<TextArea className={styles.descriptionBox}
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
/>
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
<div className={styles.action}>
<Checkbox
onChange={updateAutoClose(updateSettings, !settings.autoCloseStream)}
checked={settings.autoCloseStream} />
</div>
<div className={styles.content}>
{lang.t('configure.close-after')}
<br />
@@ -149,6 +161,7 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
</div>
</div>
</Card>
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
</div>
);
};
@@ -14,19 +14,20 @@ const updateCustomCssUrl = (updateSettings) => (event) => {
const TechSettings = ({settings, onChangeDomainlist, updateSettings}) => {
return (
<div>
<div className={styles.Configure}>
<Domainlist
domains={settings.domains.whitelist}
onChangeDomainlist={onChangeDomainlist} />
<EmbedLink />
<Card className={styles.configSetting}>
<h3>{lang.t('configure.custom-css-url')}</h3>
<p>{lang.t('configure.custom-css-url-desc')}</p>
<br />
<input
className={styles.customCSSInput}
value={settings.customCssUrl}
onChange={updateCustomCssUrl(updateSettings)} />
<div className={styles.wrapper}>
<div className={styles.settingsHeader}>{lang.t('configure.custom-css-url')}</div>
<p>{lang.t('configure.custom-css-url-desc')}</p>
<input
className={styles.customCSSInput}
value={settings.customCssUrl}
onChange={updateCustomCssUrl(updateSettings)} />
</div>
</Card>
</div>
);
@@ -8,25 +8,29 @@ import {Card} from 'coral-ui';
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
<div>
<Card id={styles.bannedWordlist} className={styles.configSetting}>
<h3>{lang.t('configure.banned-words-title')}</h3>
<div className={styles.settingsHeader}>{lang.t('configure.banned-words-title')}</div>
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
<TagsInput
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('banned', tags)}
/>
<div className={styles.wrapper}>
<TagsInput
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('banned', tags)}
/>
</div>
</Card>
<Card id={styles.suspectWordlist} className={styles.configSetting}>
<h3>{lang.t('configure.suspect-words-title')}</h3>
<div className={styles.settingsHeader}>{lang.t('configure.suspect-words-title')}</div>
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
<TagsInput
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('suspect', tags)} />
<div className={styles.wrapper}>
<TagsInput
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('suspect', tags)} />
</div>
</Card>
</div>
);
@@ -0,0 +1,49 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import styles from './Widget.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
const lang = new I18n(translations);
const ActivityWidget = ({assets}) => {
return (
<div className={styles.widget}>
<h2 className={styles.heading}>Articles with the most conversations</h2>
<div className={styles.widgetHead}>
<p>{lang.t('streams.article')}</p>
<p>{lang.t('dashboard.comment_count')}</p>
</div>
<div className={styles.widgetTable}>
{
assets.length
? assets.map(asset => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{asset.commentCount}</p>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</Link>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{lang.t('dashboard.no_activity')}</div>
}
</div>
</div>
);
};
ActivityWidget.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
commentCount: PropTypes.number,
author: PropTypes.string,
created_at: PropTypes.string
})).isRequired
};
export default ActivityWidget;
@@ -7,3 +7,28 @@
font-size: 1.5rem;
font-weight: bold;
}
.autoUpdate {
background-color: #d5d5d5;
padding: 3px 10px 10px 10px;
margin-bottom: 0;
i {
position: relative;
top: 7px;
}
b {
float: right;
border-radius: 20px;
cursor: pointer;
background-color: #c0c0c0;
width: 30px;
height: 30px;
text-align: center;
top: 4px;
position: relative;
line-height: 1.7em;
font-size: 1.3em;
}
}
@@ -4,25 +4,33 @@ import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import {getMetrics} from 'coral-admin/src/graphql/queries';
import FlagWidget from './FlagWidget';
import LikeWidget from './LikeWidget';
import ActivityWidget from './ActivityWidget';
import CountdownTimer from 'coral-admin/src/components/CountdownTimer';
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
import {Spinner} from 'coral-ui';
class Dashboard extends React.Component {
reloadData = () => {
this.props.data.refetch();
}
render () {
if (this.props.data && this.props.data.loading) {
return <Spinner />;
}
const {data: {assetsByLike, assetsByFlag}} = this.props;
const {data: {assetsByActivity, assetsByFlag}} = this.props;
return (
<div className={styles.Dashboard}>
<FlagWidget assets={assetsByFlag} />
<LikeWidget assets={assetsByLike} />
<div>
<CountdownTimer handleTimeout={this.reloadData} />
<div className={styles.Dashboard}>
<FlagWidget assets={assetsByFlag} />
<ActivityWidget assets={assetsByActivity} />
</div>
</div>
);
}
@@ -1,4 +1,4 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import styles from './Widget.css';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -6,50 +6,50 @@ import translations from 'coral-admin/src/translations';
const lang = new I18n(translations);
const FlagWidget = (props) => {
const {assets} = props;
const FlagWidget = ({assets}) => {
return (
<div className={styles.widget}>
<h2 className={styles.heading}>Articles with the most flags</h2>
<table className={styles.widgetTable}>
<thead className={styles.widgetHead}>
<tr>
<th>{lang.t('streams.article')}</th>
<th>{lang.t('dashboard.flags')}</th>
</tr>
</thead>
<tbody>
{
assets.length
? assets.map(asset => {
const flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
return (
<tr className={styles.rowLinkify} key={asset.id}>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<p className={styles.assetTitle}>{asset.title}</p>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</Link>
</td>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
</Link>
</td>
</tr>
);
})
: <tr className={styles.rowLinkify}><td colSpan="2">{lang.t('dashboard.no_flags')}</td></tr>
}
{ /* rows in a table with a fixed height will expand and ignore height.
this extra row will expand to fill the extra space. */
assets.length < 10 ? <tr></tr> : null
}
</tbody>
</table>
<div className={styles.widgetHead}>
<p>{lang.t('streams.article')}</p>
<p>{lang.t('dashboard.flags')}</p>
</div>
<div className={styles.widgetTable}>
{
assets.length
? assets.map(asset => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
}
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</Link>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{lang.t('dashboard.no_flags')}</div>
}
</div>
</div>
);
};
FlagWidget.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
action_summaries: PropTypes.array,
author: PropTypes.string,
created_at: PropTypes.string
})).isRequired
};
export default FlagWidget;
@@ -1,4 +1,4 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import styles from './Widget.css';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -6,51 +6,46 @@ import translations from 'coral-admin/src/translations';
const lang = new I18n(translations);
const LikeWidget = (props) => {
const {assets} = props;
const LikeWidget = ({assets}) => {
return (
<div className={styles.widget}>
<h2 className={styles.heading}>Articles with the most likes</h2>
<table className={styles.widgetTable}>
<thead className={styles.widgetHead}>
<tr>
<th>{lang.t('streams.article')}</th>
<th>{lang.t('modqueue.likes')}</th>
</tr>
</thead>
<tbody>
{
assets.length
? assets.map(asset => {
const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary');
return (
<tr className={styles.rowLinkify} key={asset.id}>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<p className={styles.assetTitle}>{asset.title}</p>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</Link>
</td>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
</Link>
</td>
</tr>
);
})
: <tr className={styles.rowLinkify}><td colSpan="2">{lang.t('dashboard.no_likes')}</td></tr>
}
{ /* rows in a table with a fixed height will expand and ignore height.
this extra row will expand to fill the extra space. */
assets.length < 10 ? <tr></tr> : null
}
</tbody>
</table>
<div className={styles.widgetHead}>
<p>{lang.t('streams.article')}</p>
<p>{lang.t('modqueue.likes')}</p>
</div>
<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/flagged/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</Link>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{lang.t('dashboard.no_likes')}</div>
}
</div>
</div>
);
};
LikeWidget.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
action_summaries: PropTypes.array,
author: PropTypes.string,
created_at: PropTypes.string
})).isRequired
};
export default LikeWidget;
@@ -1,14 +1,17 @@
:root {
--row-height: 80px;
--row-height: 60px;
}
.widget {
box-sizing: border-box;
margin: 10px 5px 5px 5px;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
padding: 15px;
flex: 1;
background-color: white;
box-sizing: border-box;
}
.widget * {
box-sizing: border-box;
}
.heading {
@@ -19,21 +22,28 @@
}
.widgetTable {
width: 100%;
border-collapse: collapse;
user-select: none;
height: calc(var(--row-height) * 10);
}
.widgetTable thead th {
.widgetTable + div:after {
content: '';
clear: both;
display: block;
}
.widgetHead p {
color: rgb(35, 102, 223);
padding: 10px;
text-align: left;
text-transform: capitalize;
display: inline-block;
box-sizing: border-box;
margin-bottom: 0;
}
.widgetTable thead th:last-child {
width: 10%;
.widgetHead p:last-child {
float: right;
margin-right: 100px;
}
.rowLinkify {
@@ -41,6 +51,7 @@
border-bottom: 1px solid lightgrey;
color: #555;
height: var(--row-height);
padding: 10px;
}
.rowLinkify:last-child {
@@ -51,13 +62,22 @@
background-color: #f8f8f8;
}
.widgetTable tbody td {
padding: 10px;
.linkToAsset {
display: inline-block;
text-decoration: none;
}
.linkToAsset {
display: block;
.linkToModerate {
background-color: #e0e0e0;
padding: 10px 14px;
text-decoration: none;
color: black;
float: right;
margin-left: 15px;
}
.linkToModerate:hover {
background-color: #ccc;
}
.lede {
@@ -81,4 +101,6 @@
color: #555;
font-size: 1.3em;
font-weight: 400;
float: right;
margin-top: 7px;
}
@@ -2,13 +2,16 @@ import React, {Component} from 'react';
import {connect} from 'react-redux';
import Layout from '../components/ui/Layout';
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
import {fetchConfig} from '../actions/config';
import {FullLoading} from '../components/FullLoading';
import AdminLogin from '../components/AdminLogin';
class LayoutContainer extends Component {
componentWillMount () {
const {checkLogin} = this.props;
const {checkLogin, fetchConfig} = this.props;
checkLogin();
fetchConfig();
}
render () {
const {
@@ -16,15 +19,19 @@ class LayoutContainer extends Component {
loggedIn,
loadingUser,
loginError,
loginMaxExceeded,
passwordRequestSuccess
} = this.props.auth;
const {handleLogout} = this.props;
const {handleLogout, TALK_RECAPTCHA_PUBLIC} = this.props;
if (loadingUser) { return <FullLoading />; }
if (!isAdmin) {
return <AdminLogin
loginMaxExceeded={loginMaxExceeded}
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError} />;
}
if (isAdmin && loggedIn) { return <Layout handleLogout={handleLogout} {...this.props} />; }
@@ -33,12 +40,14 @@ class LayoutContainer extends Component {
}
const mapStateToProps = state => ({
auth: state.auth.toJS()
auth: state.auth.toJS(),
TALK_RECAPTCHA_PUBLIC: state.config.get('data').get('TALK_RECAPTCHA_PUBLIC', null)
});
const mapDispatchToProps = dispatch => ({
checkLogin: () => dispatch(checkLogin()),
handleLogin: (username, password) => dispatch(handleLogin(username, password)),
fetchConfig: () => dispatch(fetchConfig()),
handleLogin: (username, password, recaptchaResponse) => dispatch(handleLogin(username, password, recaptchaResponse)),
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
handleLogout: () => dispatch(logout())
});
@@ -3,6 +3,7 @@ import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import key from 'keymaster';
import isEqual from 'lodash/isEqual';
import styles from './components/styles.css';
import {modQueueQuery} from '../../graphql/queries';
import {banUser, setCommentStatus} from '../../graphql/mutations';
@@ -17,22 +18,88 @@ import ModerationQueue from './ModerationQueue';
import ModerationMenu from './components/ModerationMenu';
import ModerationHeader from './components/ModerationHeader';
import NotFoundAsset from './components/NotFoundAsset';
import ModerationKeysModal from '../../components/ModerationKeysModal';
class ModerationContainer extends Component {
state = {
selectedIndex: 0,
sort: 'REVERSE_CHRONOLOGICAL'
}
componentWillMount() {
const {toggleModal, singleView} = this.props;
this.props.fetchSettings();
key('s', () => singleView());
key('shift+/', () => toggleModal(true));
key('esc', () => toggleModal(false));
key('j', this.select(true));
key('k', this.select(false));
key('r', this.moderate(false));
key('t', this.moderate(true));
}
moderate = (accept) => () => {
const {acceptComment, rejectComment} = this.props;
const {selectedIndex} = this.state;
const comments = this.getComments();
const commentId = {commentId: comments[selectedIndex].id};
if (accept) {
acceptComment(commentId);
} else {
rejectComment(commentId);
}
}
getComments = () => {
const {data, route} = this.props;
const activeTab = route.path === ':id' ? 'premod' : route.path;
return data[activeTab];
}
select = (next) => () => {
if (next) {
this.setState(prevState =>
({
...prevState,
selectedIndex: prevState.selectedIndex < this.getComments().length - 1
? prevState.selectedIndex + 1 : prevState.selectedIndex
}));
} else {
this.setState(prevState =>
({
...prevState,
selectedIndex: prevState.selectedIndex > 0 ?
prevState.selectedIndex - 1 : prevState.selectedIndex
}));
}
}
selectSort = (sort) => {
this.setState({sort});
this.props.modQueueResort(sort);
}
componentWillUnmount() {
key.unbind('s');
key.unbind('shift+/');
key.unbind('esc');
key.unbind('j');
key.unbind('k');
key.unbind('r');
key.unbind('t');
}
componentDidUpdate(_, prevState) {
// If paging through using keybaord shortcuts, scroll the page to keep the selected
// comment in view.
if (prevState.selectedIndex !== this.state.selectedIndex) {
// the 'smooth' flag only works in FF as of March 2017
document.querySelector(`.${styles.selected}`).scrollIntoView({behavior: 'smooth'});
}
}
componentWillReceiveProps(nextProps) {
@@ -43,7 +110,7 @@ class ModerationContainer extends Component {
}
render () {
const {data, moderation, settings, assets, modQueueResort, ...props} = this.props;
const {data, moderation, settings, assets, onClose, ...props} = this.props;
const providedAssetId = this.props.params.id;
const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path;
@@ -66,6 +133,18 @@ class ModerationContainer extends Component {
}
const comments = data[activeTab];
let activeTabCount;
switch(activeTab) {
case 'premod':
activeTabCount = data.premodCount;
break;
case 'flagged':
activeTabCount = data.flaggedCount;
break;
case 'rejected':
activeTabCount = data.rejectedCount;
break;
}
return (
<div>
@@ -75,23 +154,37 @@ class ModerationContainer extends Component {
premodCount={data.premodCount}
rejectedCount={data.rejectedCount}
flaggedCount={data.flaggedCount}
modQueueResort={modQueueResort}
selectSort={this.selectSort}
sort={this.state.sort}
/>
<ModerationQueue
currentAsset={asset}
comments={comments}
activeTab={activeTab}
singleView={moderation.singleView}
selectedIndex={this.state.selectedIndex}
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={props.showBanUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
loadMore={props.loadMore}
assetId={providedAssetId}
sort={this.state.sort}
commentCount={activeTabCount}
/>
<BanUserDialog
open={moderation.banDialog}
user={moderation.user}
commentId={moderation.commentId}
handleClose={props.hideBanUserDialog}
handleBanUser={props.banUser}
showRejectedNote={moderation.showRejectedNote}
rejectComment={props.rejectComment}
/>
<ModerationKeysModal
open={moderation.modalOpen}
onClose={onClose}/>
</div>
);
}
@@ -109,7 +202,7 @@ const mapDispatchToProps = dispatch => ({
singleView: () => dispatch(singleView()),
updateAssets: assets => dispatch(updateAssets(assets)),
fetchSettings: () => dispatch(fetchSettings()),
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
});
@@ -1,15 +1,17 @@
import React, {PropTypes} from 'react';
import Comment from './components/Comment';
import styles from './components/styles.css';
import EmptyCard from '../../components/EmptyCard';
import {actionsMap} from './helpers/moderationQueueActionsMap';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
import LoadMore from './components/LoadMore';
const lang = new I18n(translations);
const ModerationQueue = ({comments, ...props}) => {
const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loadMore, activeTab, sort, ...props}) => {
return (
<div id="moderationList">
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
<ul style={{paddingLeft: 0}}>
{
comments.length
@@ -19,8 +21,10 @@ const ModerationQueue = ({comments, ...props}) => {
key={i}
index={i}
comment={comment}
commentType={props.activeTab}
commentType={activeTab}
selected={i === selectedIndex}
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
acceptComment={props.acceptComment}
@@ -31,11 +35,20 @@ const ModerationQueue = ({comments, ...props}) => {
: <EmptyCard>{lang.t('modqueue.emptyqueue')}</EmptyCard>
}
</ul>
<LoadMore
comments={comments}
loadMore={loadMore}
sort={sort}
tab={activeTab}
showLoadMore={comments.length < commentCount}
assetId={props.assetId}
/>
</div>
);
};
ModerationQueue.propTypes = {
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
@@ -19,51 +19,54 @@ const lang = new I18n(translations);
const Comment = ({actions = [], ...props}) => {
const links = linkify.getMatches(props.comment.body);
const linkText = links ? links.map(link => link.raw) : [];
const actionSummaries = props.comment.action_summaries;
return (
<li tabIndex={props.index}
className={`mdl-card mdl-shadow--2dp ${styles.Comment} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>{props.comment.user.name}</span>
<span className={styles.created}>
<div className={styles.author}>
<span>
{props.comment.user.name}
</span>
<span className={styles.created}>
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
<BanUserButton user={props.comment.user} onClick={() => props.showBanUserDialog(props.comment.user, props.comment.id)} />
<CommentType type={props.commentType} />
</div>
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={props.comment.user}
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
/>
)}
<BanUserButton user={props.comment.user} onClick={() => props.showBanUserDialog(props.comment.user, props.comment.id, props.comment.status !== 'REJECTED')} />
<CommentType type={props.commentType} />
</div>
{props.comment.user.status === 'banned' ?
<span className={styles.banned}>
<Icon name='error_outline'/>
<Icon name='error_outline'/>
{lang.t('comment.banned_user')}
</span>
</span>
: null}
</div>
</div>
{!props.currentAsset && (
<div className={styles.moderateArticle}>
Story: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate &rarr;</Link>
</div>
)}
<div className={styles.moderateArticle}>
Story: {props.comment.asset.title}
{!props.currentAsset && (
<Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate &rarr;</Link>
)}
</div>
<div className={styles.itemBody}>
<p className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter searchWords={props.suspectWords} textToHighlight={props.comment.body}/>
</Linkify>
<Highlighter
searchWords={[...props.suspectWords, ...props.bannedWords, ...linkText]}
textToHighlight={props.comment.body} />
</p>
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={props.comment.user}
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
/>
)}
</div>
</div>
</div>
</div>
{actionSummaries && <FlagBox actionSummaries={actionSummaries} />}
@@ -75,6 +78,7 @@ Comment.propTypes = {
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
@@ -90,9 +94,4 @@ Comment.propTypes = {
})
};
const linkStyles = {
backgroundColor: 'rgb(255, 219, 135)',
padding: '1px 2px'
};
export default Comment;
@@ -4,12 +4,12 @@
top: 11px;
color: white;
background: grey;
padding: 2px 13px;
font-size: 14px;
height: 32px;
box-sizing: border-box;
line-height: 29px;
padding-left: 26px;
padding: 2px 8px 2px 26px;
border-radius: 2px;
font-size: 12px;
i {
font-size: 14px;
@@ -0,0 +1,31 @@
import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import styles from './styles.css';
const LoadMore = ({comments, loadMore, sort, tab, assetId, showLoadMore}) =>
<div className={styles.loadMoreContainer}>
{
showLoadMore && <Button
className={styles.loadMore}
onClick={() =>
loadMore({
cursor: comments[comments.length - 1].created_at,
sort,
tab,
asset_id: assetId
})}>
Load More
</Button>
}
</div>;
LoadMore.propTypes = {
comments: PropTypes.array.isRequired,
loadMore: PropTypes.func.isRequired,
sort: PropTypes.oneOf(['CHRONOLOGICAL', 'REVERSE_CHRONOLOGICAL']).isRequired,
tab: PropTypes.oneOf(['rejected', 'premod', 'flagged']).isRequired,
assetId: PropTypes.string,
showLoadMore: PropTypes.bool.isRequired
};
export default LoadMore;
@@ -10,9 +10,9 @@ const ModerationHeader = props => (
props.asset ?
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
<Link className="mdl-tabs__tab" to="/admin/moderate">All Streams</Link>
<a className="mdl-tabs__tab">
{props.asset.title}
<a href={props.asset.url} className={styles.settingsButton}><Icon name="settings"/></a>
<a className="mdl-tabs__tab" href={props.asset.url}>
<span>{props.asset.title}</span>
<Icon className={styles.settingsButton} name="open_in_new"/>
</a>
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
</div>
@@ -1,4 +1,4 @@
import React, {PropTypes, Component} from 'react';
import React, {PropTypes} from 'react';
import CommentCount from './CommentCount';
import styles from './styles.css';
import {SelectField, Option} from 'react-mdl-selectfield';
@@ -8,57 +8,63 @@ import {Link} from 'react-router';
const lang = new I18n(translations);
class ModerationMenu extends Component {
state = {
sort: 'REVERSE_CHRONOLOGICAL',
}
const ModerationMenu = (
{asset, premodCount, rejectedCount, flaggedCount, selectSort, sort}
) => {
const premodPath = asset
? `/admin/moderate/premod/${asset.id}`
: '/admin/moderate/premod';
const rejectPath = asset
? `/admin/moderate/rejected/${asset.id}`
: '/admin/moderate/rejected';
const flagPath = asset
? `/admin/moderate/flagged/${asset.id}`
: '/admin/moderate/flagged';
static propTypes = {
premodCount: PropTypes.number.isRequired,
rejectedCount: PropTypes.number.isRequired,
flaggedCount: PropTypes.number.isRequired,
asset: PropTypes.shape({
id: PropTypes.string
})
}
selectSort = (sort) => {
this.setState({sort});
this.props.modQueueResort(sort);
}
render() {
const {asset, premodCount, rejectedCount, flaggedCount} = this.props;
const premodPath = asset ? `/admin/moderate/premod/${asset.id}` : '/admin/moderate/premod';
const rejectPath = asset ? `/admin/moderate/rejected/${asset.id}` : '/admin/moderate/rejected';
const flagPath = asset ? `/admin/moderate/flagged/${asset.id}` : '/admin/moderate/flagged';
return (
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div className={styles.tabBarPadding}/>
<div>
<Link to={premodPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
{lang.t('modqueue.premod')} <CommentCount count={premodCount} />
</Link>
<Link to={rejectPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
{lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
</Link>
<Link to={flagPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
{lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
</div>
<SelectField
className={styles.selectField}
label='Sort'
value={this.state.sort}
onChange={sort => this.selectSort(sort)}>
<Option value={'REVERSE_CHRONOLOGICAL'}>Newest First</Option>
<Option value={'CHRONOLOGICAL'}>Oldest First</Option>
</SelectField>
return (
<div className="mdl-tabs">
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div className={styles.tabBarPadding} />
<div>
<Link
to={premodPath}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.premod')} <CommentCount count={premodCount} />
</Link>
<Link
to={flagPath}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
<Link
to={rejectPath}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
</Link>
</div>
<SelectField
className={styles.selectField}
label="Sort"
value={sort}
onChange={sort => selectSort(sort)}>
<Option value={'REVERSE_CHRONOLOGICAL'}>Newest First</Option>
<Option value={'CHRONOLOGICAL'}>Oldest First</Option>
</SelectField>
</div>
);
}
}
</div>
);
};
ModerationMenu.propTypes = {
premodCount: PropTypes.number.isRequired,
rejectedCount: PropTypes.number.isRequired,
flaggedCount: PropTypes.number.isRequired,
asset: PropTypes.shape({
id: PropTypes.string
})
};
export default ModerationMenu;
@@ -84,11 +84,10 @@ span {
margin-bottom: -1px;
.settingsButton {
i {
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
}
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
font-size: 16px;
}
.moderateAsset {
@@ -114,7 +113,15 @@ span {
}
&:nth-child(2) {
text-align: center;
span {
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 344px;
display: inline-block;
vertical-align: top;
}
}
&:last-child {
@@ -136,7 +143,7 @@ span {
display: none;
}
&.singleView .listItem.activeItem {
&.singleView .listItem.selected {
display: block;
height: 100%;
font-size: 1.5em;
@@ -147,7 +154,6 @@ span {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
@@ -164,18 +170,17 @@ span {
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 660px;
max-width: 700px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: box-shadow 200ms;
margin-top: 0;
padding: 4px 0 0;
transition: all 200ms;
padding: 10px 0 0;
min-height: 220px;
.container {
padding: 0 14px;
min-height: 220px;
min-height: 180px;
}
&:hover {
@@ -186,6 +191,11 @@ span {
border-bottom: none;
}
&.selected {
max-width: 720px;
max-height: 410px;
}
.context {
a {
color: #f36451;
@@ -195,11 +205,8 @@ span {
}
.sideActions {
position: absolute;
right: 0;
height: 100%;
top: 0;
padding: 100px 12px 65px;
box-sizing: border-box;
}
@@ -209,17 +216,21 @@ span {
justify-content: space-between;
.author {
font-weight: 300;
min-width: 230px;
display: flex;
align-items: center;
color: #262626;
font-size: 16px;
}
}
.itemBody {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 16px;
font-size: 14px;
line-height: 1.5;
font-weight: 300;
}
.avatar {
@@ -233,9 +244,11 @@ span {
}
.created {
color: #666;
font-size: 13px;
color: #262626;
font-size: 14px;
margin-left: 15px;
line-height: 1px;
font-weight: 300;
}
.actionButton {
@@ -246,10 +259,11 @@ span {
.body {
margin-top: 0px;
flex: 1;
font-size: 0.88em;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
font-size: 16px;
}
.flagged {
@@ -319,9 +333,12 @@ span {
.Comment {
.moderateArticle {
font-size: 15px;
margin-top: 14px;
font-size: 14px;
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
max-width: 500px;
a {
display: inline-block;
color: #063b9a;
@@ -381,3 +398,23 @@ span {
cursor: pointer;
}
}
.loadMoreContainer {
display: flex;
justify-content: center;
width: 100%;
};
.loadMore {
width: 100%;
text-align: center;
color: #FFF;
max-width: 660px;
margin-bottom: 30px;
background-color: #2376D8;
cursor: pointer;
}
.loadMore:hover {
background-color: #4399FF;
}
@@ -0,0 +1,192 @@
import React, {Component} from 'react';
import styles from './Stories.css';
import {connect} from 'react-redux';
import I18n from 'coral-framework/modules/i18n/i18n';
import {fetchAssets, updateAssetState} from 'coral-admin/src/actions/assets';
import translations from 'coral-admin/src/translations.json';
import {Link} from 'react-router';
import {Pager, Icon} from 'coral-ui';
import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import sortBy from 'lodash/sortBy';
const limit = 25;
class Stories extends Component {
state = {
search: '',
sort: 'desc',
filter: 'all',
statusMenus: {},
timer: null,
page: 0
}
componentDidMount () {
this.props.fetchAssets(0, limit, '', this.state.sortBy);
}
onSettingChange = (setting) => (e) => {
let options = this.state;
this.setState({[setting]: e.target.value});
options[setting] = e.target.value;
this.props.fetchAssets(0, limit, options.search, options.sort, options.filter);
}
onSearchChange = (e) => {
const search = e.target.value;
this.setState((prevState) => {
prevState.search = search;
clearTimeout(prevState.timer);
const fetchAssets = this.props.fetchAssets;
prevState.timer = setTimeout(() => {
fetchAssets(0, limit, search, this.state.sort, this.state.filter);
}, 350);
return prevState;
});
}
renderDate = (date) => {
const d = new Date(date);
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
}
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
if (statusMenuOpen) {
this.setState(prev => {
prev.statusMenus[id] = false;
return prev;
});
this.props.updateAssetState(id, closeStream ? Date.now() : null)
.then(() => {
const {search, sort, filter, page} = this.state;
this.props.fetchAssets(page, limit, search, sort, filter);
});
} else {
this.setState(prev => {
prev.statusMenus[id] = true;
return prev;
});
}
}
renderTitle = (title, {id}) => <Link to={`/admin/moderate/${id}`}>{title}</Link>
renderStatus = (closedAt, {id}) => {
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
const statusMenuOpen = this.state.statusMenus[id];
return <div className={styles.statusMenu}>
<div
className={closed ? styles.statusMenuClosed : styles.statusMenuOpen}
onClick={this.onStatusClick(closed, id, statusMenuOpen)}>
{!statusMenuOpen && <Icon className={styles.statusMenuIcon} name='keyboard_arrow_down'/>}
{closed ? lang.t('streams.closed') : lang.t('streams.open')}
</div>
{
statusMenuOpen &&
<div
className={!closed ? styles.statusMenuClosed : styles.statusMenuOpen}
onClick={this.onStatusClick(!closed, id, statusMenuOpen)}>
{!closed ? lang.t('streams.closed') : lang.t('streams.open')}
</div>
}
</div>;
}
onPageClick = (page) => {
this.setState({page});
const {search, sort, filter} = this.state;
this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter);
}
render () {
const {search, sort, filter} = this.state;
const {assets} = this.props;
const assetsIds = sortBy(assets.ids.map((id) => assets.byId[id]), 'publication_date');
if (this.state.sort === 'desc') {
assetsIds.reverse();
}
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
<input
type='text'
value={search}
className={styles.searchBoxInput}
onChange={this.onSearchChange}
placeholder={lang.t('streams.search')}/>
</div>
<div className={styles.optionHeader}>{lang.t('streams.filter-streams')}</div>
<div className={styles.optionDetail}>{lang.t('streams.stream-status')}</div>
<RadioGroup
name='status filter'
value={filter}
childContainer='div'
onChange={this.onSettingChange('filter')}
className={styles.radioGroup}
>
<Radio value='all'>{lang.t('streams.all')}</Radio>
<Radio value='open'>{lang.t('streams.open')}</Radio>
<Radio value='closed'>{lang.t('streams.closed')}</Radio>
</RadioGroup>
<div className={styles.optionHeader}>{lang.t('streams.sort-by')}</div>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}
className={styles.radioGroup}
>
<Radio value='desc'>{lang.t('streams.newest')}</Radio>
<Radio value='asc'>{lang.t('streams.oldest')}</Radio>
</RadioGroup>
</div>
{
assetsIds.length
? <div className={styles.mainContent}>
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
<TableHeader name="title" cellFormatter={this.renderTitle}>{lang.t('streams.article')}</TableHeader>
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
{lang.t('streams.pubdate')}
</TableHeader>
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
{lang.t('streams.status')}
</TableHeader>
</DataTable>
<Pager
totalPages={Math.ceil((assets.count || 0) / limit)}
page={this.state.page}
onNewPageHandler={this.onPageClick} />
</div>
: <EmptyCard>{lang.t('streams.empty_result')}</EmptyCard>
}
</div>
);
}
}
const mapStateToProps = ({assets}) => {
return {
assets: assets.toJS()
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchAssets: (...args) => {
dispatch(fetchAssets.apply(this, args));
},
updateAssetState: (...args) => dispatch(updateAssetState.apply(this, args))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Stories);
const lang = new I18n(translations);
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
import styles from './Streams.css';
import styles from './Stories.css';
import {connect} from 'react-redux';
import I18n from 'coral-framework/modules/i18n/i18n';
import {fetchAssets, updateAssetState} from '../../actions/assets';
@@ -12,7 +12,7 @@ import EmptyCard from 'coral-admin/src/components/EmptyCard';
const limit = 25;
class Streams extends Component {
class Stories extends Component {
state = {
search: '',
@@ -80,8 +80,8 @@ class Streams extends Component {
<div
className={closed ? styles.statusMenuClosed : styles.statusMenuOpen}
onClick={this.onStatusClick(closed, id, statusMenuOpen)}>
{closed ? lang.t('streams.closed') : lang.t('streams.open')}
{!statusMenuOpen && <Icon className={styles.statusMenuIcon} name='keyboard_arrow_down'/>}
{closed ? lang.t('streams.closed') : lang.t('streams.open')}
</div>
{
statusMenuOpen &&
@@ -182,6 +182,6 @@ const mapDispatchToProps = (dispatch) => {
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Streams);
export default connect(mapStateToProps, mapDispatchToProps)(Stories);
const lang = new I18n(translations);
@@ -4,6 +4,7 @@ fragment metrics on Asset {
url
author
created_at
commentCount
action_summaries {
type: __typename
actionCount
@@ -1,6 +1,7 @@
import {graphql} from 'react-apollo';
import SET_USER_STATUS from './setUserStatus.graphql';
import SET_COMMENT_STATUS from './setCommentStatus.graphql';
import SUSPEND_USER from './suspendUser.graphql';
export const banUser = graphql(SET_USER_STATUS, {
props: ({mutate}) => ({
@@ -9,11 +10,40 @@ export const banUser = graphql(SET_USER_STATUS, {
variables: {
userId,
status: 'BANNED'
}
},
refetchQueries: ['Users']
});
}}),
});
export const setUserStatus = graphql(SET_USER_STATUS, {
props: ({mutate}) => ({
approveUser: ({userId}) => {
return mutate({
variables: {
userId,
status: 'APPROVED'
},
refetchQueries: ['Users']
});
}
})
});
export const suspendUser = graphql(SUSPEND_USER, {
props: ({mutate}) => ({
suspendUser: ({userId, message}) => {
return mutate({
variables: {
userId,
message
},
refetchQueries: ['Users']
});
}
})
});
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
props: ({mutate}) => ({
acceptComment: ({commentId}) => {
@@ -22,7 +52,26 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'ACCEPTED'
},
refetchQueries: ['ModQueue']
updateQueries: {
ModQueue: (oldData) => {
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const rejected = oldData.rejected.filter(c => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
return {
...oldData,
premodCount,
flaggedCount,
rejectedCount,
premod,
flagged,
rejected,
};
}
}
});
},
rejectComment: ({commentId}) => {
@@ -31,7 +80,27 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'REJECTED'
},
refetchQueries: ['ModQueue']
updateQueries: {
ModQueue: (oldData) => {
const comment = oldData.premod.concat(oldData.flagged).filter(c => c.id === commentId)[0];
const rejected = [comment].concat(oldData.rejected);
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = oldData.rejectedCount + 1;
return {
...oldData,
premodCount,
flaggedCount,
rejectedCount,
premod,
flagged,
rejected
};
}
}
});
}
})
@@ -0,0 +1,7 @@
mutation suspendUser($userId: ID!, $message: String) {
suspendUser(id: $userId, message: $message) {
errors {
translation_key
}
}
}
@@ -1,6 +1,8 @@
import {graphql} from 'react-apollo';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
import METRICS from './metricsQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
@@ -14,7 +16,8 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
},
props: ({ownProps: {params: {id = null}}, data}) => ({
data,
modQueueResort: modQueueResort(id, data.fetchMore)
modQueueResort: modQueueResort(id, data.fetchMore),
loadMore: loadMore(data.fetchMore)
})
});
@@ -30,6 +33,50 @@ export const getMetrics = graphql(METRICS, {
}
});
export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => {
let statuses;
switch(tab) {
case 'premod':
statuses = ['PREMOD'];
break;
case 'flagged':
statuses = ['NONE', 'PREMOD'];
break;
case 'rejected':
statuses = ['REJECTED'];
break;
}
return fetchMore({
query: MOD_QUEUE_LOAD_MORE,
variables: {
limit,
cursor,
sort,
statuses,
asset_id
},
updateQuery: (oldData, {fetchMoreResult:{data:{comments}}}) => {
return {
...oldData,
[tab]: [
...oldData[tab],
...comments
]
};
}
});
};
export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY, {
options: ({params: {action_type = 'FLAG'}}) => {
return {
variables: {
action_type: action_type
}
};
}
});
export const modQueueResort = (id, fetchMore) => (sort) => {
return fetchMore({
query: MOD_QUEUE_QUERY,
@@ -0,0 +1,13 @@
#import "../fragments/commentView.graphql"
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort}) {
...commentView
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
}
@@ -4,7 +4,7 @@ query Metrics ($from: Date!, $to: Date!) {
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
...metrics
}
assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
...metrics
}
}
@@ -0,0 +1,26 @@
query Users ($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
}
}
}
}
-1
View File
@@ -7,7 +7,6 @@ import store from './services/store';
import App from './components/App';
import 'react-mdl/extra/material.css';
import 'react-mdl/extra/material.js';
render(
+9 -4
View File
@@ -23,8 +23,13 @@ export default function assets (state = initialState, action) {
}
const replaceAssets = (action, state) => {
const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
return state.set('byId', assets)
.set('count', action.count)
.set('ids', List(assets.keys()));
const assets = fromJS(action.assets.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {}));
return state
.set('byId', assets)
.set('count', action.count)
.set('ids', List(assets.keys()));
};
+7
View File
@@ -6,6 +6,7 @@ const initialState = Map({
user: null,
isAdmin: false,
loginError: null,
loginMaxExceeded: false,
passwordRequestSuccess: null
});
@@ -29,12 +30,18 @@ export default function auth (state = initialState, action) {
return initialState;
case actions.LOGIN_REQUEST:
return state.set('loginError', null);
case actions.LOGIN_SUCCESS:
return state.set('loginMaxExceeded', false).set('loginError', null);
case actions.LOGIN_FAILURE:
return state.set('loginError', action.message);
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return state.set('passwordRequestSuccess', null);
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.');
case actions.LOGIN_MAXIMUM_EXCEEDED:
return state
.set('loginMaxExceeded', true)
.set('loginError', action.message);
default :
return state;
}
+51 -21
View File
@@ -6,58 +6,88 @@ import {
FETCH_COMMENTERS_SUCCESS,
SORT_UPDATE,
SET_ROLE,
SET_COMMENTER_STATUS
SET_COMMENTER_STATUS,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_SUSPENDUSER_DIALOG,
HIDE_SUSPENDUSER_DIALOG
} from '../constants/community';
const initialState = Map({
community: Map(),
isFetching: false,
error: '',
commenters: [],
field: 'created_at',
asc: false,
totalPages: 0,
page: 0
isFetchingPeople: false,
errorPeople: '',
accounts: [],
fieldPeople: 'created_at',
ascPeople: false,
totalPagesPeople: 0,
pagePeople: 0,
user: Map({}),
banDialog: false,
suspendDialog: false
});
export default function community (state = initialState, action) {
switch (action.type) {
case FETCH_COMMENTERS_REQUEST :
return state
.set('isFetching', true);
.set('isFetchingPeople', true);
case FETCH_COMMENTERS_FAILURE :
return state
.set('isFetching', false)
.set('error', action.error);
.set('isFetchingPeople', false)
.set('errorPeople', action.error);
case FETCH_COMMENTERS_SUCCESS : {
const {commenters, type, ...rest} = action; // eslint-disable-line
const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
return state
.merge({
isFetching: false,
error: '',
isFetchingPeople: false,
errorPeople: '',
pagePeople: page,
countPeople: count,
limitPeople: limit,
totalPagesPeople: totalPages,
...rest
})
.set('commenters', commenters); // Sets to normal array
.set('accounts', accounts); // Sets to normal array
}
case SET_ROLE : {
const commenters = state.get('commenters');
const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].roles[0] = action.role;
return state.set('commenters', commenters.map(id => id));
return state.set('accounts', commenters.map(id => id));
}
case SET_COMMENTER_STATUS: {
const commenters = state.get('commenters');
const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].status = action.status;
return state.set('commenters', commenters.map(id => id));
return state.set('accounts', commenters.map(id => id));
}
case SORT_UPDATE :
return state
.set('field', action.sort.field)
.set('asc', !state.get('asc'));
.set('fieldPeople', action.sort.field)
.set('ascPeople', !state.get('ascPeople'));
case HIDE_BANUSER_DIALOG:
return state
.set('banDialog', false);
case SHOW_BANUSER_DIALOG:
return state
.merge({
user: Map(action.user),
banDialog: true
});
case HIDE_SUSPENDUSER_DIALOG:
return state
.set('suspendDialog', false);
case SHOW_SUSPENDUSER_DIALOG:
return state
.merge({
user: Map(action.user),
suspendDialog: true
});
default :
return state;
}
+16
View File
@@ -0,0 +1,16 @@
import {Map} from 'immutable';
import * as actions from '../actions/config';
const initialState = Map({
data: Map({})
});
export default function config (state = initialState, action) {
switch (action.type) {
case actions.CONFIG_UPDATED:
return state.set('data', Map(action.data));
default:
return state;
}
}
+3 -1
View File
@@ -4,6 +4,7 @@ import settings from './settings';
import community from './community';
import moderation from './moderation';
import install from './install';
import config from './config';
export default {
auth,
@@ -11,5 +12,6 @@ export default {
settings,
community,
moderation,
install
install,
config
};
@@ -19,6 +19,7 @@ export default function moderation (state = initialState, action) {
.merge({
user: Map(action.user),
commentId: action.commentId,
showRejectedNote: action.showRejectedNote,
banDialog: true
});
case actions.SET_ACTIVE_TAB:
+104 -48
View File
@@ -1,7 +1,8 @@
{
"en": {
"errors": {
"NOT_AUTHORIZED": "Your username or password is not recognized by our system."
"NOT_AUTHORIZED": "Your username or password is not recognized by our system.",
"LOGIN_MAXIMUM_EXCEEDED": "You have made too many unsuccessful password attempts. Please wait."
},
"community": {
"username_and_email": "Username and Email",
@@ -16,7 +17,21 @@
"active": "Active",
"banned": "Banned",
"banned-user": "Banned User",
"loading": "Loading results"
"loading": "Loading results",
"flags": "Flags",
"flaggedaccounts": "Flagged Usernames",
"people": "People",
"no-flagged-accounts": "The Account Flags queue is currently empty.",
"I don't like this username": "I don't like this username",
"This user is impersonating": "Impersonation",
"This looks like an ad/marketing": "Spam/Ads",
"This username is offensive": "Offensive",
"Other": "Other",
"ban_user": "Ban User?",
"are_you_sure": "Are you sure you would like to ban {0}?",
"note": "Note: Banning this user will not let them edit, comment or remove anything.",
"cancel": "Cancel",
"yes_ban_user": "Yes, Ban User"
},
"modqueue": {
"likes": "likes",
@@ -53,6 +68,7 @@
"copy": "Copy to Clipboard"
},
"configure": {
"closed-stream-settings": "Closed Stream Message",
"stream-settings": "Stream Settings",
"moderation-settings": "Moderation Settings",
"tech-settings": "Tech Settings",
@@ -63,9 +79,11 @@
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
"require-email-verification": "Require Email Verification",
"require-email-verification-text": "New Users must verify their email before commenting",
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream": "Include Comment Stream Description for Readers",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
"include-text": "Include your text here.",
"enable-premod-links": "Pre-Moderate Comments Containing Links",
"enable-premod-links-text": "Moderators must approve any comment containing a link before its published.",
"comment-settings": "Settings",
"embed-comment-stream": "Embed Stream",
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
@@ -78,8 +96,8 @@
"moderate": "Moderate",
"configure": "Configure",
"community": "Community",
"streams": "Streams",
"closed-comments-desc": "Write a message for closed threads",
"stories": "Stories",
"closed-comments-desc": "Write a message to be displayed when when your comment stream is closed and no longer accepting comments.",
"closed-comments-label": "Write a message...",
"hours": "Hours",
"days": "Days",
@@ -87,7 +105,7 @@
"close-after": "Close comments after",
"comment-count-header": "Limit Comment Length",
"comment-count-text-pre": "Comments will be limited to ",
"comment-count-text-post": " characters.",
"comment-count-text-post": " characters",
"comment-count-error": "Please enter a valid number.",
"domain-list-title": "Permitted Domains",
"domain-list-text": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)."
@@ -100,8 +118,9 @@
"yes_ban_user": "Yes, Ban User"
},
"suspenduser": {
"title_0": "We noticed you rejected a {0}",
"description_0": "Would you like to temporarily ban this user becuase of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}.",
"title": "Suspend a user",
"title_0": "We noticed you rejected a username",
"description_0": "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}.",
"title_1": "Notify the user of their temporary suspension",
"description_1": "Suspending this user will temporarily disable their account and hide all of their comments on the site.",
"no_cancel": "No, cancel",
@@ -110,13 +129,16 @@
"bio": "bio",
"username": "username",
"email_subject": "Your account has been suspended",
"email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.",
"email": "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please e-mail us if you have any questions or concerns.",
"write_message": "Write a message"
},
"dashboard": {
"next-update": "{0} minutes until next update.",
"auto-update": "Data automatically updates every five minutes or when you Reload.",
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
"no_likes": "There have been no likes in the last 5 minutes. All quiet.",
"flags": "Flags",
"no_activity": "There haven't been any comments anywhere in the last five minutes.",
"comment_count": "comments"
},
"streams": {
@@ -132,28 +154,58 @@
"sort-by": "Sort By",
"open": "Open",
"closed": "Closed",
"article": "Article",
"article": "Story",
"pubdate": "Publication Date",
"status": "Status"
"status": "Stream Status"
}
},
"es": {
"errors": {
"NOT_AUTHORIZED": "Acción no autorizada."
"NOT_AUTHORIZED": "Acción no autorizada.",
"LOGIN_MAXIMUM_EXCEEDED": "Ha realizado demasiados intentos fallidos de colocar la contraseña. Por favor espere."
},
"community": {
"username_and_email": "Usuario y E-mail",
"account_creation_date": "Fecha de creación de la cuenta",
"newsroom_role": "Rol en la redacción",
"admin": "Administrador",
"moderator": "Moderador",
"admin": "Administradora",
"moderator": "Moderadora",
"role": "Seleccionar rol...",
"no-results": "No se encontraron usuarios con ese nombre de usuario o correo electronico.",
"no-results": "No se encontraron usuarixs con ese nombre de usuario o e-mail.",
"status": "Estado",
"select-status": "Seleccionar estado...",
"active": "Activa",
"banned": "Suspendido",
"banned-user": "Usuario Suspendido",
"loading": "Cargando resultados",
"flags": "Reporte",
"flaggedaccounts": "Nombres de Usuario Reportados",
"people": "Gente",
"no-flagged-accounts": "No hay ninguna cuenta reportada.",
"I don't like this username": "No me gusta ese nombre de usuario",
"This user is impersonating": "Suplantación",
"This looks like an ad/marketing": "Spam/Propaganda",
"This username is offensive": "Ofensivo",
"Other": "Otros",
"ban_user": "Quieres suspender al Usuario?",
"are_you_sure": "Estas segura que quieres suspender a {0}?",
"note": "Nota: Suspender a este usuario no le va a permitir (al usuario) borrar ni editar ni comentar.",
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
},
"suspenduser": {
"title": "Suspendiendo un usuario",
"title_0": "Esta queriendo suspender un usuario?",
"description_0": "Le gustaria suspender a esta usuaria temporarianmente por su nombre de usuario? Si lo hace sus comentarios serán escondidos temporariamente hasta que puedan reescribir su nombre de usuario.",
"title_1": "Enviarle una nota al usuario sobre su cuenta suspendida",
"description_1": "Si suspende a este usuario, su cuenta va a ser deshabilitada y todos sus comentarios escondidos del sitio.",
"no_cancel": "No, cancelar",
"yes_suspend": "Si, suspender",
"send": "Enviar",
"username": "nombre de usuario",
"email_subject": "Su cuenta ha sido suspendida temporariamente",
"email": "Otra persona de la comunidad recientemente marcó su nombre de usuario para ser revisado. Por su contenido, el nombre de usuario ha sido rechazado. Esto quiere decir que no puede comentar, gustar o marcar contenido hasta que modifique su nombre de usuario. Por favor, envienos un correo a moderator@newsorg.com si tiene alguna pregunta o preocupación",
"write_message": "Escribir un mensaje",
"loading": "Cargando resultados"
},
"modqueue": {
@@ -173,38 +225,41 @@
"banned_user": "Usuario Suspendido"
},
"user": {
"user_bio": "",
"bio_flags": "",
"username_flags": ""
"user_bio": "marcas para este usuario",
"bio_flags": "marcas para esta biografia",
"username_flags": "marcas para este nombre de usuario"
},
"configure": {
"closed-stream-settings": "Mensaje a enviar cuando los comentarios están cerrados en el artículo",
"stream-settings": "Configuración de Comentarios",
"moderation-settings": "Configuración de Moderación",
"tech-settings": "Configuración Technical",
"tech-settings": "Configuración Técnica",
"custom-css-url": "URL CSS a medida",
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto de Embed Stream. Puede ser interna o externa.",
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto del hilo de comentarios. Puede ser interna o externa.",
"dashboard": "Panel",
"enable-pre-moderation": "Habilitar pre-moderación",
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
"require-email-verification": "Necesita confirmación de correo",
"require-email-verification-text": "Nuevos usuarios deben verificar sus correos antes de comentar",
"require-email-verification": "Necesita confirmación de e-mail",
"require-email-verification-text": "Nuevos usuarios deben verificar sus e-mails antes de comentar",
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
"include-text": "Incluir tu texto aqui.",
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
"wordlist": "Palabras Suspendidas y Suspechosas",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"banned-words-title": "Banned words list",
"suspect-words-title": "Suspect words list",
"enable-premod-links": "Pre-Moderar Commentarios que contienen Enlaces",
"enable-premod-links-text": "Los y las Moderadoras deben aprobar cualquier comentario que contengan links antes de su publicación.",
"wordlist": "Palabras Suspendidas y Sospechosas",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente marcadas para separar los comentarios publicados.",
"suspect-word-text": "Comentarios que contengan estas palabras o frases, considerando mayusculas y minusculas, serán automaticamente destacadas en los comentarios publicados. Escribir una palabra y apretar Enter o Tabulador para agergarla. Opcionalmente pegar una lista separada por coma.",
"banned-words-title": "Lista de palabras prohibidas",
"suspect-words-title": "Lista de palabras sospechosas",
"save-changes": "Guardar Cambios",
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
"moderate": "Moderar",
"configure": "Configurar",
"community": "Comunidad",
"streams": "Streams",
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
"stories": "Artículos",
"closed-comments-desc": "Escribe un mensaje que será mostrado cuando los comentarios estén cerrados y no se acepten más comentarios.",
"closed-comments-label": "Escribe un mensaje...",
"never": "Nunca",
"hours": "Horas",
@@ -216,40 +271,41 @@
"comment-count-text-post": " caracteres",
"comment-count-error": "Por favor escribe un número válido.",
"domain-list-title": "Lista de Dominios Permitidos",
"domain-list-text": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com)."
"domain-list-text": "Agrega dominios permitidos a Talk, por ejemplo tu localhost, staging y ambientes de producción (ej. localhost:3000, staging.domain.com, domain.com)."
},
"embedlink": {
"copy": "Copiar"
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
"are_you_sure": "Estas segura que quieres suspender a {0}?",
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
"ban_user": "¿Quieres suspender el Usuario?",
"are_you_sure": "¿Estás segura que quieres suspender a {0}?",
"note": "Nota: Suspender a este usuario también va a colocar este comentario en la cola de Rechazados.",
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
},
"dashbord": {
"next-update": "{0} minutos hasta la siguiente actualización.",
"auto-update": "Los datos se actualizan automaticamente cada 5 minutos o cuando recargas.",
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
"no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
"flags": "Marcados",
"no_activity": "No hubo comentarios en los ultimos 5 minutos",
"comment_count": "comentarios"
},
"streams": {
"empty_result": "No se encuentro articulo con esta busqueda. Tal vez extender la busqueda?",
"search": "",
"filter-streams": "",
"stream-status": "",
"all": "",
"open": "",
"closed": "",
"newest": "",
"oldest": "",
"sort-by": "",
"open": "",
"closed": "",
"article": "",
"pubdate": "",
"status": ""
"empty_result": "No se encuentro articulo con esta busqueda. ¿Tal vez puedas extender la busqueda?",
"search": "buscar",
"filter-streams": "Filtrar Hilos de Comentarios",
"stream-status": "Estado del Hilo de Comentarios",
"all": "todxs",
"open": "abrir",
"closed": "cerrado",
"newest": "más nuevoß",
"oldest": "más viejo",
"sort-by": "ordenar por",
"article": "artículo",
"pubdate": "Fecha de Pblicación",
"status": "Estado"
}
}
}
@@ -34,6 +34,18 @@ export default ({handleChange, handleApply, changed, updateQuestionBoxContent, .
description: lang.t('configureCommentStream.enablePremodDescription')
}} />
</li>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premodLinks"
onChange={handleChange}
defaultChecked={props.premodLinks}
info={{
title: lang.t('configureCommentStream.enablePremodLinks'),
description: lang.t('configureCommentStream.enablePremodLinksDescription')
}} />
</li>
<li>
<Checkbox
className={styles.checkbox}
@@ -31,13 +31,14 @@ class ConfigureStreamContainer extends Component {
const questionBoxEnable = elements.qboxenable.checked;
const questionBoxContent = elements.qboxcontent.value;
// const premodLinks = elements.premodLinks.checked;
const premodLinksEnable = elements.premodLinks.checked;
const {changed} = this.state;
const newConfig = {
moderation: premod ? 'PRE' : 'POST',
questionBoxEnable,
questionBoxContent
questionBoxContent,
premodLinksEnable
};
if (changed) {
@@ -77,10 +78,9 @@ class ConfigureStreamContainer extends Component {
}
render () {
const status = this.props.asset.closedAt === null ? 'open' : 'closed';
const premod = this.props.asset.settings.moderation === 'PRE';
const questionBoxEnable = this.props.asset.settings.questionBoxEnable;
const questionBoxContent = this.props.asset.settings.questionBoxContent;
const {settings, closedAt} = this.props.asset;
const status = closedAt === null ? 'open' : 'closed';
const premod = settings.moderation === 'PRE';
return (
<div>
@@ -88,11 +88,11 @@ class ConfigureStreamContainer extends Component {
handleChange={this.handleChange}
handleApply={this.handleApply}
changed={this.state.changed}
premodLinks={false}
premodLinks={settings.premodLinks}
premod={premod}
updateQuestionBoxContent={this.updateQuestionBoxContent}
questionBoxEnable={questionBoxEnable}
questionBoxContent={questionBoxContent}
questionBoxEnable={settings.questionBoxEnable}
questionBoxContent={settings.questionBoxContent}
/>
<hr />
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
+3 -3
View File
@@ -9,7 +9,7 @@
"enablePremodLinks": "Pre-Moderate Comments Containing Links",
"enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published.",
"enableQuestionBox": "Ask readers a question",
"enableQuestionBoxDescription": "This question will appear at the top of this comment stram. Ask readers about a certain issue in the article or pose discussion questions, etc.",
"enableQuestionBoxDescription": "This question will appear at the top of this comment stream. Ask readers about a certain issue in the article or pose discussion questions, etc.",
"includeQuestionHere": "Write your question here."
}
},
@@ -20,8 +20,8 @@
"description": "Como Administrador/a puedes modificar las opciones de los comentarios en este artículo",
"enablePremod": "Activar Pre Moderación",
"enablePremodDescription": "Los y las Moderadoras deben aprobar cualquier comentario antes de su publicación",
"enablePremodLinks": "Pre-Moderar Commentarios que contienen Links",
"enablePremodLinksDescription": "Los y las Moderadoras deben probar cualquier comentario que contengan links antes de su publicación.",
"enablePremodLinks": "Pre-Moderar Comentarios que contienen Enlaces",
"enablePremodLinksDescription": "Los y las moderadoras deben aprobar cualquier comentario que contengan enlaces antes de su publicación.",
"enableQuestionBox": "Hacer una pregunta a los y las lectoras.",
"enableQuestionBoxDescription": "Esta pregunta aparecera en la parte de arriba del hilo de comentarios.",
"includeQuestionHere": "Escribir la pregunta aquí."
+11
View File
@@ -1,3 +1,14 @@
.Reply {
position: relative;
margin-bottom: 15px;
}
.Comment {
margin-bottom: 15px;
position: relative;
}
.pendingComment {
filter: blur(2px);
pointer-events: none;
}
+95 -27
View File
@@ -17,6 +17,7 @@ import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
import styles from './Comment.css';
@@ -25,6 +26,11 @@ const getActionSummary = (type, comment) => comment.action_summaries
.filter((a) => a.__typename === type)[0];
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
// hold actions links (e.g. Like, Reply) along the comment footer
const ActionButton = ({children}) => {
return <span className="comment__action-button comment__action-button--nowrap">{ children }</span>;
};
class Comment extends React.Component {
constructor(props) {
@@ -37,6 +43,7 @@ class Comment extends React.Component {
// id of currently opened ReplyBox. tracked in Stream.js
activeReplyBox: PropTypes.string.isRequired,
disableReply: PropTypes.bool,
setActiveReplyBox: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
postFlag: PropTypes.func.isRequired,
@@ -74,7 +81,13 @@ class Comment extends React.Component {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
}).isRequired
}).isRequired,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
}
render () {
@@ -94,14 +107,38 @@ class Comment extends React.Component {
loadMore,
setActiveReplyBox,
activeReplyBox,
deleteAction
deleteAction,
addCommentTag,
removeCommentTag,
disableReply,
} = this.props;
const like = getActionSummary('LikeActionSummary', comment);
const flag = getActionSummary('FlagActionSummary', comment);
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async () => {
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
try {
return await fn();
} catch (error) {
addNotification('error', errorToMessage(error));
throw error;
}
};
const addBestTag = notifyOnError(() => addCommentTag({
id: comment.id,
tag: BEST_TAG,
}), () => 'Failed to tag comment as best');
const removeBestTag = notifyOnError(() => removeCommentTag({
id: comment.id,
tag: BEST_TAG,
}), () => 'Failed to remove best comment tag');
return (
<div
@@ -109,14 +146,21 @@ class Comment extends React.Component {
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}>
<hr aria-hidden={true} />
<AuthorName
author={comment.user}/>
{ isStaff(comment.tags)
? <TagLabel isStaff={true}/>
<div className={highlighted === comment.id ? 'highlighted-comment' : ''}>
<AuthorName
author={comment.user}/>
{ isStaff(comment.tags)
? <TagLabel>Staff</TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<Content body={comment.body} />
<div className="commentActionsLeft">
{ commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<Content body={comment.body} />
<div className="commentActionsLeft comment__action-container">
<ActionButton>
<LikeButton
like={like}
id={comment.id}
@@ -124,23 +168,42 @@ class Comment extends React.Component {
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false} />
</div>
<div className="commentActionsRight">
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
<FlagComment
flag={flag && flag.current_user ? flag : dontagree}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</ActionButton>
{
!disableReply &&
<ActionButton>
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false} />
</ActionButton>
}
<ActionButton>
<IfUserCanModifyBest user={currentUser}>
<BestButton
isBest={commentIsBest(comment)}
addBest={addBestTag}
removeBest={removeBestTag} />
</IfUserCanModifyBest>
</ActionButton>
</div>
<div className="commentActionsRight comment__action-container">
<ActionButton>
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
</ActionButton>
<ActionButton>
<FlagComment
flag={flag && flag.current_user ? flag : dontagree}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</ActionButton>
</div>
</div>
{
activeReplyBox === comment.id
@@ -161,6 +224,7 @@ class Comment extends React.Component {
comment.replies.map(reply => {
return <Comment
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
@@ -172,6 +236,8 @@ class Comment extends React.Component {
postLike={postLike}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
@@ -185,6 +251,8 @@ class Comment extends React.Component {
assetId={asset.id}
comments={comment.replies}
parentId={comment.id}
topLevel={false}
replyCount={comment.replyCount}
moreComments={comment.replyCount > comment.replies.length}
loadMore={loadMore}/>
</div>
+93 -60
View File
@@ -6,17 +6,18 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
import {TabBar, Tab, TabContent, Spinner} from 'coral-ui';
import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui';
const {logout, showSignInDialog, requestConfirmEmail} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, postDontAgree, deleteAction} from 'coral-framework/graphql/mutations';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {updateCountCache} from 'coral-framework/actions/asset';
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset';
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
import InfoBox from 'coral-plugin-infobox/InfoBox';
@@ -31,7 +32,7 @@ import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUserna
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
import Comment from './Comment';
import HighlightedComment from './Comment';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
@@ -55,17 +56,43 @@ class Embed extends Component {
data: React.PropTypes.shape({
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired
}).isRequired,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
}
componentDidMount () {
pym.sendMessage('childReady');
}
componentWillUnmount () {
clearInterval(this.state.countPoll);
}
componentWillReceiveProps (nextProps) {
const {loadAsset} = this.props;
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
loadAsset(nextProps.data.asset);
const {getCounts, updateCountCache} = this.props;
const {asset} = nextProps.data;
updateCountCache(asset.id, asset.commentCount);
this.setState({
countPoll: setInterval(() => {
const {asset} = this.props.data;
getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
});
}, NEW_COMMENT_COUNT_POLL_INTERVAL)
});
}
}
@@ -73,7 +100,7 @@ class Embed extends Component {
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0);
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
}
}
@@ -91,6 +118,8 @@ class Embed extends Component {
const {closedAt, countCache = {}} = this.props.asset;
const {loading, asset, refetch, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
const highlightedComment = comment && comment.parent ? comment.parent : comment;
const openStream = closedAt === null;
@@ -114,10 +143,20 @@ class Embed extends Component {
<div style={expandForLogin}>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.commentCount}/></Tab>
<Tab>{lang.t('profile')}</Tab>
<Tab><Count count={asset.totalCommentCount}/></Tab>
<Tab>{lang.t('MY_COMMENTS')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
highlightedComment &&
<Button
cStyle='darkGrey'
style={{float: 'right'}}
onClick={() => {
this.props.viewAllComments();
this.props.data.refetch();
}}>{lang.t('showAllComments')}</Button>
}
{loggedIn && <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>}
<TabContent show={activeTab === 0}>
{
@@ -154,7 +193,6 @@ class Embed extends Component {
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
: null
}
<ModerationLink assetId={asset.id} isAdmin={isAdmin} />
</RestrictedContent>
</div>
: <p>{asset.settings.closedMessage}</p>
@@ -164,13 +202,16 @@ class Embed extends Component {
refetch={refetch}
offset={signInOffset}/>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
{/* the highlightedComment is isolated after the user followed a permalink */}
{
highlightedComment &&
<Comment
highlightedComment
? <HighlightedComment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
addNotification={addNotification}
addNotification={this.props.addNotification}
depth={0}
postItem={this.props.postItem}
asset={asset}
@@ -185,41 +226,42 @@ class Embed extends Component {
key={highlightedComment.id}
reactKey={highlightedComment.id}
comment={highlightedComment} />
: <div>
<NewCount
commentCount={asset.commentCount}
countCache={countCache[asset.id]}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
updateCountCache={this.props.updateCountCache}
/>
<div className="embed__stream">
<Stream
open={openStream}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
</div>
<LoadMore
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={countCache[asset.id] > asset.comments.length}
loadMore={this.props.loadMore} />
</div>
}
<NewCount
commentCount={asset.commentCount}
countCache={countCache[asset.id]}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
updateCountCache={this.props.updateCountCache}
/>
<Stream
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
getCounts={this.props.getCounts}
updateCountCache={this.props.updateCountCache}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={{text: null}}
/>
<LoadMore
assetId={asset.id}
comments={asset.comments}
moreComments={asset.commentCount > asset.comments.length}
loadMore={this.props.loadMore}/>
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
@@ -236,11 +278,6 @@ class Embed extends Component {
/>
</RestrictedContent>
</TabContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
</div>
);
@@ -248,7 +285,6 @@ class Embed extends Component {
}
const mapStateToProps = state => ({
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS(),
asset: state.asset.toJS()
@@ -257,17 +293,12 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
requestConfirmEmail: () => dispatch(requestConfirmEmail()),
loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)),
addNotification: (type, text) => {
pym.sendMessage('getPosition');
pym.onMessage('position', position => {
dispatch(addNotification(type, text, position));
});
},
addNotification: (type, text) => addNotification(type, text),
clearNotification: () => dispatch(clearNotification()),
editName: (username) => dispatch(editName(username)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
viewAllComments: () => dispatch(viewAllComments()),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
});
@@ -278,6 +309,8 @@ export default compose(
postFlag,
postLike,
postDontAgree,
addCommentTag,
removeCommentTag,
deleteAction,
queryStream
)(Embed);
+43 -20
View File
@@ -5,41 +5,64 @@ import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments';
import {Button} from 'coral-ui';
const lang = new I18n(translations);
const loadMoreComments = (assetId, comments, loadMore, parentId) => {
const loadMoreComments = (assetId, comments, loadMore, parentId, replyCount) => {
if (!comments.length) {
return;
let cursor = null;
if (comments.length) {
cursor = parentId
? comments[0].created_at
: comments[comments.length - 1].created_at;
}
const cursor = parentId
? comments[0].created_at
: comments[comments.length - 1].created_at;
loadMore({
limit: ADDTL_COMMENTS_ON_LOAD_MORE,
limit: parentId ? replyCount : ADDTL_COMMENTS_ON_LOAD_MORE,
cursor,
assetId,
asset_id: assetId,
parent_id: parentId,
sort: parentId ? 'CHRONOLOGICAL' : 'REVERSE_CHRONOLOGICAL'
});
};
const LoadMore = ({assetId, comments, loadMore, moreComments, parentId}) => (
moreComments
? <Button
className='coral-load-more'
onClick={() => loadMoreComments(assetId, comments, loadMore, parentId)}>
{
lang.t('loadMore')
}
</Button>
: null
);
class LoadMore extends React.Component {
componentDidMount () {
this.initialState = true;
}
replyCountFormat = (count) => {
if (count === 1) {
return lang.t('viewReply');
}
if (this.initialState) {
return lang.t('viewAllRepliesInitial', count);
} else {
return lang.t('viewAllReplies', count);
}
}
render () {
const {assetId, comments, loadMore, moreComments, parentId, replyCount, topLevel} = this.props;
return moreComments
? <div className='coral-load-more'>
<Button
onClick={() => {
this.initialState = false;
loadMoreComments(assetId, comments, loadMore, parentId, replyCount);
}}>
{topLevel ? lang.t('viewMoreComments') : this.replyCountFormat(replyCount)}
</Button>
</div>
: null;
}
}
LoadMore.propTypes = {
assetId: PropTypes.string.isRequired,
comments: PropTypes.array.isRequired,
moreComments: PropTypes.bool.isRequired,
topLevel: PropTypes.bool.isRequired,
replyCount: PropTypes.number,
loadMore: PropTypes.func.isRequired
};
+16 -24
View File
@@ -1,6 +1,5 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
class Stream extends React.Component {
@@ -8,11 +7,18 @@ class Stream extends React.Component {
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
asset: PropTypes.object.isRequired,
open: PropTypes.bool.isRequired,
comments: PropTypes.array.isRequired,
currentUser: PropTypes.shape({
username: PropTypes.string,
id: PropTypes.string
})
}),
// dispatch action to add a tag to a comment
addCommentTag: PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: PropTypes.func,
}
constructor(props) {
@@ -20,26 +26,6 @@ class Stream extends React.Component {
this.state = {activeReplyBox: '', countPoll: null};
}
componentDidMount() {
const {asset, getCounts, updateCountCache} = this.props;
updateCountCache(asset.id, asset.commentCount);
// Note: Apollo's built-in polling doesn't work with fetchMore queries, so a
// setInterval is being used instead.
this.setState({
countPoll: setInterval(() => getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
}), NEW_COMMENT_COUNT_POLL_INTERVAL),
});
}
componentWillUnmount() {
clearInterval(this.state.countPoll);
}
render () {
const {
comments,
@@ -49,17 +35,21 @@ class Stream extends React.Component {
addNotification,
postFlag,
postLike,
open,
postDontAgree,
loadMore,
deleteAction,
showSignInDialog
showSignInDialog,
addCommentTag,
removeCommentTag
} = this.props;
return (
<div>
<div id='stream'>
{
comments.map(comment =>
<Comment
disableReply={!open}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
@@ -70,6 +60,8 @@ class Stream extends React.Component {
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
+83 -18
View File
@@ -1,15 +1,22 @@
* {
font-weight: inherit;
font-family: inherit;
font-style: inherit;
font-size: 100%;
}
html, body {
width:auto;
height:auto;
}
body {
font-family: 'Open Sans', sans-serif;
font-family: 'Lato', sans-serif;
width: 100%;
font-size: 14px;
margin: 0px;
padding: 0px 0px 50px 0px;
height: auto !important;
}
.expandForSignin {
@@ -80,6 +87,16 @@ hr {
padding: 10px;
margin-bottom: 10px;
display: block;
box-sizing: border-box;
border-radius: 2px;
}
.commentStream .material-icons {
vertical-align: middle;
width: 1em;
font-size: 1em;
overflow: hidden;
}
/* Question Box Styles */
@@ -95,15 +112,41 @@ hr {
font-weight: bold;
font-size: 14px;
display: block;
overflow: hidden;
height: 50px;
}
.coral-plugin-questionbox-icon {
.coral-plugin-questionbox-icon.bubble{
position: absolute;
top: 11px;
left: 15px;
color: #949393;
font-size: 20px;
z-index: 0;
}
.coral-plugin-questionbox-icon.person{
z-index: 2;
top: 20px;
left: 20px;
position: absolute;
font-size: 24px;
color: white;
}
.coral-plugin-questionbox-box {
position: relative;
border: 0;
background: black;
color: white;
padding: 20px;
margin-left: 0px !important;
margin-right: 10px;
display: inline-block;
width: 15px;
height: 100%;
padding: 3px 20px;
vertical-align: middle;
}
.hidden {
@@ -127,6 +170,8 @@ hr {
flex: 1;
padding: 5px;
min-height: 100px;
margin-top: 10px;
font-size: 14px;
}
.coral-plugin-commentbox-button-container {
@@ -168,11 +213,12 @@ hr {
.coral-plugin-commentcontent-text {
margin-bottom: 7px;
font-size: 16px;
}
.coral-plugin-author-name-text {
display: inline-block;
margin: 10px 8px 10px 0;
margin: 10px 5px 10px 0;
font-weight: bold;
}
@@ -191,7 +237,7 @@ hr {
background-color: #4C1066;
color: white;
display: inline-block;
margin: 10px 10px;
margin: 0px 5px;
padding: 5px 5px;
border-radius: 2px;
}
@@ -218,10 +264,23 @@ hr {
width: 50%;
}
.material-icons {
font-size: 12px !important;
.comment__action-container .material-icons {
font-size: 12px;
margin-left: 3px;
vertical-align: middle;
}
button.comment__action-button,
.comment__action-button button {
cursor: pointer;
}
button.comment__action-button[disabled],
.comment__action-button[disabled] button {
cursor: inherit;
}
.comment__action-button--nowrap {
white-space: nowrap;
}
.likedButton {
@@ -236,6 +295,7 @@ hr {
color: #696969;
display: inline-block;
font-size: .75rem;
margin-left: 5px;
}
.coral-plugin-permalinks-container {
@@ -247,10 +307,7 @@ hr {
display: none;
background-color: white;
border: 1px solid black;
width: calc(100% - 15px);
position: absolute;
top: 70px;
right: 0;
padding: 5px;
}
@@ -302,14 +359,14 @@ hr {
}
.coral-plugin-flags-popup-counter {
float: left;
margin-top: 21px;
color: #999;
float: left;
margin-top: 21px;
color: #999;
}
.coral-plugin-flags-popup-button {
float: right;
margin-top: 10px;
float: right;
margin-top: 10px;
}
.coral-plugin-flags-reason-text {
@@ -359,15 +416,23 @@ hr {
/* Load More */
button.coral-load-more {
width: 100%;
.coral-load-more {
text-align: center;
}
.coral-load-more button {
text-align: center;
color: #FFF;
background-color: #2376D8;
cursor: pointer;
padding: 10px;
border-radius: 2px;
line-height: 1em;
text-transform: capitalize;
display: inline-block;
}
button.coral-load-more:hover {
.coral-load-more:hover button {
background-color: #4399FF;
}
+73 -4
View File
@@ -1,5 +1,26 @@
import pym from 'pym.js';
const snackbarStyles = {
position: 'fixed',
cursor: 'default',
userSelect: 'none',
backgroundColor: '#323232',
zIndex: 3,
willChange: 'transform, opacity',
transition: 'transform .35s cubic-bezier(.55,0,.1,1), opacity .35s',
pointerEvents: 'none',
padding: '12px 18px',
color: '#fff',
borderRadius: '3px 3px 0 0',
textAlign: 'center',
maxWidth: '400px',
left: '50%',
opacity: 0,
transform: 'translate(-50%, 20px)',
bottom: 0,
boxSizing: 'border-box'
};
// This function should return value of window.Coral
const Coral = {};
const Talk = Coral.Talk = {};
@@ -31,13 +52,52 @@ function buildStreamIframeUrl(talkBaseUrl, asset_url, comment, asset_id) {
function configurePymParent(pymParent, asset_url) {
let notificationOffset = 200;
let ready = false;
let cachedHeight;
const snackbar = document.createElement('div');
snackbar.id = 'coral-notif';
for (let key in snackbarStyles) {
snackbar.style[key] = snackbarStyles[key];
}
window.document.body.appendChild(snackbar);
// Resize parent iframe height when child height changes
pymParent.onMessage('height', function(height) {
if (height !== cachedHeight) {
pymParent.el.firstChild.style.height = `${height}px`;
cachedHeight = height;
}
});
// TODO: In local testing, this is firing nonstop. Maybe there's a bug on the inside?
// Or it's by design of pym... but that's very wasteful of CPU and DOM reflows (jank)
pymParent.el.querySelector('iframe').height = `${height }px`;
pymParent.onMessage('coral-clear-notification', function () {
snackbar.style.opacity = 0;
});
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-all-comments', function () {
window.history.replaceState(
{},
document.title,
location.origin + location.pathname + location.search
);
});
pymParent.onMessage('coral-alert', function (message) {
const [type, text] = message.split('|');
snackbar.style.transform = 'translate(-50%, 20px)';
snackbar.style.opacity = 0;
snackbar.className = `coral-notif-${type}`;
snackbar.textContent = text;
setTimeout(() => {
snackbar.style.transform = 'translate(-50%, 0)';
snackbar.style.opacity = 1;
}, 0);
setTimeout(() => {
snackbar.style.opacity = 0;
}, 5000);
});
// Helps child show notifications at the right scrollTop
@@ -116,7 +176,16 @@ Talk.render = function (el, opts) {
el.id = `_${Math.random()}`;
}
let asset_url = opts.asset_url || window.location.href.split('#')[0];
let asset_url = opts.asset_url;
if (!asset_url) {
try {
asset_url = document.querySelector('link[rel="canonical"]').href;
} catch (e) {
console.warn('This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.');
asset_url = window.location.origin + window.location.pathname;
}
}
let comment = window.location.hash.slice(1);
let query = {
+36 -1
View File
@@ -1,8 +1,9 @@
import * as actions from '../constants/asset';
import coralApi from '../helpers/response';
import {addNotification} from '../actions/notification';
import {pym} from 'coral-framework';
import I18n from 'coral-framework/modules/i18n/i18n';
import I18n from '../../coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
@@ -49,3 +50,37 @@ export const updateOpenStatus = status => dispatch => {
dispatch(updateOpenStream({closedAt: new Date().getTime()}));
}
};
function removeParam(key, sourceURL) {
let rtn = sourceURL.split('?')[0];
let param;
let params_arr = [];
let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : '';
if (queryString !== '') {
params_arr = queryString.split('&');
for (let i = params_arr.length - 1; i >= 0; i -= 1) {
param = params_arr[i].split('=')[0];
if (param === key) {
params_arr.splice(i, 1);
}
}
rtn = `${rtn}?${params_arr.join('&')}`;
}
return rtn;
}
export const viewAllComments = () => {
// remove the comment_id url param
const modifiedUrl = removeParam('comment_id', location.href);
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, modifiedUrl);
// also change the parent url
pym.sendMessage('coral-view-all-comments');
} catch (e) { /* not sure if we're worried about old browsers */ }
return {type: actions.VIEW_ALL_COMMENTS};
};
+5 -3
View File
@@ -1,8 +1,9 @@
import I18n from 'coral-framework/modules/i18n/i18n';
import I18n from '../../coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {pym} from 'coral-framework';
// Dialog Actions
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
@@ -48,7 +49,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
return coralApi('/auth/local', {method: 'POST', body: formData})
.then(({user}) => {
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
})
@@ -135,7 +136,8 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU
export const fetchForgotPassword = email => (dispatch) => {
dispatch(forgotPassowordRequest(email));
coralApi('/account/password/reset', {method: 'POST', body: {email}})
const redirectUri = pym.parentUrl || location.href;
coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
};
+4 -12
View File
@@ -1,17 +1,9 @@
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
import {pym} from '../../coral-framework';
export const addNotification = (notifType, text, position) => {
return {
type: ADD_NOTIFICATION,
notifType,
text,
position
};
export const addNotification = (notifType, text) => {
pym.sendMessage('coral-alert', `${notifType}|${text}`);
};
export const clearNotification = () => {
return {
type: CLEAR_NOTIFICATION
};
pym.sendMessage('coral-clear-notification');
};
+8
View File
@@ -1,13 +1,21 @@
import {addNotification} from '../actions/notification';
import coralApi from '../helpers/response';
import * as actions from '../constants/auth';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
const editUsernameFailure = error => ({type: actions.EDIT_USERNAME_FAILURE, error});
const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS});
export const editName = (username) => (dispatch) => {
return coralApi('/account/username', {method: 'PUT', body: {username}})
.then(() => {
dispatch(editUsernameSuccess());
dispatch(addNotification('success', lang.t('successNameUpdate')));
})
.catch(error => {
dispatch(editUsernameFailure(lang.t(`error.${error.translation_key}`)));
});
};
@@ -9,3 +9,5 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE';
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
+5
View File
@@ -11,6 +11,11 @@ export const CREATE_USERNAME = 'CREATE_USERNAME';
export const SHOW_CREATEUSERNAME_DIALOG = 'SHOW_CREATEUSERNAME_DIALOG';
export const HIDE_CREATEUSERNAME_DIALOG = 'HIDE_CREATEUSERNAME_DIALOG';
export const EDIT_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
export const EDIT_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
export const EDIT_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
export const EDIT_USERNAME = 'CREATE_USERNAME';
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';

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