Compare commits

...
259 Commits
Author SHA1 Message Date
Wyatt Johnson d72b661c93 Merge pull request #272 from coralproject/fix-252
CLI + Init fixes
2017-01-31 10:47:02 -07:00
Wyatt Johnson 0af41a5b6b Updated circle test 2017-01-30 18:41:23 -07:00
Wyatt Johnson 864413b5d6 Fixed issues with e2e 2017-01-30 18:38:06 -07:00
Wyatt Johnson 91cd2e9e86 Fixes #266 2017-01-30 18:21:36 -07:00
Wyatt Johnson e6ef4b615f Changed prompt lib, updated cli's
- fixes #252
2017-01-30 17:46:09 -07:00
Wyatt Johnson b75557912f Merge pull request #268 from coralproject/graph-pagination
Graph Pagination
2017-01-30 14:43:44 -07:00
Belén Curcio 887fe08819 Merge branch 'master' into graph-pagination 2017-01-30 17:12:23 -03:00
David Erwin 1070dfd5ae Merge pull request #249 from coralproject/e2e-env-fix
E2E environment fix
2017-01-30 12:14:50 -05:00
Wyatt Johnson 04cd7312a6 Added support for new commentCount 2017-01-27 15:52:05 -07:00
David Jay b674ff45c0 Addressing errors with replies in e2e. 2017-01-27 17:29:23 -05:00
David Jay 1da61784b4 Fixing bugs around settings in e2e. 2017-01-27 16:10:47 -05:00
David Jay f0bb9db892 Merge branch 'e2e-env-fix' of github.com:coralproject/talk into e2e-env-fix 2017-01-27 15:21:47 -05:00
David Jay 9d15a0448a Adding settings init to pree2e script. 2017-01-27 15:21:18 -05:00
David Jay f09d9d0a8b Merge branch 'master' into e2e-env-fix 2017-01-27 14:10:59 -05:00
David Jay 801d18639b Merge branch 'e2e-env-fix' of github.com:coralproject/talk into e2e-env-fix 2017-01-27 14:09:29 -05:00
David Jay fcf22ef4f9 Adding selenium settings and fixing bug in moderation approval. 2017-01-27 14:07:34 -05:00
Wyatt Johnson 2e6c7f7b09 First pass at pagination 2017-01-27 12:05:19 -07:00
Wyatt Johnson d829d2541f Added new env var for e2e reports 2017-01-27 11:20:45 -07:00
Wyatt Johnson ebc928c6f2 Merge pull request #267 from coralproject/lodash-dep
Lodash Import
2017-01-27 11:03:59 -07:00
Belen Curcio 55a8b4984a only importing isEqual 2017-01-27 14:58:36 -03:00
David Erwin 087b1a7bd7 Merge pull request #264 from coralproject/postmod-default
Update default setting to POST moderation
2017-01-27 11:16:36 -05:00
Wyatt Johnson 0096de6e79 Merge branch 'master' into postmod-default 2017-01-27 09:11:51 -07:00
Kim Gardner 88d718e7c9 Merge pull request #265 from coralproject/remove-avatar
Remove avatar from user cards
2017-01-27 10:32:12 -05:00
David Erwin c8c160a551 Remove avatar from user cards 2017-01-27 10:16:09 -05:00
Wyatt Johnson 5d1fd61425 Merge branch 'master' into e2e-env-fix 2017-01-27 08:04:34 -07:00
Wyatt Johnson 9fbde9c023 Update init.js 2017-01-27 08:04:00 -07:00
David Erwin 744fce4563 Set settings model as well 2017-01-27 10:01:58 -05:00
David Erwin c071484357 Removing test artifiacts 2017-01-27 09:48:41 -05:00
David Erwin 0a80205095 Update default setting to POST moderation 2017-01-27 09:45:35 -05:00
David Erwin fbc5a6660a Merge pull request #263 from coralproject/moderate-bio
Marking correct user as "Pending" on user flags.
2017-01-27 09:25:02 -05:00
Wyatt Johnson 71647d5233 Merge branch 'master' into e2e-env-fix 2017-01-26 16:47:52 -07:00
David Jay e7587d9a41 Merge branch 'master' into moderate-bio 2017-01-26 17:46:20 -05:00
David Jay 02ece80227 Marking correct user as Pending on user flags. 2017-01-26 17:42:45 -05:00
David Erwin 74d78b76a5 Merge pull request #219 from coralproject/moderate-bio
Moderate bio or username
2017-01-26 17:09:44 -05:00
David Jay d4c9bdb6fd Merge branch 'e2e-env-fix' of github.com:coralproject/talk into e2e-env-fix 2017-01-26 16:52:12 -05:00
David Jay b8ff2b6763 Resetting chrome version to 2.25 in nightwatch. 2017-01-26 16:51:03 -05:00
Wyatt Johnson fa985acd14 Update circle.yml 2017-01-26 14:45:02 -07:00
David Jay f06edec6a2 Merge branch 'master' of github.com:coralproject/talk into moderate-bio 2017-01-26 16:38:43 -05:00
David Jay b6d8a90084 Updating style of UserActions and moving ActionButton to its own component. 2017-01-26 16:36:43 -05:00
David Erwin 3d08d1f54f Merge branch 'master' into e2e-env-fix 2017-01-26 16:30:14 -05:00
David Erwin b35336d1ed Merge pull request #260 from coralproject/hide-premod-links
Hiding premod links
2017-01-26 16:28:48 -05:00
David Erwin f42b8f324a Merge branch 'master' into hide-premod-links 2017-01-26 16:24:06 -05:00
David Erwin 30583cd93f Merge pull request #261 from coralproject/collapse-replies
Collapse replies
2017-01-26 16:21:07 -05:00
David Erwin 2086bd7b72 Merge branch 'master' into collapse-replies 2017-01-26 16:17:23 -05:00
David Erwin fe62c1a739 Merge pull request #257 from coralproject/refresh-data-fix
Cleanup and more GraphQL features
2017-01-26 16:15:55 -05:00
David Jay 06331d3c07 Updating bio moderation to graphQL architecture. 2017-01-26 15:31:15 -05:00
Riley Davis 2ad27ef6f9 add a cancel button to the reply box 2017-01-26 13:21:23 -07:00
Riley Davis 3cbe8bb7ac Stream now tracks what reply box is open 2017-01-26 12:45:14 -07:00
David Jay 963fc1f091 Merge branch 'e2e-env-fix' of github.com:coralproject/talk into e2e-env-fix 2017-01-26 14:41:41 -05:00
David Jay 2e34f726eb Updating tests to reflect graphql changes. 2017-01-26 14:41:03 -05:00
David Erwin afb246b173 Hiding premod links 2017-01-26 10:46:01 -05:00
Wyatt Johnson 48cc82b039 Merge branch 'master' into e2e-env-fix 2017-01-26 07:59:01 -07:00
Belen Curcio 88169526c3 Missing semicolon 2017-01-26 10:50:53 -03:00
Belen Curcio 4a54c571ed Open Stream and Closed Stream status 2017-01-26 10:45:50 -03:00
Belen Curcio 853981e21f Working configure tab and refresh data. Adding uncontrolled components, deleting state 2017-01-26 10:19:49 -03:00
Belen Curcio 74bef18098 user checker 2017-01-26 09:34:47 -03:00
Belen Curcio 64ed10845b Merge 2017-01-25 21:59:42 -03:00
David Erwin 0b73f18eb3 Merge pull request #258 from coralproject/streams-by-asset-id
Streams by asset
2017-01-25 17:48:41 -05:00
David Erwin 1a065ffa51 Merge branch 'streams-by-asset-id' of https://github.com/coralproject/talk into streams-by-asset-id 2017-01-25 17:45:09 -05:00
David Erwin c4256fd27b Ditch console logs 2017-01-25 17:44:21 -05:00
David Jay aa83aea849 Merge branch 'master' of github.com:coralproject/talk into e2e-env-fix 2017-01-25 17:17:04 -05:00
David Jay 31abe57cb2 Merge branch 'master' of github.com:coralproject/talk into moderate-bio
Integrating graphql, updating tests, and preserving transition to ModerationList from CommentList.
2017-01-25 17:15:12 -05:00
Wyatt Johnson 000f2c64bb Merge branch 'master' into streams-by-asset-id 2017-01-25 14:50:51 -07:00
Wyatt Johnson 34ca36822f Merge pull request #259 from coralproject/graphql-abstraction
Moved the graphql file out of the const string
2017-01-25 14:50:42 -07:00
Wyatt Johnson 0035037583 Merge branch 'master' into graphql-abstraction 2017-01-25 14:32:52 -07:00
Wyatt Johnson 693e92a627 Moved the graphql file out of the string 2017-01-25 14:31:11 -07:00
David Erwin b4abb9724e Fix homepage bug 2017-01-25 15:59:08 -05:00
David Erwin 9764631a37 Fix asset search skip limit bug 2017-01-25 15:37:18 -05:00
David Erwin e320a9dd20 Merge branch 'master' into streams-by-asset-id 2017-01-25 15:30:44 -05:00
David Erwin 1f1b1d43ae Add useful links 2017-01-25 15:27:38 -05:00
David Erwin 29e69a66bb Add basic index page for assets 2017-01-25 15:23:02 -05:00
Wyatt Johnson 9598c55466 Merge pull request #256 from coralproject/graoh-fixes
Comment ordering fixed, banned users can't do anything
2017-01-25 13:16:05 -07:00
Belen Curcio ec0dd34d40 Configure tab while you are loggedOff 2017-01-25 16:33:55 -03:00
David Erwin ad9684acd9 Create load stream by id route 2017-01-25 14:08:19 -05:00
Wyatt Johnson 91402a3e8b Moved PID into util.js 2017-01-25 11:57:40 -07:00
Wyatt Johnson edf9ced454 Added PID support 2017-01-25 11:11:33 -07:00
Belen Curcio 7c132600f0 Console log and typo 2017-01-25 11:47:35 -03:00
Belen Curcio b3eb3e3c43 Updated tests for Comment History 2017-01-25 11:44:51 -03:00
Belen Curcio d19d2ebe48 My Comment History Working - linted 2017-01-25 11:19:34 -03:00
Belen Curcio 30b6f8d112 my Comment History working 🎉 : TADA : 2017-01-25 11:04:23 -03:00
Belen Curcio 14483a39a0 Tests passing, removing legacy items 2017-01-25 08:38:03 -03:00
Belen Curcio c1d3cdce77 deprecating items 2017-01-25 08:11:52 -03:00
Belen Curcio cebc5f2f45 Adding fragment views 2017-01-25 07:09:11 -03:00
Belen Curcio b1848c28e4 Adding Missing Translations 2017-01-25 07:08:55 -03:00
Belen Curcio e993330467 Adding fragments 🎉 2017-01-25 05:20:34 -03:00
Belen Curcio e3b183c565 Items cleanup 2017-01-25 05:15:04 -03:00
Wyatt Johnson 373f119ac2 Added more comment sorting 2017-01-24 17:22:29 -07:00
Wyatt Johnson eb469df573 Removed debugging tool 2017-01-24 17:08:12 -07:00
Wyatt Johnson 26b7d48f1f Comment ordering fixed, banned users can't do anything 2017-01-24 17:06:40 -07:00
David Erwin 79b366f257 Merge pull request #255 from coralproject/translation-fix
fixed translation key, enforced moderation setting schema
2017-01-24 18:34:48 -05:00
Wyatt Johnson b6d917a22b fixed translation key, enforced moderation setting schema 2017-01-24 16:08:13 -07:00
David Erwin 622afcc80f Merge pull request #254 from coralproject/graphql
Graphql redux (as in 'the return of')
2017-01-24 18:02:18 -05:00
David Erwin a09c208f58 Merge branch 'master' into graphql 2017-01-24 17:56:04 -05:00
Wyatt Johnson 2698045e82 Fixed capital 2017-01-24 15:50:37 -07:00
Wyatt Johnson 5a2d8c5201 Fixed template tag 2017-01-24 15:40:32 -07:00
David Erwin 9616c50d49 Merge pull request #251 from coralproject/graphql
Graphql for the comment feed
2017-01-24 17:24:11 -05:00
David Erwin 66b0babdde Merge branch 'graphql' of https://github.com/coralproject/talk into graphql 2017-01-24 17:17:38 -05:00
David Erwin 9435b627b1 Uppercase more enums in coral-admin 2017-01-24 17:17:03 -05:00
Riley Davis da7349343e Merged branch graphql into graphql 2017-01-24 15:09:09 -07:00
Riley Davis 62ae297887 make sure the app does not break if you don't have react devtools installed 2017-01-24 15:08:38 -07:00
Belen Curcio d2acffc19c linting! 2017-01-24 19:07:53 -03:00
Belen Curcio 054baa0530 çonfig actions not defined 2017-01-24 19:01:45 -03:00
Belen Curcio fb791a9df9 merge 2017-01-24 18:52:37 -03:00
Belen Curcio ecfb365c24 log 2017-01-24 18:51:06 -03:00
Wyatt Johnson fa300b1c24 Fixes to circle 2017-01-24 14:38:44 -07:00
David Jay b0acdff34d Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-24 16:23:27 -05:00
Belen Curcio 6f44b9cba0 Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-24 18:23:11 -03:00
David Jay 7c3f566202 Handling links inside of iframe in via Pym. 2017-01-24 16:21:49 -05:00
Belen Curcio 9edf99730d Status 2017-01-24 18:21:49 -03:00
Wyatt Johnson 050e98f157 Fixed report location 2017-01-24 14:12:52 -07:00
David Erwin 5566856542 Merge branch 'graphql' of https://github.com/coralproject/talk into graphql 2017-01-24 16:12:13 -05:00
David Erwin 53dd34f406 Updating admin screen for all caps enums 2017-01-24 16:11:17 -05:00
Belen Curcio 9d7b727f08 Settings container 2017-01-24 18:08:58 -03:00
Wyatt Johnson 603854c0be Adjusted tests for circle 2017-01-24 14:06:39 -07:00
David Jay ce73118f53 Loading history links in parent page. 2017-01-24 16:04:03 -05:00
Wyatt Johnson 2ffecbeeff Removed build step, is already done via postinstall hook 2017-01-24 13:46:53 -07:00
Wyatt Johnson 0a5a23c1f3 Added fixes for tests on CI 2017-01-24 13:45:06 -07:00
Belen Curcio ee9674cd23 Settings stream tab 2017-01-24 17:27:30 -03:00
Wyatt Johnson 9dc57c82d1 Fixes to consts + services 2017-01-24 13:27:22 -07:00
Belen Curcio 5750758cf1 Error with asset resolver 2017-01-24 17:27:05 -03:00
David Jay 88ed6ffe76 Updating logic for returning users to frontend. 2017-01-24 15:20:27 -05:00
Wyatt Johnson aa1f06d1ee Added explicit joinBy to dataLoader reponse 2017-01-24 13:12:51 -07:00
Wyatt Johnson cda768d694 Fixer pre/post 2017-01-24 13:10:35 -07:00
Belen Curcio 33ec0798e9 Uppercase model 2017-01-24 17:07:23 -03:00
Wyatt Johnson eeaefd9656 fixes for cli 2017-01-24 13:01:56 -07:00
Wyatt Johnson 3a59a785f8 Uppercased status's 2017-01-24 12:58:20 -07:00
Wyatt Johnson 5e9f8eb624 Fixed statuses 2017-01-24 12:43:22 -07:00
David Jay d921a59858 Hiding bio tooltip when the user has no bio. 2017-01-24 14:39:46 -05:00
David Jay 74ced3b2fd Removing display of user ID in settings. 2017-01-24 14:35:14 -05:00
David Jay 6e9757ae91 Updating tests to use capitalized admin. 2017-01-24 14:27:50 -05:00
David Jay 8b8bef4ea7 Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-24 14:19:58 -05:00
David Jay 4b33199f2f Adding signin button to settings page. 2017-01-24 14:19:20 -05:00
Wyatt Johnson 368606befe Fixed yet, another, import 2017-01-24 12:18:32 -07:00
Wyatt Johnson 71f219c0c4 Fixed another import 2017-01-24 12:17:06 -07:00
Wyatt Johnson 0e27984518 Fixed import 2017-01-24 12:15:31 -07:00
Riley Davis 98b3d299e5 Merged branch graphql into graphql 2017-01-24 12:14:06 -07:00
Riley Davis d977c33cf5 refetch when you post a comment 2017-01-24 12:13:56 -07:00
Wyatt Johnson a7e9c0c776 Const, service, and model updates
- Updated enum values to be uppercase
- Updated services to expose service models
- Updated models to only export the mongoose model
- Moved all mongoose static methods over to new services
- Updated tests to refelct new setup

BREAKING

- Status that were uppercased (caps) have caused issues with the
  admin pages
2017-01-24 12:10:32 -07:00
Riley Davis 0994023864 Merged branch graphql into graphql 2017-01-24 12:09:16 -07:00
Riley Davis 8cd929bb1b npm chokes on .js package names 2017-01-24 12:09:04 -07:00
David Jay 840e0d0ef6 Re-adding buttons when the user is not logged in and showing login if a non-logged-in user clicks 'Reply.' 2017-01-24 14:00:16 -05:00
David Jay 90c58885aa Fixing lint errors and renaming Pym file. 2017-01-24 13:23:42 -05:00
David Jay 0d7b482dc1 Re-adding pym import. 2017-01-24 13:21:13 -05:00
Belen Curcio c1fea3151c Merge 2017-01-24 15:15:17 -03:00
Belen Curcio a409fcbab7 Asset reducers 2017-01-24 15:10:09 -03:00
Riley Davis 4e8306bcc9 close reply box when comment is posted 2017-01-24 10:55:51 -07:00
David Jay c4e1c1ae44 Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-24 12:53:30 -05:00
David Jay 58e50b4242 Fixing permalinks and moving Pym to a seperate file. 2017-01-24 12:51:52 -05:00
Riley Davis 7fd075a7a6 posting a reply refetches comments 2017-01-24 10:43:29 -07:00
David Jay 75bcf29154 Replies to replies are children of the root comment. 2017-01-24 12:37:50 -05:00
David Jay 33d274386a Re-adding display of comment date. 2017-01-24 12:24:30 -05:00
Belen Curcio 365f60c428 Reducer and constants 2017-01-24 14:22:58 -03:00
Belen Curcio f375ff48c9 dispatching 2017-01-24 14:13:43 -03:00
Belen Curcio 765edb48b9 Émbed 2017-01-24 14:07:46 -03:00
David Jay 9fbd1d3a77 Displaying author names and bios. 2017-01-24 11:49:59 -05:00
Belen Curcio 9fd184f638 Store normalized 💪 2017-01-24 10:06:42 -03:00
Belen Curcio c3f238b0b2 Connected to the Apollo Store 🎉 2017-01-24 09:56:26 -03:00
Riley Davis fa4c6e8ec5 lint! 2017-01-23 16:56:44 -07:00
Riley Davis 6282c39f3a merge remote. can reply to a comment 2017-01-23 16:52:16 -07:00
Wyatt Johnson 9e22c100f0 Added admin login form (naive approach) 2017-01-23 16:31:37 -07:00
Belen Curcio 01f905b79d Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-23 19:46:34 -03:00
Belen Curcio adf4729d89 Resolver settings fix, info box and func working! 2017-01-23 19:45:34 -03:00
David Jay 44058b1718 Adding flag action buttons. 2017-01-23 17:34:24 -05:00
David Jay da761468b6 Enabling like buttons. 2017-01-23 16:18:51 -05:00
David Jay a8227bd4ad Activating post and delete actions on likebutton. 2017-01-23 14:22:25 -05:00
Wyatt Johnson d1621ad424 Fixed context 2017-01-23 11:57:35 -07:00
David Jay b2d9688c89 Adding postAction mutation and fixing lint errors. 2017-01-23 13:25:24 -05:00
David Jay f023d1de08 Merge branch 'master' into e2e-env-fix 2017-01-23 12:07:12 -05:00
Wyatt Johnson 6563f29ccf Moved tests -> test because mocha likes that 2017-01-20 17:28:08 -07:00
Wyatt Johnson fbd0bf52fe Fixed methods 2017-01-20 16:53:43 -07:00
Wyatt Johnson 0ec2c11cf7 modularized mutators, loaders 2017-01-20 16:49:19 -07:00
Riley Davis 32d23214f1 replies 2017-01-20 16:23:34 -07:00
Riley Davis 0dcc912fd0 whoops, committed broken code 2017-01-20 15:58:36 -07:00
Riley Davis dfc073ab10 Merge remote-tracking branch 'refs/remotes/origin/graphql'
Conflicts:
	client/coral-embed-stream/src/Embed.js
2017-01-20 15:56:03 -07:00
Riley Davis d724351fb6 merge my changes 2017-01-20 15:54:18 -07:00
Belen Curcio 9c40ee1245 Login sign In back on! 🎉 2017-01-20 19:35:31 -03:00
Belen Curcio 450d74848e Notifications are back 2017-01-20 19:20:43 -03:00
Belen Curcio 39bf092ebb Correct Status 2017-01-20 18:56:42 -03:00
Belen Curcio b98b71189f atempt to solution 2017-01-20 18:53:11 -03:00
Belen Curcio f813a77194 queries and mutations 2017-01-20 18:48:35 -03:00
Belen Curcio 715faf321f Wordlist is undefined 2017-01-20 18:04:30 -03:00
Belen Curcio 2615db8575 mutations and queries, lint errors 2017-01-20 17:59:28 -03:00
Belen Curcio ae29d65c75 mutations and queries in separate folders 🎉 2017-01-20 17:40:33 -03:00
Belen Curcio d89f41a041 Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-20 16:47:28 -03:00
Belen Curcio 3c540b9daa createComment and suspect list 2017-01-20 16:47:02 -03:00
Riley Davis 8f95e29315 Stream is a separate component that renders comments 2017-01-20 12:41:33 -07:00
Belen Curcio f0f692fc26 ádding webpack loader and post comment 2017-01-20 16:39:23 -03:00
Riley Davis d14fa33077 rename files to make more sense 2017-01-20 12:36:33 -07:00
Riley Davis c16f3b8fbe change scalar URL type for a bit 2017-01-20 11:54:18 -07:00
Belen Curcio 7a5aa0a80c Changes 2017-01-20 15:25:18 -03:00
Belen Curcio cc740a4a5e Adding closedAt status commentStream 2017-01-20 12:52:35 -03:00
Belen Curcio 0f15c21cc9 Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-20 12:34:14 -03:00
Belen Curcio 89c1fd19b7 Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-20 12:32:46 -03:00
Wyatt Johnson 07b2f9c2e5 Update typeDefs.js 2017-01-20 08:13:45 -07:00
Wyatt Johnson f286263f1f Adjusted placement of server side graph code. 2017-01-19 18:26:33 -07:00
Wyatt Johnson fec18b50d1 Merge branch 'master' into graphql 2017-01-19 18:08:38 -07:00
Wyatt Johnson 1db1908778 Introduced some changes to the resolvers + mutators 2017-01-19 18:06:06 -07:00
David Jay 8bad6f7a73 Enabling signup. 2017-01-19 15:51:38 -05:00
David Jay e9846c654c Enabling stream login with graphql. 2017-01-19 15:37:05 -05:00
David Jay 672d1ee19e Updating store to include auth and apollo. 2017-01-19 13:39:46 -05:00
Wyatt Johnson 840c91274d Added comments to typeDefs 2017-01-19 11:17:47 -07:00
Wyatt Johnson 03a5acc736 Modularized reolvers 2017-01-19 11:01:24 -07:00
Wyatt Johnson d683b09b1d Added more mutators 2017-01-19 10:39:29 -07:00
Belen Curcio b172e30e5b By doing this we can refactor one by one and still having a working app 2017-01-19 11:30:30 -03:00
Belen Curcio 2d414a5ab7 Transport, Client, from coral-framework 2017-01-19 10:01:43 -03:00
David Jay d34d511ce6 Merge branch 'graphql' of github.com:coralproject/talk into graphql 2017-01-18 23:13:50 -05:00
David Jay a40971e376 Adding thunk to apollog redux store. 2017-01-18 19:23:47 -05:00
David Jay 44f3f99b47 Adding ability to query the currently logged in user. 2017-01-18 19:20:36 -05:00
David Jay d3c215a18d Switching bare-bones commentstream to graphql. 2017-01-18 18:23:32 -05:00
Riley Davis bbfd017998 adding changes 2017-01-18 15:27:48 -07:00
Riley Davis 58e7abbe0c sdfasdf 2017-01-18 15:25:48 -07:00
Riley Davis 89a188255f super amazing code. 2017-01-18 15:23:07 -07:00
Riley Davis 5f63af8140 Merged branch master into graphql 2017-01-18 14:24:45 -07:00
David Erwin 347fe8bf2b Showing graphql data in console 2017-01-18 16:15:40 -05:00
David Erwin e36ecdfb33 Implement stream with data fetch 2017-01-18 15:58:36 -05:00
Wyatt Johnson 5b30fd89c8 More comments 2017-01-18 11:28:39 -07:00
Wyatt Johnson 52ed18a9b8 Added comments 2017-01-18 11:23:47 -07:00
David Jay f8e55ddc8b Merge branch 'master' of github.com:coralproject/talk into e2e-env-fix 2017-01-18 12:44:44 -05:00
David Jay 074630e628 Updating usernames and passwords to new standards. 2017-01-18 12:43:09 -05:00
David Jay b59512d2b8 Allowing e2e to run with only node_env=test. 2017-01-18 12:12:49 -05:00
Wyatt Johnson 17849898ae First pass 2017-01-18 00:09:26 -07:00
David Jay dc63cc6bce Merge branch 'moderate-bio' of github.com:coralproject/talk into moderate-bio 2017-01-13 16:39:17 -05:00
David Jay 0131dd6b13 Merge branch 'master' of github.com:coralproject/talk into moderate-bio 2017-01-13 16:37:31 -05:00
David Jay 1432cfba4a Updating signing dialog style to render as intended in Firefox. 2017-01-12 15:49:23 -05:00
Belén Curcio b957e75d68 Merge branch 'master' into moderate-bio 2017-01-12 09:21:02 -03:00
David Jay 61bc87bafc Adding ALL moderation list. 2017-01-11 17:35:09 -05:00
David Jay 30c7539cde Merge branch 'moderate-bio' of github.com:coralproject/talk into moderate-bio 2017-01-11 13:14:44 -05:00
David Jay e8dc319a9a Merge branch 'master' of github.com:coralproject/talk into moderate-bio 2017-01-11 13:12:07 -05:00
David Jay 98a6ba5f1f Merge branch 'master' into moderate-bio 2017-01-11 12:32:25 -05:00
David Jay 9d7810f48c Merge branch 'master' into moderate-bio 2017-01-10 19:48:09 -05:00
David Jay f957e3c8ce Closing suspend user modal once user is suspended and e-mail is sent. 2017-01-09 18:33:12 -05:00
David Jay 8fa7561e20 Updating item_type for consitency. 2017-01-09 18:27:54 -05:00
David Jay 01d9fc313e Merge branch 'master' of github.com:coralproject/talk into moderate-bio 2017-01-09 18:20:44 -05:00
David Jay 8601562418 Improving extraction of user e-mail address. 2017-01-09 18:18:19 -05:00
David Jay a2704e3099 Updating user email address. 2017-01-09 18:15:25 -05:00
David Jay 9809403701 Pluralizing user item_type for consistency. 2017-01-09 17:43:39 -05:00
David Jay b8fc2c9cc7 Addressing CSRF and user status issues. 2017-01-09 15:39:08 -05:00
David Jay 0f24ab6b28 Merge branch 'master' of github.com:coralproject/talk into moderate-bio 2017-01-06 15:56:03 -05:00
David Jay ec020df488 Adding account flags tab. 2017-01-06 15:42:19 -05:00
David Jay d5a82f04c2 Fixing issues with e-mail and status update. 2017-01-06 15:35:32 -05:00
David Jay 9e651a9292 Adding actions to suspend users and send e-mail. 2017-01-06 15:16:23 -05:00
David Jay 565e34e09d Adding endpoint for admins to send transactional e-mail. 2017-01-06 14:29:50 -05:00
David Jay e510f99754 Adding style to suspension modal. 2017-01-05 20:51:40 -05:00
David Jay a468fe4c28 Adding language and e-mail input to suspend user modal. 2017-01-05 16:51:37 -05:00
David Jay 890ab810f3 Activating SuspendUserModal and updating language for clarity. 2017-01-05 14:50:21 -05:00
David Jay 9ba0176b3c Seperating username and bio flags 2017-01-04 16:11:31 -05:00
David Jay 7a239c02ab Limiting to user actions. 2017-01-04 15:56:52 -05:00
David Jay 216b21251d Styling UserActions Component. 2017-01-04 15:33:41 -05:00
David Jay c1e3580ef7 Resolving error in moderation list. 2017-01-04 14:46:06 -05:00
David Jay 0a064c0e11 Merge branch 'master' of github.com:coralproject/talk into moderate-bio 2017-01-04 14:33:27 -05:00
David Jay 3fa0460f34 Sending actions to ModerationList. 2017-01-04 14:20:00 -05:00
David Jay f792aeccd2 Adding user display. 2017-01-04 13:39:10 -05:00
David Jay b517adab56 Moving commentList to ModerationList 2017-01-03 16:57:53 -05:00
David Jay 5c5a7f5ba5 Switching to central call for mod queue to support current reducer structure. 2017-01-03 15:13:09 -05:00
David Jay dd7bdb8a26 Adding actions for fetching user moderation queue. 2017-01-03 15:05:45 -05:00
David Jay 800382132d Merge branch 'master' of github.com:coralproject/talk into moderate-bio 2017-01-03 14:29:05 -05:00
David Jay dff8413404 Adding pending users and actions to moderation queue call. 2016-12-19 20:34:27 -05:00
David Jay 20505acfbe Updating tests for pending users endpoint. 2016-12-19 20:27:58 -05:00
David Jay 845d274b9b Creatind endpoint to return pending users and action summaries. 2016-12-19 19:54:20 -05:00
215 changed files with 6531 additions and 4758 deletions
+1
View File
@@ -1 +1,2 @@
node_modules
test
+1 -1
View File
@@ -17,7 +17,7 @@
"no-template-curly-in-string": [1],
"no-unsafe-negation": [1],
"array-callback-return": [1],
"eqeqeq": [2],
"eqeqeq": [2, "smart"],
"no-eval": [2],
"no-global-assign": [2],
"no-implied-eval": [2],
+2 -2
View File
@@ -3,13 +3,13 @@ npm-debug.log*
dist
!dist/coral-admin
dist/coral-admin/bundle.js
tests/e2e/reports
test/e2e/reports
.DS_Store
*.iml
*.swp
dump.rdb
.env
gaba.cfg
*.cfg
.idea/
coverage/
yarn.lock
+2 -1
View File
@@ -1,4 +1,5 @@
{
"verbose": true,
"ignore": ["tests/*", "client/*", "dist/*"]
"ignore": ["test/*", "client/*", "dist/*"],
"ext": "js,json,graphql"
}
-3
View File
@@ -1,3 +0,0 @@
language: node_js
node_js:
- 4
+20 -1
View File
@@ -10,6 +10,8 @@ const RedisStore = require('connect-redis')(session);
const redis = require('./services/redis');
const csrf = require('csurf');
const errors = require('./errors');
const graph = require('./graph');
const apollo = require('graphql-server-express');
const app = express();
@@ -49,7 +51,7 @@ const session_opts = {
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 18000000, // 30 minutes for expiry.
maxAge: 36000000, // 1 hour for expiry.
},
store: new RedisStore({
ttl: 1800,
@@ -77,6 +79,23 @@ app.use(session(session_opts));
app.use(passport.initialize());
app.use(passport.session());
//==============================================================================
// GraphQL Router
//==============================================================================
// GraphQL endpoint.
app.use('/api/v1/graph/ql', apollo.graphqlExpress(graph.createGraphOptions));
// Only include the graphiql tool if we aren't in production mode.
if (app.get('env') !== 'production') {
// Interactive graphiql interface.
app.use('/api/v1/graph/iql', apollo.graphiqlExpress({
endpointURL: '/api/v1/graph/ql'
}));
}
//==============================================================================
// CSRF MIDDLEWARE
//==============================================================================
+14 -28
View File
@@ -4,38 +4,13 @@
* Module dependencies.
*/
const program = require('commander');
const pkg = require('../package.json');
const dotenv = require('dotenv');
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.version(pkg.version)
.option('-c, --config [path]', 'Specify the configuration file to load')
.parse(process.argv);
if (program.config) {
let r = dotenv.config({
path: program.config
});
if (r.error) {
throw r.error;
}
}
// Perform rewrites to the runtime environment variables based on the contents
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
// entrypoint for the entire application.
require('env-rewrite').rewrite();
const util = require('../util');
const program = require('./commander');
program
.command('serve', 'serve the application')
.command('assets', 'interact with assets')
.command('settings', 'work with the application settings')
.command('setup', 'setup the application')
.command('jobs', 'work with the job queues')
.command('users', 'work with the application auth')
.parse(process.argv);
@@ -43,4 +18,15 @@ program
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
return;
}
// The ensures that the child process that is created here is always cleaned up
// properly when the parent process dies.
util.onshutdown([
(signal) => {
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
program.runningCommand.kill(signal);
}
}
]);
+4 -8
View File
@@ -4,11 +4,10 @@
* Module dependencies.
*/
const program = require('commander');
const pkg = require('../package.json');
const program = require('./commander');
const parseDuration = require('parse-duration');
const Table = require('cli-table');
const Asset = require('../models/asset');
const AssetModel = require('../models/asset');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('../util');
@@ -22,7 +21,7 @@ util.onshutdown([
* Lists all the assets registered in the database.
*/
function listAssets() {
Asset
AssetModel
.find({})
.sort({'created_at': 1})
.then((asset) => {
@@ -56,7 +55,7 @@ function refreshAssets(ageString) {
const ageMs = parseDuration(ageString);
const age = new Date(now - ageMs);
Asset.find({
AssetModel.find({
$or: [
{
scraped: {
@@ -86,9 +85,6 @@ function refreshAssets(ageString) {
// Setting up the program command line arguments.
//==============================================================================
program
.version(pkg.version);
program
.command('list')
.description('list all the assets in the database')
+1 -1
View File
@@ -4,7 +4,7 @@
* Module dependencies.
*/
const program = require('commander');
const program = require('./commander');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const util = require('../util');
+7 -19
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env node
const app = require('../app');
const program = require('./commander');
const debug = require('debug')('talk:server');
const http = require('http');
const init = require('../init');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const kue = require('../services/kue');
@@ -86,27 +86,15 @@ function onListening() {
* Start the app.
*/
function startApp() {
init().then(() => {
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
}
/**
* Module dependencies.
*/
const program = require('commander');
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const program = require('commander');
const mongoose = require('../services/mongoose');
const Setting = require('../models/setting');
const util = require('../util');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('init')
.description('initilizes the talk settings')
.action(() => {
const defaults = {id: '1', moderation: 'pre'};
Setting
.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
.then(() => {
console.log('Created settings object.');
util.shutdown();
})
.catch((err) => {
console.error(`failed to create the settings object ${JSON.stringify(err)}`);
util.shutdown(1);
});
});
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
util.shutdown();
}
Executable
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const program = require('./commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingModel = require('../models/setting');
const SettingsService = require('../services/settings');
const util = require('../util');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.description('runs the setup wizard to setup the application')
.option('--defaults', 'apply defaults for config instead of prompting')
.parse(process.argv);
//==============================================================================
// Setup the application
//==============================================================================
SettingsService
.init()
.then((settings) => {
if (program.defaults) {
return settings.save();
}
console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
return inquirer.prompt([
{
type: 'input',
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
{
type: 'list',
choices: SettingModel.MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
}
])
.then((answers) => {
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
});
return settings.save();
});
})
.then(() => {
console.log('Talk is now installed!');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
+152 -117
View File
@@ -4,105 +4,139 @@
* Module dependencies.
*/
const program = require('commander');
const pkg = require('../package.json');
const prompt = require('prompt');
const User = require('../models/user');
const program = require('./commander');
const inquirer = require('inquirer');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const mongoose = require('../services/mongoose');
const util = require('../util');
const Table = require('cli-table');
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
if (input && input.length >= len) {
return true;
}
return msg;
};
// Regeister the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
function getUserCreateAnswers(options) {
if (options.flag_mode) {
let user = {
email: options.email,
password: options.password,
confirmPassword: options.password,
displayName: options.name,
roles: []
};
if (options.role && UserModel.USER_ROLES.indexOf(options.role) > -1) {
user.roles = [options.role];
}
return Promise.resolve(user);
}
return inquirer.prompt([
{
name: 'email',
message: 'Email',
format: 'email',
validate: validateRequired('Email is required')
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'displayName',
message: 'Display Name',
filter: (displayName) => {
return UsersService
.isValidDisplayName(displayName)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'roles',
message: 'User Role',
type: 'checkbox',
choices: UserModel.USER_ROLES
}
]);
}
/**
* Prompts for input and registers a user based on those.
*/
function createUser(options) {
return new Promise((resolve, reject) => {
if (options.flag_mode) {
return resolve({
email: options.email,
password: options.password,
displayName: options.name,
role: options.role
});
}
prompt.start();
prompt.get([
{
name: 'email',
description: 'Email',
format: 'email',
required: true
},
{
name: 'password',
description: 'Password',
hidden: true,
required: true
},
{
name: 'confirmPassword',
description: 'Confirm Password',
hidden: true,
required: true
},
{
name: 'displayName',
description: 'Display Name',
required: true
},
{
name: 'role',
description: 'User Role',
required: false
}
], (err, result) => {
if (err) {
return reject(err);
getUserCreateAnswers(options)
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
if (result.password !== result.confirmPassword) {
return reject(new Error('Passwords do not match'));
}
return answers;
})
.then((answers) => {
return UsersService
.createLocalUser(answers.email.trim(), answers.password.trim(), answers.displayName.trim())
.then((user) => {
console.log(`Created user ${user.id}.`);
resolve(result);
});
})
.then((result) => {
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
.then((user) => {
console.log(`Created user ${user.id}.`);
if (result.role && result.role.length > 0) {
return User
.addRoleToUser(user.id, result.role.trim())
.then(() => {
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
util.shutdown();
});
} else {
util.shutdown();
}
if (answers.roles.length > 0) {
return Promise.all(answers.roles.map((role) => {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the role ${role} to User ${user.id}.`);
});
}));
}
});
})
.then(() => {
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown();
});
});
}
/**
* Deletes a user.
*/
function deleteUser(userID) {
User
UserModel
.findOneAndRemove({
id: userID
})
@@ -120,44 +154,34 @@ function deleteUser(userID) {
* Changes the password for a user.
*/
function passwd(userID) {
prompt.start();
prompt.get([
inquirer.prompt([
{
name: 'password',
description: 'Password',
hidden: true,
required: true
message: 'Password',
type: 'password',
validate: validateRequired('Password is required')
},
{
name: 'confirmPassword',
description: 'Confirm Password',
hidden: true,
required: true
message: 'Confirm Password',
type: 'password',
validate: validateRequired('Confirm Password is required')
}
], (err, result) => {
if (err) {
console.error(err);
util.shutdown();
return;
])
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
return Promise.reject(new Error('Password mismatch'));
}
if (result.password !== result.confirmPassword) {
console.error(new Error('Password mismatch'));
util.shutdown(1);
return;
}
User
.changePassword(userID, result.password)
.then(() => {
console.log('Password changed.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
return UsersService.changePassword(userID, answers.password);
})
.then(() => {
console.log('Password changed.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
@@ -168,7 +192,7 @@ function updateUser(userID, options) {
const updates = [];
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
let q = User.update({
let q = UserModel.update({
'id': userID,
'profiles.provider': 'local'
}, {
@@ -181,7 +205,7 @@ function updateUser(userID, options) {
}
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
let q = User.update({
let q = UserModel.update({
'id': userID
}, {
$set: {
@@ -208,7 +232,7 @@ function updateUser(userID, options) {
* Lists all the users registered in the database.
*/
function listUsers() {
User
UsersService
.all()
.then((users) => {
let table = new Table({
@@ -248,7 +272,7 @@ function listUsers() {
* @param {String} srcUserID id of the user to which is the source of the merge
*/
function mergeUsers(dstUserID, srcUserID) {
User
UsersService
.mergeUsers(dstUserID, srcUserID)
.then(() => {
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
@@ -266,7 +290,14 @@ function mergeUsers(dstUserID, srcUserID) {
* @param {String} role the role to add
*/
function addRole(userID, role) {
User
if (UserModel.USER_ROLES.indexOf(role) === -1) {
console.error(`Role '${role}' is not supported. Supported roles are ${UserModel.USER_ROLES.join(', ')}.`);
util.shutdown(1);
return;
}
UsersService
.addRoleToUser(userID, role)
.then(() => {
console.log(`Added the ${role} role to User ${userID}.`);
@@ -284,7 +315,14 @@ function addRole(userID, role) {
* @param {String} role the role to remove
*/
function removeRole(userID, role) {
User
if (UserModel.USER_ROLES.indexOf(role) === -1) {
console.error(`Role '${role}' is not supported. Supported roles are ${UserModel.USER_ROLES.join(', ')}.`);
util.shutdown(1);
return;
}
UsersService
.removeRoleFromUser(userID, role)
.then(() => {
console.log(`Removed the ${role} role from User ${userID}.`);
@@ -301,8 +339,8 @@ function removeRole(userID, role) {
* @param {String} userID id of the user to ban
*/
function ban(userID) {
User
.setStatus(userID, 'banned', '')
UsersService
.setStatus(userID, 'BANNED')
.then(() => {
console.log(`Banned the User ${userID}.`);
util.shutdown();
@@ -318,8 +356,8 @@ function ban(userID) {
* @param {String} userUD id of the user to remove the role from
*/
function unban(userID) {
User
.setStatus(userID, 'active', '')
UsersService
.setStatus(userID, 'ACTIVE')
.then(() => {
console.log(`Unban the User ${userID}.`);
util.shutdown();
@@ -335,7 +373,7 @@ function unban(userID) {
* @param {String} userID the ID of a user to disable
*/
function disableUser(userID) {
User
UsersService
.disableUser(userID)
.then(() => {
console.log(`User ${userID} was disabled.`);
@@ -352,7 +390,7 @@ function disableUser(userID) {
* @param {String} userID the ID of a user to enable
*/
function enableUser(userID) {
User
UsersService
.enableUser(userID)
.then(() => {
console.log(`User ${userID} was enabled.`);
@@ -368,9 +406,6 @@ function enableUser(userID) {
// Setting up the program command line arguments.
//==============================================================================
program
.version(pkg.version);
program
.command('create')
.option('--email [email]', 'Email to use')
+46
View File
@@ -0,0 +1,46 @@
const pkg = require('../package.json');
const dotenv = require('dotenv');
const fs = require('fs');
const program = require('commander');
const util = require('../util');
// Perform rewrites to the runtime environment variables based on the contents
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
// entrypoint for the entire application.
require('env-rewrite').rewrite();
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
const parseArgs = require('minimist')(process.argv.slice(2), {
alias: {
'c': 'config'
},
string: [
'config',
'pid'
],
default: {
'config': null,
'pid': null
}
});
if (parseArgs.config) {
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
Object.keys(envConfig).forEach((k) => {
process.env[k] = envConfig[k];
});
}
// If the `--pid` flag is used, put the current pid there.
if (parseArgs.pid) {
util.pid(parseArgs.pid);
}
module.exports = program
.version(pkg.version)
.option('-c, --config [path]', 'Specify the configuration file to load')
.option('--pid [path]', 'Specify a path to output the current PID to');
+17 -3
View File
@@ -3,12 +3,26 @@ machine:
version: 7
services:
- docker
- redis
dependencies:
post:
# Lint the project here, before tests are ran.
- npm run lint
database:
post:
# Initialize the settings in the database, this will create indicies for the
# database.
- ./bin/cli setup --defaults
- sleep 2
test:
override:
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml ./node_modules/.bin/mocha tests --reporter mocha-junit-reporter
- npm run lint
- npm run build
# Run the tests using the junit reporter.
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml NPM_PACKAGE_CONFIG_MOCHA_REPORTER=mocha-junit-reporter npm run test
# Run the e2e test suite
- E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e npm run e2e
deployment:
release:
+5 -2
View File
@@ -11,10 +11,13 @@ export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then(result => {
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => dispatch(checkLoginFailure(error)));
.catch(error => {
console.error(error);
dispatch(checkLoginFailure(`${error.message}`));
});
};
// LogOut Actions
+14 -4
View File
@@ -15,17 +15,18 @@ export const fetchModerationQueueComments = () => {
return Promise.all([
coralApi('/queue/comments/pending'),
coralApi('/queue/users/pending'),
coralApi('/queue/comments/rejected'),
coralApi('/queue/comments/flagged')
])
.then(([pending, rejected, flagged]) => {
.then(([pendingComments, pendingUsers, rejected, flagged]) => {
/* Combine seperate calls into a single object */
flagged.comments.forEach(comment => comment.flagged = true);
return {
comments: [...pending.comments, ...rejected.comments, ...flagged.comments],
users: [...pending.users, ...rejected.users, ...flagged.users],
actions: [...pending.actions, ...rejected.actions, ...flagged.actions]
comments: [...pendingComments.comments, ...rejected.comments, ...flagged.comments],
users: [...pendingComments.users, ...pendingUsers.users, ...rejected.users, ...flagged.users],
actions: [...pendingComments.actions, ...pendingUsers.actions, ...rejected.actions, ...flagged.actions]
};
})
.then(addUsersCommentsActions.bind(this, dispatch));
@@ -41,6 +42,15 @@ export const fetchPendingQueue = () => {
};
};
export const fetchPendingUsersQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/users/pending')
.then(addUsersCommentsActions.bind(this, dispatch));
};
};
export const fetchRejectedQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
+12 -4
View File
@@ -1,5 +1,5 @@
import coralApi from '../../../coral-framework/helpers/response';
import * as actions from '../constants/user';
import * as userTypes from '../constants/users';
/**
* Action disptacher related to users
@@ -7,9 +7,17 @@ import * as actions from '../constants/user';
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch) => {
dispatch({type: actions.UPDATE_STATUS_REQUEST});
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res}))
.catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error}));
.then(res => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch(error => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
};
};
// change status of a user
export const sendNotificationEmail = (userId, subject, body) => {
return (dispatch) => {
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
.catch(error => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
};
};
@@ -0,0 +1,48 @@
import React from 'react';
import styles from './ModerationList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import {FabButton, Button, Icon} from 'coral-ui';
const ActionButton = ({option, type, comment = {}, user, menuOptionsMap, onClickAction, onClickShowBanDialog}) =>
{
const banned = user.status === 'BANNED';
if (option === 'flag' && (type === 'USERS' || comment.status || comment.flagged === true)) {
return null;
}
if (option === 'ban') {
return (
<div className={styles.ban}>
<Button
className={`ban ${styles.banButton}`}
cStyle='darkGrey'
disabled={banned ? 'disabled' : ''}
onClick={() => onClickShowBanDialog(user.id, user.displayName, comment.id)
}
raised
>
<Icon name='not_interested' className={styles.banIcon} />
{lang.t('comment.ban_user')}
</Button>
</div>
);
}
const menuOption = menuOptionsMap[option];
const action = {
item_type: type,
item_id: type === 'COMMENTS' ? comment.id : user.id
};
return (
<FabButton
className={`${option} ${styles.actionButton}`}
cStyle={option}
icon={menuOption.icon}
onClick={() => onClickAction(menuOption.status, type === 'COMMENTS' ? comment : user, action)}
/>
);
};
export default ActionButton;
const lang = new I18n(translations);
@@ -35,7 +35,7 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => (
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} raised>
{lang.t('bandialog.cancel')}
</Button>
<Button cStyle="black" className={styles.ban} onClick={() => onClickBanUser(user.userId, user.commentId)} raised>
<Button cStyle="black" className={styles.ban} onClick={() => onClickBanUser('BANNED', user.userId, user.commentId)} raised>
{lang.t('bandialog.yes_ban_user')}
</Button>
</div>
+17 -41
View File
@@ -2,18 +2,19 @@ import React from 'react';
import timeago from 'timeago.js';
import Linkify from 'react-linkify';
import styles from './CommentList.css';
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 {FabButton, Button, Icon} from 'coral-ui';
import {Icon} from 'coral-ui';
import ActionButton from './ActionButton';
const linkify = new Linkify();
// Render a single comment for the list
export default props => {
const Comment = props => {
const {comment, author} = props;
let authorStatus = author.status;
const links = linkify.getMatches(comment.body);
@@ -30,7 +31,18 @@ export default props => {
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{props.modActions.map((action, i) => getActionButton(action, i, props))}
{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}
@@ -49,43 +61,7 @@ export default props => {
);
};
// Get the button of the action performed over a comment if any
const getActionButton = (action, i, props) => {
const {comment, author} = props;
const status = comment.status;
const flagged = comment.flagged;
const banned = (author.status === 'banned');
if (action === 'flag' && (status || flagged === true)) {
return null;
}
if (action === 'ban') {
return (
<div className={styles.ban}>
<Button
className={`ban ${styles.banButton}`}
cStyle='darkGrey'
disabled={banned ? 'disabled' : ''}
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
key={i}
raised
>
<Icon name='not_interested' className={styles.banIcon} />
{lang.t('comment.ban_user')}
</Button>
</div>
);
}
return (
<FabButton
className={`${action} ${styles.actionButton}`}
cStyle={action}
icon={props.actionsMap[action].icon}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, comment)}
/>
);
};
export default Comment;
const linkStyles = {
backgroundColor: 'rgb(255, 219, 135)',
@@ -1,166 +0,0 @@
import React, {PropTypes} from 'react';
import styles from './CommentList.css';
import key from 'keymaster';
import Hammer from 'hammerjs';
import Comment from 'components/Comment';
// Each action has different meaning and configuration
const modActions = {
'reject': {status: 'rejected', icon: 'close', key: 'r'},
'approve': {status: 'accepted', icon: 'done', key: 't'},
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'},
'ban': {status: 'banned', icon: 'not interested'}
};
// Renders a comment list and allow performing actions
export default class CommentList extends React.Component {
static propTypes = {
isActive: PropTypes.bool,
singleView: PropTypes.bool,
commentIds: PropTypes.arrayOf(PropTypes.string).isRequired,
comments: PropTypes.object.isRequired,
users: PropTypes.object.isRequired,
onClickAction: PropTypes.func,
// list of actions (flags, etc) associated with the comments
modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
loading: PropTypes.bool,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
}
constructor (props) {
super(props);
this.state = {active: null};
this.onClickAction = this.onClickAction.bind(this);
this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this);
}
// remove key handlers before leaving
componentWillUnmount () {
this.unbindKeyHandlers();
}
// add key handlers and gestures
componentDidMount () {
this.bindKeyHandlers();
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
}
// If entering to singleview and no active, active is the first eleement
componentWillReceiveProps (nextProps) {
if (nextProps.singleView && !this.state.active) {
this.setState({active: nextProps.commentIds[0]});
}
}
// Add swipe to approve or reject
bindGestures () {
const {modActions} = this.props;
this._hammer = new Hammer(this.base);
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
if (modActions.indexOf('reject') !== -1) {
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
}
if (modActions.indexOf('approve') !== -1) {
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
}
}
// Add key handlers. Each action has one and added j/k for moving around
bindKeyHandlers () {
this.props.modActions.filter(action => modActions[action].key).forEach(action => {
key(modActions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(modActions[action].status));
});
key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down'));
key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up'));
key.setScope('commentList');
}
// Perform an action using the keys only if the comment is active
actionKeyHandler (action) {
if (this.props.isActive && this.state.active) {
this.onClickAction(action, this.state.active);
}
}
// move around with j/k
moveKeyHandler (direction) {
if (!this.props.isActive) {
return;
}
const {commentIds} = this.props;
const {active} = this.state;
// check boundaries
if (active === null || !commentIds.length) {
this.setState({active: commentIds[0]});
} else if (direction === 'up' && active !== commentIds[0]) {
this.setState({active: commentIds[commentIds.indexOf(active) - 1]});
} else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) {
this.setState({active: commentIds[commentIds.indexOf(active) + 1]});
}
// scroll to the position
const index = Math.max(commentIds.indexOf(this.state.active), 0);
this.base.childNodes[index] && this.base.childNodes[index].focus();
}
unbindKeyHandlers () {
key.deleteScope('commentList');
}
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
// TODO: In the future this can be improved and look at the actual state to
// resolve since the content of the list could change externally. For now it works as expected
onClickAction (action, id, author_id) {
// activate the next comment
if (id === this.state.active) {
const {commentIds} = this.props;
if (commentIds[commentIds.length - 1] === this.state.active) {
this.setState({active: commentIds[commentIds.length - 2]});
} else {
this.setState({active: commentIds[Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.length - 1)]});
}
}
this.props.onClickAction(action, id, author_id);
}
onClickShowBanDialog(userId, userName, commentId) {
this.props.onClickShowBanDialog(userId, userName, commentId);
}
render () {
const {singleView, commentIds, comments, users, hideActive, key, suspectWords} = this.props;
const {active} = this.state;
return (
<ul
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
id='commentList'
>
{commentIds.map((commentId, index) => {
const comment = comments[commentId];
const author = users[comment.author_id];
return <Comment
suspectWords={suspectWords}
comment={comment}
author={author}
key={index}
index={index}
onClickAction={this.onClickAction}
onClickShowBanDialog={this.onClickShowBanDialog}
modActions={this.props.modActions}
actionsMap={modActions}
isActive={commentId === active}
hideActive={hideActive} />;
})}
</ul>
);
}
}
@@ -19,7 +19,6 @@
.inner {
width: 100vw;
height: 100vh;
background-color: #fff;
box-shadow: 2px 2px 5px 0px rgba(153,153,153,1);
padding: 20px;
@@ -37,7 +36,6 @@
@media (--big-viewport) {
.inner {
width: 600px;
height: 50vh;
border-radius: 4px;
}
}
@@ -69,6 +69,7 @@
justify-content: space-between;
.author {
min-width: 230px;
display: flex;
align-items: center;
}
@@ -112,6 +113,12 @@
padding-top: 15px;
padding-left: 10px;
}
.flagCount{
font-size: 12px;
color: #d32f2f;
}
}
.empty {
@@ -0,0 +1,227 @@
import React, {PropTypes} from 'react';
import styles from './ModerationList.css';
import key from 'keymaster';
import Hammer from 'hammerjs';
import Comment from './Comment';
import UserAction from './UserAction';
import SuspendUserModal from './SuspendUserModal';
// Each action has different meaning and configuration
const menuOptionsMap = {
'reject': {status: 'REJECTED', icon: 'close', key: 'r'},
'approve': {status: 'ACCEPTED', icon: 'done', key: 't'},
'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
'ban': {status: 'BANNED', icon: 'not interested'}
};
// Renders a comment list and allow performing actions
export default class ModerationList extends React.Component {
static propTypes = {
isActive: PropTypes.bool,
singleView: PropTypes.bool,
commentIds: PropTypes.arrayOf(PropTypes.string),
actionIds: PropTypes.arrayOf(PropTypes.string),
comments: PropTypes.object,
users: PropTypes.object.isRequired,
actions: PropTypes.object,
userStatusUpdate: PropTypes.func.isRequired,
suspendUser: PropTypes.func.isRequired,
// list of actions (flags, etc) associated with the comments
modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
loading: PropTypes.bool,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
}
state = {active: null, suspendUserModal: null, email: null};
// remove key handlers before leaving
componentWillUnmount () {
this.unbindKeyHandlers();
}
// add key handlers and gestures
componentDidMount () {
this.bindKeyHandlers();
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
}
// If entering to singleview and no active, active is the first eleement
componentWillReceiveProps (nextProps) {
if (nextProps.singleView && !this.state.active) {
this.setState({active: nextProps.commentIds[0]});
}
}
// Add swipe to approve or reject
bindGestures () {
const {modActions} = this.props;
this._hammer = new Hammer(this.base);
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
if (modActions.indexOf('reject') !== -1) {
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
}
if (modActions.indexOf('approve') !== -1) {
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
}
}
// Add key handlers. Each action has one and added j/k for moving around
bindKeyHandlers () {
const {modActions, isActive} = this.props;
modActions.filter(action => menuOptionsMap[action].key).forEach(action => {
key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status));
});
key('j', 'moderationList', () => isActive && this.moveKeyHandler('down'));
key('k', 'moderationList', () => isActive && this.moveKeyHandler('up'));
key.setScope('moderationList');
}
// Perform an action using the keys only if the comment is active
actionKeyHandler (action) {
if (this.props.isActive && this.state.active) {
this.onClickAction(action, this.state.active);
}
}
// move around with j/k
moveKeyHandler (direction) {
if (!this.props.isActive) {
return;
}
const {commentIds} = this.props;
const {active} = this.state;
// check boundaries
if (active === null || !commentIds.length) {
this.setState({active: commentIds[0]});
} else if (direction === 'up' && active !== commentIds[0]) {
this.setState({active: commentIds[commentIds.indexOf(active) - 1]});
} else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) {
this.setState({active: commentIds[commentIds.indexOf(active) + 1]});
}
// scroll to the position
const index = Math.max(commentIds.indexOf(this.state.active), 0);
this.base.childNodes[index] && this.base.childNodes[index].focus();
}
unbindKeyHandlers () {
key.deleteScope('moderationList');
}
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
// TODO: In the future this can be improved and look at the actual state to
// resolve since the content of the list could change externally. For now it works as expected
onClickAction = (menuOption, id, action) => {
// activate the next comment
if (id === this.state.active) {
const moderationIds = this.getModerationIds();
if (moderationIds[moderationIds.length - 1] === this.state.active) {
this.setState({active: moderationIds[moderationIds.length - 2]});
} else {
this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]});
}
}
// Update the status right away if this is a comment
if (action.item_type === 'COMMENTS') {
this.props.updateCommentStatus(menuOption, id);
} else if (action.item_type === 'USERS') {
// If a user bio or name is rejected, bring up a dialog before suspending them.
if (menuOption === 'REJECTED') {
this.setState({suspendUserModal: action});
} else if (menuOption === 'ACCEPTED') {
this.props.userStatusUpdate('ACTIVE', action.item_id);
}
}
}
onClickShowBanDialog = (userId, userName, commentId) => {
this.props.onClickShowBanDialog(userId, userName, commentId);
}
mapModItems = (itemId, index) => {
const {comments = {}, users, actions = {}, modActions, suspectWords, hideActive} = this.props;
const {active} = this.state;
// Because ids are unique, the id will either appear as an action or as a comment.
const item = comments[itemId] || actions[itemId];
let modItem;
if (item.body) {
// If the item is a comment...
const author = users[item.author_id];
modItem = <Comment
suspectWords={suspectWords}
comment={item}
author={author}
key={index}
index={index}
onClickAction={this.onClickAction}
onClickShowBanDialog={this.onClickShowBanDialog}
modActions={modActions}
menuOptionsMap={menuOptionsMap}
isActive={itemId === active}
hideActive={hideActive} />;
} else {
// If the item is an action...
const user = users[item.item_id];
modItem = user && <UserAction
suspectWords={suspectWords}
action={item}
user={user}
key={index}
index={index}
onClickAction={this.onClickAction}
onClickShowBanDialog={this.onClickShowBanDialog}
modActions={modActions}
menuOptionsMap={menuOptionsMap}
isActive={itemId === active}
hideActive={hideActive} />;
}
return modItem;
}
getModerationIds = () => {
const {commentIds = [], actionIds = [], comments, actions} = this.props;
if (comments && actions) {
return [ ...commentIds, ...actionIds ].sort((a, b) => {
const itemA = comments[a] || actions[a];
const itemB = comments[b] || actions[b];
return itemB.updated_at - itemA.updated_at;
});
} else {
return comments ? commentIds : actionIds;
}
}
render () {
const {singleView, key, suspendUser} = this.props;
// Combine moderations and actions into a single stream and sort by most recently updated.
const moderationIds = this.getModerationIds();
return (
<ul
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
id='moderationList'>
{moderationIds.map(this.mapModItems)}
<SuspendUserModal
action = {this.state.suspendUserModal}
onClose={() => this.setState({suspendUserModal:null})}
suspendUser={suspendUser} />
</ul>
);
}
}
@@ -0,0 +1,31 @@
.modalButtons {
display: flex;
justify-content: flex-end;
margin-top: 50px;
}
.emailInput {
border-radius: 3px;
width: 100%;
padding: 10px;
font-size: 14px;
}
.writeContainer {
margin-top: 20px;
}
.emailContainer {
display: flex;
}
.emailMessage {
font-weight: 800;
}
.title {
font-size: 20px;
font-weight: 800;
margin-bottom: 20px;
}
@@ -0,0 +1,106 @@
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 = this.props.actionType === 'flag_bio' ? lang.t('suspenduser.bio') : 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);
@@ -0,0 +1,79 @@
import React from 'react';
import Linkify from 'react-linkify';
import styles from './ModerationList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import {Icon} from 'react-mdl';
import Highlighter from 'react-highlight-words';
import ActionButton from './ActionButton';
const linkify = new Linkify();
// Render a single comment for the list
const UserAction = props => {
const {action, user} = props;
let userStatus = user.status;
const links = user.settings.bio ? linkify.getMatches(user.settings.bio) : [];
// 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.displayName}</span>
</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='USERS'
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>
{
user.settings.bio &&
<div>
<div className={styles.itemBody}>
<div>{lang.t('user.user_bio')}:</div>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter
searchWords={props.suspectWords}
textToHighlight={user.settings.bio} />
</Linkify>
</span>
</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 UserAction;
const linkStyles = {
backgroundColor: 'rgb(255, 219, 135)',
padding: '1px 2px'
};
const lang = new I18n(translations);
@@ -1,3 +1,5 @@
export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST';
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 USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
@@ -3,11 +3,11 @@ import styles from './CommentStream.css';
import {Snackbar} from 'react-mdl';
import {connect} from 'react-redux';
import {createComment, flagComment} from 'actions/comments';
import CommentList from 'components/CommentList';
import ModerationList from 'components/ModerationList';
import CommentBox from 'components/CommentBox';
/**
* Renders a comment stream using a CommentList component
* Renders a comment stream using a ModerationList component
* and adds a box for adding a new comment
*/
@@ -39,12 +39,12 @@ class CommentStream extends React.Component {
}
}
// Render the comment box along with the CommentList
// Render the comment box along with the ModerationList
render ({comments, users}, {snackbar, snackbarMsg}) {
return (
<div className={styles.container}>
<CommentBox onSubmit={this.onSubmit}/>
<CommentList isActive hideActive
<CommentBox onSubmit={this.onSubmit} />
<ModerationList isActive hideActive
singleView={false}
commentIds={comments.ids}
comments={comments.byId}
@@ -55,8 +55,8 @@ class Table extends Component {
className={styles.selectField}
label={lang.t('community.status')}
onChange={status => this.onCommenterStatusChange(row.id, status)}>
<Option value={'active'}>{lang.t('community.active')}</Option>
<Option value={'banned'}>{lang.t('community.banned')}</Option>
<Option value={'ACTIVE'}>{lang.t('community.active')}</Option>
<Option value={'BANNED'}>{lang.t('community.banned')}</Option>
</SelectField>
</td>
<td className="mdl-data-table__cell--non-numeric">
@@ -65,8 +65,8 @@ class Table extends Component {
label={lang.t('community.role')}
onChange={role => this.onRoleChange(row.id, role)}>
<Option value={''}>.</Option>
<Option value={'moderator'}>{lang.t('community.moderator')}</Option>
<Option value={'admin'}>{lang.t('community.admin')}</Option>
<Option value={'MODERATOR'}>{lang.t('community.moderator')}</Option>
<Option value={'ADMIN'}>{lang.t('community.admin')}</Option>
</SelectField>
</td>
</tr>
@@ -28,7 +28,7 @@ const updateCharCount = (updateSettings, settingsError) => (event) => {
};
const updateModeration = (updateSettings, mod) => () => {
const moderation = mod === 'pre' ? 'post' : 'pre';
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
updateSettings({moderation});
};
@@ -70,15 +70,15 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
return (
<div className={styles.commentSettingsSection}>
<h3>{title}</h3>
<Card className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? styles.enabledSetting : styles.disabledSetting}`}>
<div className={styles.action}>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'pre'} />
checked={settings.moderation === 'PRE'} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'pre' ? '' : styles.disabledSettingText}>
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</div>
@@ -32,4 +32,3 @@ export default connect(
mapStateToProps,
mapDispatchToProps
)(LayoutContainer);
@@ -11,7 +11,7 @@ import {
fetchFlaggedQueue,
fetchModerationQueueComments,
} from 'actions/comments';
import {userStatusUpdate} from 'actions/users';
import {userStatusUpdate, sendNotificationEmail} from 'actions/users';
import {fetchSettings} from 'actions/settings';
import ModerationQueue from './ModerationQueue';
@@ -22,7 +22,7 @@ class ModerationContainer extends React.Component {
super(props);
this.state = {
activeTab: 'pending',
activeTab: 'all',
singleView: false,
modalOpen: false
};
@@ -74,20 +74,22 @@ class ModerationContainer extends React.Component {
}
render () {
const {comments} = this.props;
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'premod');
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'rejected');
const {comments, actions} = this.props;
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD');
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED');
const flaggedIds = comments.ids.filter(id =>
comments.byId[id].flagged === true &&
comments.byId[id].status !== 'rejected' &&
comments.byId[id].status !== 'accepted'
comments.byId[id].status !== 'REJECTED' &&
comments.byId[id].status !== 'ACCEPTED'
);
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
return (
<ModerationQueue
onTabClick={this.onTabClick}
onClose={this.onClose}
premodIds={premodIds}
userActionIds={userActionIds}
rejectedIds={rejectedIds}
flaggedIds={flaggedIds}
{...this.props}
@@ -100,7 +102,8 @@ class ModerationContainer extends React.Component {
const mapStateToProps = state => ({
comments: state.comments.toJS(),
settings: state.settings.toJS(),
users: state.users.toJS()
users: state.users.toJS(),
actions: state.actions.toJS(),
});
const mapDispatchToProps = dispatch => {
@@ -112,9 +115,13 @@ const mapDispatchToProps = dispatch => {
fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()),
showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
banUser: (userId, commentId) => dispatch(userStatusUpdate('banned', userId, commentId)).then(() => {
userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => {
dispatch(fetchModerationQueueComments());
}),
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('suspended', userId))
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
.then(() => dispatch(fetchModerationQueueComments()))
,
updateStatus: (action, comment) => dispatch(updateStatus(action, comment))
};
};
@@ -2,7 +2,7 @@ import React from 'react';
import styles from './ModerationQueue.css';
import ModerationKeysModal from 'components/ModerationKeysModal';
import CommentList from 'components/CommentList';
import ModerationList from 'components/ModerationList';
import BanUserDialog from 'components/BanUserDialog';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -10,23 +10,40 @@ import translations from '../../translations.json';
const lang = new I18n(translations);
export default ({onTabClick, ...props}) => (
export default (props) => (
<div>
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<a href='#all'
onClick={(e) => {
e.preventDefault();
props.onTabClick('all');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`}
>
{lang.t('modqueue.all')}
</a>
<a href='#pending'
onClick={(e) => {
e.preventDefault();
onTabClick('pending');
props.onTabClick('pending');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'pending' ? styles.active : ''}`}
>
{lang.t('modqueue.pending')}
</a>
<a href='#account'
onClick={(e) => {
e.preventDefault();
props.onTabClick('account');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
{lang.t('modqueue.account')}
</a>
<a href='#rejected'
onClick={(e) => {
e.preventDefault();
onTabClick('rejected');
props.onTabClick('rejected');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
>
@@ -35,69 +52,146 @@ export default ({onTabClick, ...props}) => (
<a href='#flagged'
onClick={(e) => {
e.preventDefault();
onTabClick('flagged');
props.onTabClick('flagged');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
>
{lang.t('modqueue.flagged')}
</a>
</div>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='all'>
{
props.activeTab === 'pending'
? <div>
<CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'pending'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading} />
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.banUser}
user={props.comments.banUser} />
</div>
: null
props.activeTab === 'all' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'all'}
singleView={props.singleView}
commentIds={[...props.premodIds, ...props.flaggedIds]}
comments={props.comments.byId}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
{
props.activeTab === 'rejected'
? <CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading} />
: null
}
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
{
props.activeTab === 'pending' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'pending'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='account'>
{
props.activeTab === 'account' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'account'}
singleView={props.singleView}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
{
props.activeTab === 'flagged'
? <CommentList
props.activeTab === 'flagged' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'flagged'}
singleView={props.singleView}
commentIds={props.flaggedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
updateCommentStatus={props.updateStatus}
modActions={['reject', 'approve']}
loading={props.comments.loading} />
: null
}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
{
props.activeTab === 'rejected' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading}
/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
</div>
</div>
+9 -6
View File
@@ -1,8 +1,9 @@
import {Map, Set} from 'immutable';
import {Map, Set, fromJS} from 'immutable';
import * as types from '../constants/actions';
const initialState = Map({
ids: Set()
ids: Set(),
byId: Map()
});
export default (state = initialState, action) => {
@@ -14,11 +15,13 @@ export default (state = initialState, action) => {
};
const addActions = (state, action) => {
const ids = action.actions.map(action => action.item_id);
// Make ids that are unique by item_id and by action type
const typeId = (action) => `${action.action_type}_${action.item_id}`;
const ids = action.actions.map(action => typeId(action));
const map = action.actions.reduce((memo, action) => {
memo[action.item_id] = action;
memo[typeId(action)] = action;
return memo;
}, {});
const newIds = state.get('ids').concat(ids);
return state.merge(map).set('ids', newIds);
return state.set('byId', fromJS(map)).set('ids', new Set(ids));
};
+1 -1
View File
@@ -1,5 +1,5 @@
import * as actions from '../constants/comments';
import * as userActions from '../constants/user';
import * as userActions from '../constants/users';
import {Map, List, fromJS} from 'immutable';
/**
+26
View File
@@ -16,9 +16,11 @@
"loading": "Loading results"
},
"modqueue": {
"all": "all",
"pending": "pending",
"rejected": "rejected",
"flagged": "flagged",
"account": "account flags",
"shortcuts": "Shortcuts",
"close": "Close",
"actions": "Actions",
@@ -37,6 +39,11 @@
"ban_user": "Ban User",
"banned_user": "Banned User"
},
"user": {
"user_bio": "User Bio",
"bio_flags": "flags for this bio",
"username_flags": "flags for this username"
},
"embedlink": {
"copy": "Copy to Clipboard"
},
@@ -79,6 +86,20 @@
"cancel": "Cancel",
"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_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",
"yes_suspend": "Yes, suspend",
"send": "Send",
"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.",
"write_message": "Write a message"
},
"streams": {
"search": "Search",
"filter-streams": "Filter Streams",
@@ -126,6 +147,11 @@
"ban_user": "Suspender Usuario",
"banned_user": "Usuario Suspendido"
},
"user": {
"user_bio": "",
"bio_flags": "",
"username_flags": ""
},
"configure": {
"enable-pre-moderation": "Habilitar pre-moderación",
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
@@ -7,47 +7,50 @@ import translations from '../translations.json';
const lang = new I18n(translations);
export default ({handleChange, handleApply, changed, ...props}) => (
<div className={styles.wrapper}>
<div className={styles.container}>
<h3>{lang.t('configureCommentStream.title')}</h3>
<p>{lang.t('configureCommentStream.description')}</p>
<Button
className={styles.apply}
cStyle={changed ? 'green' : 'darkGrey'}
onClick={handleApply}
>
{lang.t('configureCommentStream.apply')}
</Button>
</div>
<ul>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premod"
<form onSubmit={handleApply}>
<div className={styles.wrapper}>
<div className={styles.container}>
<h3>{lang.t('configureCommentStream.title')}</h3>
<p>{lang.t('configureCommentStream.description')}</p>
<Button
type="submit"
className={styles.apply}
onChange={handleChange}
checked={props.premod}
info={{
title: lang.t('configureCommentStream.enablePremod'),
description: lang.t('configureCommentStream.enablePremodDescription')
}}
/>
<ul>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premodLinks"
onChange={handleChange}
checked={props.premodLinks}
info={{
title: lang.t('configureCommentStream.enablePremodLinks'),
description: lang.t('configureCommentStream.enablePremodDescription')
}}
/>
</li>
</ul>
</li>
</ul>
</div>
cStyle={changed ? 'green' : 'darkGrey'} >
{lang.t('configureCommentStream.apply')}
</Button>
</div>
<ul>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premod"
onChange={handleChange}
defaultChecked={props.premod}
info={{
title: lang.t('configureCommentStream.enablePremod'),
description: lang.t('configureCommentStream.enablePremodDescription')
}} />
{/* To be implimented
<ul>
<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.enablePremodDescription')
}} />
</li>
</ul>
*/}
</li>
</ul>
</div>
</form>
);
@@ -1,8 +1,9 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {I18n} from '../../coral-framework';
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config';
import {I18n} from 'coral-framework';
import {updateOpenStatus, updateConfiguration} from 'coral-framework/actions/asset';
import CloseCommentsInfo from '../components/CloseCommentsInfo';
import ConfigureCommentStream from '../components/ConfigureCommentStream';
@@ -14,8 +15,7 @@ class ConfigureStreamContainer extends Component {
super(props);
this.state = {
premod: props.config.moderation === 'pre',
premodLinks: false
changed: false
};
this.toggleStatus = this.toggleStatus.bind(this);
@@ -23,11 +23,18 @@ class ConfigureStreamContainer extends Component {
this.handleApply = this.handleApply.bind(this);
}
handleApply () {
const {premod, changed} = this.state;
handleApply (e) {
e.preventDefault();
const {elements} = e.target;
const premod = elements.premod.checked;
// const premodLinks = elements.premodLinks.checked;
const {changed} = this.state;
const newConfig = {
moderation: premod ? 'pre' : 'post'
moderation: premod ? 'PRE' : 'POST'
};
if (changed) {
this.props.updateConfiguration(newConfig);
setTimeout(() => {
@@ -38,26 +45,27 @@ class ConfigureStreamContainer extends Component {
}
}
handleChange (e) {
const {name, checked} = e.target;
handleChange () {
this.setState({
[name]: checked,
changed: true
});
}
toggleStatus () {
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
this.props.updateStatus(
this.props.asset.closedAt === null ? 'closed' : 'open'
);
}
getClosedIn () {
const {closedTimeout} = this.props.config;
const {closedTimeout} = this.props.asset.settings;
const {created_at} = this.props.asset;
return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
}
render () {
const {status} = this.props;
const status = this.props.asset.closedAt === null ? 'open' : 'closed';
const premod = this.props.asset.settings.moderation === 'PRE';
return (
<div>
@@ -65,7 +73,8 @@ class ConfigureStreamContainer extends Component {
handleChange={this.handleChange}
handleApply={this.handleApply}
changed={this.state.changed}
{...this.state}
premodLinks={false}
premod={premod}
/>
<hr />
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
@@ -80,11 +89,7 @@ class ConfigureStreamContainer extends Component {
}
const mapStateToProps = (state) => ({
config: state.config.toJS(),
asset: state.items
.get('assets')
.first()
.toJS()
asset: state.asset.toJS()
});
const mapDispatchToProps = dispatch => ({
@@ -92,7 +97,6 @@ const mapDispatchToProps = dispatch => ({
updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig))
});
export default connect(
mapStateToProps,
mapDispatchToProps
export default compose(
connect(mapStateToProps, mapDispatchToProps)
)(ConfigureStreamContainer);
@@ -0,0 +1 @@
+172
View File
@@ -0,0 +1,172 @@
// this component will
// render its children
// render a like button
// render a permalink button
// render a reply button
// render a flag button
// translate things?
import React, {PropTypes} from 'react';
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import Content from 'coral-plugin-commentcontent/CommentContent';
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';
const getAction = (type, comment) => comment.actions.filter((a) => a.type === type)[0];
class Comment extends React.Component {
constructor(props) {
super(props);
this.state = {replyBoxVisible: false};
}
static propTypes = {
reactKey: PropTypes.string.isRequired,
// id of currently opened ReplyBox. tracked in Stream.js
activeReplyBox: PropTypes.string.isRequired,
setActiveReplyBox: PropTypes.func.isRequired,
refetch: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
postAction: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
parentId: PropTypes.string,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
url: PropTypes.string
}).isRequired,
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
comment: PropTypes.shape({
depth: PropTypes.number,
actions: PropTypes.array.isRequired,
body: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
replies: PropTypes.arrayOf(
PropTypes.shape({
body: PropTypes.string.isRequired,
id: PropTypes.string.isRequired
})
),
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
}).isRequired
}
render () {
const {
comment,
parentId,
currentUser,
asset,
depth,
postItem,
refetch,
addNotification,
showSignInDialog,
postAction,
setActiveReplyBox,
activeReplyBox,
deleteAction
} = this.props;
const like = getAction('LIKE', comment);
const flag = getAction('FLAG', comment);
return (
<div
className={parentId ? 'reply' : 'comment'}
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}>
<hr aria-hidden={true} />
<AuthorName
author={comment.user}
addNotification={this.props.addNotification}
id={comment.id}
author_id={comment.user.id}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
currentUser={currentUser}/>
<PubDate created_at={comment.created_at} />
<Content body={comment.body} />
<div className="commentActionsLeft">
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false} />
<LikeButton
like={like}
id={comment.id}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</div>
<div className="commentActionsRight">
<FlagComment
flag={flag}
id={comment.id}
author_id={comment.user.id}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
</div>
{
activeReplyBox === comment.id
? <ReplyBox
commentPostedHandler={() => {
setActiveReplyBox('');
refetch();
}}
setActiveReplyBox={setActiveReplyBox}
parentId={parentId || comment.id}
addNotification={addNotification}
authorId={currentUser.id}
postItem={postItem}
assetId={asset.id} />
: null
}
{
comment.replies &&
comment.replies.map(reply => {
return <Comment
refetch={refetch}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
depth={depth + 1}
asset={asset}
currentUser={currentUser}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply} />;
})
}
</div>
);
}
}
export default Comment;
@@ -1,331 +0,0 @@
import React, {Component, PropTypes} from 'react';
import Pym from 'pym.js';
import {connect} from 'react-redux';
import {
itemActions,
Notification,
notificationActions,
authActions
} from '../../coral-framework';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
import InfoBox from '../../coral-plugin-infobox/InfoBox';
import Content from '../../coral-plugin-commentcontent/CommentContent';
import PubDate from '../../coral-plugin-pubdate/PubDate';
import Count from '../../coral-plugin-comment-count/CommentCount';
import AuthorName from '../../coral-plugin-author-name/AuthorName';
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
import FlagComment from '../../coral-plugin-flags/FlagComment';
import LikeButton from '../../coral-plugin-likes/LikeButton';
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import UserBox from '../../coral-sign-in/components/UserBox';
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {logout, showSignInDialog} = authActions;
class CommentStream extends Component {
constructor (props) {
super(props);
this.state = {
activeTab: 0
};
this.changeTab = this.changeTab.bind(this);
}
changeTab (tab) {
this.setState({
activeTab: tab
});
}
static propTypes = {
items: PropTypes.object.isRequired,
addItem: PropTypes.func.isRequired,
updateItem: PropTypes.func.isRequired
}
componentDidMount () {
// Set up messaging between embedded Iframe an parent component
this.pym = new Pym.Child({polling: 100});
let path = this.pym.parentUrl.split('#')[0];
if (!path) {
path = window.location.href.split('#')[0];
}
this.props.getStream(path || window.location);
this.path = path;
this.pym.sendMessage('childReady');
this.pym.onMessage('DOMContentLoaded', hash => {
const commentId = hash.replace('#', 'c_');
let count = 0;
const interval = setInterval(() => {
if (document.getElementById(commentId)) {
window.clearInterval(interval);
this.pym.scrollParentToChildEl(commentId);
}
if (++count > 100) { // ~10 seconds
// give up waiting for the comments to load.
// it would be weird for the page to jump after that long.
window.clearInterval(interval);
}
}, 100);
});
}
render () {
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {actions, users, comments} = this.props.items;
const {status, moderation, closedMessage, charCount, charCountEnable} = this.props.config;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
const {activeTab} = this.state;
const banned = (this.props.userData.status === 'banned');
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 150
} : {};
return <div style={expandForLogin}>
{
rootItem
? <div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count id={rootItemId} items={this.props.items}/></Tab>
<Tab>Settings</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
<TabContent show={activeTab === 0}>
{
status === 'open'
? <div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
<RestrictedContent restricted={banned} restrictedComp={<SuspendedAccount />}>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={moderation}
reply={false}
currentUser={this.props.auth.user}
banned={banned}
author={user}
charCount={charCountEnable && charCount}/>
</RestrictedContent>
</div>
: <p>{closedMessage}</p>
}
{!loggedIn && <SignInContainer offset={signInOffset}/>}
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName
author={users[comment.author_id]}
addNotification={this.props.addNotification}
id={commentId}
author_id={comment.author_id}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
currentUser={this.props.auth.user}
showReply={comment.showReply}
banned={banned}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
showSignInDialog={this.props.showSignInDialog}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}
banned={banned}/>
</div>
<div className="commentActionsRight">
<FlagComment
addNotification={this.props.addNotification}
id={commentId}
author_id={comment.author_id}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
banned={banned}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={commentId}
articleURL={this.path}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
premod={moderation}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={comment.showReply}/>
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId} id={`c_${replyId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[reply.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}
banned={banned}
currentUser={this.props.auth.user}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}
banned={banned}/>
</div>
<div className="replyActionsRight">
<FlagComment
addNotification={this.props.addNotification}
id={replyId}
author_id={comment.author_id}
flag={actions[reply.flag]}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
banned={banned}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
articleURL={this.path}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={moderation}
banned={banned}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={reply.showReply}/>
</div>;
})
}
</div>;
})
}
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.handleSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
:
<Spinner/>
}
</div>;
}
}
const mapStateToProps = state => ({
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS()
});
const mapDispatchToProps = (dispatch) => ({
addItem: (item, item_id) => dispatch(addItem(item, item_id)),
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
postAction: (item, itemType, action) => dispatch(postAction(item, itemType, action)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)),
appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)),
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
logout: () => dispatch(logout())
});
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
+208
View File
@@ -0,0 +1,208 @@
import React, {Component} from 'react';
import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import isEqual from 'lodash/isEqual';
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
const {logout, showSignInDialog} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postAction, deleteAction} from 'coral-framework/graphql/mutations';
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import Count from 'coral-plugin-comment-count/CommentCount';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import UserBox from '../../coral-sign-in/components/UserBox';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
class Embed extends Component {
constructor (props) {
super(props);
this.state = {
activeTab: 0,
showSignInDialog: false
};
this.changeTab = this.changeTab.bind(this);
}
changeTab (tab) {
this.setState({
activeTab: tab
});
}
static propTypes = {
data: React.PropTypes.shape({
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired
}
componentDidMount () {
// stream id, logged in user, settings
// Set up messaging between embedded Iframe an parent component
// this.props.getStream(path || window.location);
// this.path = window.location.href.split('#')[0];
//
pym.sendMessage('childReady');
pym.onMessage('DOMContentLoaded', hash => {
const commentId = hash.replace('#', 'c_');
let count = 0;
const interval = setInterval(() => {
if (document.getElementById(commentId)) {
window.clearInterval(interval);
pym.scrollParentToChildEl(commentId);
}
if (++count > 100) { // ~10 seconds
// give up waiting for the comments to load.
// it would be weird for the page to jump after that long.
window.clearInterval(interval);
}
}, 100);
});
}
componentWillReceiveProps (nextProps) {
const {loadAsset} = this.props;
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
loadAsset(nextProps.data.asset);
}
}
render () {
const {activeTab} = this.state;
const {closedAt} = this.props.asset;
const {loading, asset, refetch} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
const openStream = closedAt === null;
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 200
} : {};
return <div style={expandForLogin}>
{
loading ? <Spinner/>
: <div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.commentCount}/></Tab>
<Tab>Settings</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
<TabContent show={activeTab === 0}>
{
openStream
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
{
user
? <CommentBox
commentPostedHandler={refetch}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
currentUser={this.props.auth.user}
banned={false}
authorId={user.id}
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
: null
}
</RestrictedContent>
</div>
: <p>{asset.settings.closedMessage}</p>
}
{!loggedIn && <SignInContainer offset={signInOffset}/>}
<Stream
refetch={refetch}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
asset={asset}
currentUser={user}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={{text: null}}
/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.showSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
}
</div>;
}
}
const mapStateToProps = state => ({
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS(),
asset: state.asset.toJS()
});
const mapDispatchToProps = dispatch => ({
loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
postComment,
postAction,
deleteAction,
queryStream
)(Embed);
+72
View File
@@ -0,0 +1,72 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
class Stream extends React.Component {
static propTypes = {
refetch: PropTypes.func.isRequired,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
asset: PropTypes.object.isRequired,
comments: PropTypes.array.isRequired,
currentUser: PropTypes.shape({
displayName: PropTypes.string,
id: PropTypes.string
})
}
constructor(props) {
super(props);
this.state = {activeReplyBox: ''};
this.setActiveReplyBox = this.setActiveReplyBox.bind(this);
}
setActiveReplyBox (reactKey) {
if (!this.props.currentUser) {
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
} else {
this.setState({activeReplyBox: reactKey});
}
}
render () {
const {
comments,
currentUser,
asset,
postItem,
addNotification,
postAction,
deleteAction,
showSignInDialog,
refetch
} = this.props;
return (
<div>
{
comments.map(comment => {
return <Comment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment} />;
})
}
</div>
);
}
}
export default Stream;
+11 -7
View File
@@ -1,11 +1,15 @@
import React from 'react';
import {render} from 'react-dom';
import CommentStream from './CommentStream';
import {Provider} from 'react-redux';
import {store} from '../../coral-framework';
import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/client';
import store from 'coral-framework/store';
import Embed from './Embed';
render(
<Provider store={store}>
<CommentStream />
</Provider>
, document.querySelector('#coralStream'));
<ApolloProvider client={client} store={store}>
<Embed />
</ApolloProvider>
, document.querySelector('#coralStream')
);
+9
View File
@@ -0,0 +1,9 @@
import Pym from '../../node_modules/pym.js';
const pym = new Pym.Child({polling: 100});
export default pym;
export const link = (url) => (e) => {
e.preventDefault();
pym.sendMessage('navigate', url);
};
@@ -1,42 +1,39 @@
import * as actions from '../constants/asset';
import coralApi from '../helpers/response';
import * as actions from '../constants/config';
import {addNotification} from '../actions/notification';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
const updateConfigRequest = () => ({type: actions.UPDATE_CONFIG_REQUEST});
const updateConfigSuccess = config => ({type: actions.UPDATE_CONFIG_SUCCESS, config});
const updateConfigFailure = () => ({type: actions.UPDATE_CONFIG_FAILURE});
export const fetchAssetRequest = () => ({type: actions.FETCH_ASSET_REQUEST});
export const fetchAssetSuccess = asset => ({type: actions.FETCH_ASSET_SUCCESS, asset});
export const fetchAssetFailure = error => ({type: actions.FETCH_ASSET_FAILURE, error});
const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_REQUEST});
const updateAssetSettingsSuccess = settings => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
const updateAssetSettingsFailure = () => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE});
export const updateConfiguration = newConfig => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
dispatch(updateConfigRequest());
const assetId = getState().asset.toJS().id;
dispatch(updateAssetSettingsRequest());
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
.then(() => {
dispatch(addNotification('success', lang.t('successUpdateSettings')));
dispatch(updateConfigSuccess(newConfig));
dispatch(updateAssetSettingsSuccess(newConfig));
})
.catch(error => dispatch(updateConfigFailure(error)));
.catch(error => dispatch(updateAssetSettingsFailure(error)));
};
export const updateOpenStream = closedBody => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
dispatch(updateConfigRequest());
const assetId = getState().asset.toJS().id;
dispatch(fetchAssetRequest());
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
.then(() => {
dispatch(addNotification('success', lang.t('successUpdateSettings')));
dispatch(updateConfigSuccess(closedBody));
dispatch(fetchAssetSuccess(closedBody));
})
.catch(error => dispatch(updateConfigFailure(error)));
.catch(error => dispatch(fetchAssetFailure(error)));
};
const openStream = () => ({type: actions.OPEN_COMMENTS});
+6 -6
View File
@@ -3,7 +3,6 @@ import translations from './../translations';
const lang = new I18n(translations);
import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {addItem} from './items';
// Dialog Actions
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
@@ -27,10 +26,9 @@ export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
coralApi('/auth/local', {method: 'POST', body: formData})
.then(({user}) => {
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
dispatch(addItem(user, 'users'));
})
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
};
@@ -59,7 +57,6 @@ export const facebookCallback = (err, data) => dispatch => {
const user = JSON.parse(data);
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(addItem(user, 'users'));
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -132,8 +129,11 @@ export const checkLogin = () => dispatch => {
throw new Error('Not logged in');
}
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => dispatch(checkLoginFailure(error)));
.catch(error => {
console.error(error);
dispatch(checkLoginFailure(`${error.message}`));
});
};
-251
View File
@@ -1,251 +0,0 @@
import coralApi from '../helpers/response';
import {fromJS} from 'immutable';
import {UPDATE_CONFIG} from '../constants/config';
/**
* Action name constants
*/
export const ADD_ITEM = 'ADD_ITEM';
export const UPDATE_ITEM = 'UPDATE_ITEM';
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
/**
* Action creators
*/
/*
* Adds an item to the local store without posting it to the server
* Useful for optimistic posting, etc.
*
* @params
* item - the item to be posted
*
*/
export const addItem = (item, item_type) => {
if (!item.id) {
console.warn('addItem called without an item id.');
}
return {
type: ADD_ITEM,
item,
item_type,
id: item.id
};
};
/*
* Updates an item in the local store without posting it to the server
* Useful for item-level toggles, etc.
*
* @params
* id - the id of the item to be posted
* property - the property to be updated
* value - the value that the property should be set to
* item_type - the type of the item being updated (users, comments, etc)
*
*/
export const updateItem = (id, property, value, item_type) => {
return {
type: UPDATE_ITEM,
id,
property,
value,
item_type
};
};
/*
* Appends data to an array in an item in the local store without posting it to the server
* Useful for adding a recently posted reply to a comment, etc.
*
* @params
* id - the id of the item to be posted
* property - the property to be updated (should be an array)
* value - the value that should be added to the array
* add_to_front - boolean that defines whether value is added at the beginning (unshift) or end (push)
* item_type - the type of the item being updated (users, comments, etc)
*
*/
export const appendItemArray = (id, property, value, add_to_front, item_type) => {
return {
type: APPEND_ITEM_ARRAY,
id,
property,
value,
add_to_front,
item_type
};
};
/*
* Get Items from Query
* Gets a set of items from a predefined query
*
* @params
* Query - a predefiend query for retreiving items
*
* @returns
* A promise resolving to a set of items
*
* @dispatches
* A set of items to the item store
*/
export function getStream (assetUrl) {
return (dispatch) => {
return coralApi(`/stream?asset_url=${encodeURIComponent(assetUrl)}`)
.then((json) => {
/* Add items to the store */
Object.keys(json).forEach(type => {
if (type === 'actions') {
json[type].forEach(action => {
action.id = `${action.action_type}_${action.item_id}`;
dispatch(addItem(action, 'actions'));
});
} else if (type === 'settings') {
dispatch({type: UPDATE_CONFIG, config: fromJS(json[type])});
} else {
json[type].forEach(item => {
dispatch(addItem(item, type));
});
}
});
const assetId = json.assets[0].id;
/* Sort comments by date*/
json.comments.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
const rels = json.comments.reduce((h, item) => {
/* Check for root and child comments. */
if (
item.asset_id === assetId &&
!item.parent_id) {
h.rootComments.push(item.id);
} else if (
item.asset_id === assetId
) {
let children = h.childComments[item.parent_id] || [];
h.childComments[item.parent_id] = children.concat(item.id);
}
return h;
}, {rootComments: [], childComments: {}});
dispatch(updateItem(assetId, 'comments', rels.rootComments, 'assets'));
Object.keys(rels.childComments).forEach(key => {
dispatch(updateItem(key, 'children', rels.childComments[key].reverse(), 'comments'));
});
/* Hydrate actions on comments */
json.actions.forEach(action => {
dispatch(updateItem(action.item_id, action.action_type, action.id, 'comments'));
});
return (json);
});
};
}
/*
* Get Items Array
* Gets a set of items from an array of item ids
*
* @params
* Query - a predefiend query for retreiving items
*
* @returns
* A promise resolving to a set of items
*
* @dispatches
* A set of items to the item store
*/
export function getItemsArray (ids) {
return (dispatch) => {
return coralApi(`/item/${ids}`)
.then((json) => {
for (let i = 0; i < json.items.length; i++) {
dispatch(addItem(json.items[i]));
}
return json.items;
});
};
}
/*
* PutItem
* Puts an item
*
* @params
* Item - the item to be put
*
* @returns
* A promise resolving to an item is
*
* @dispatches
* The newly put item to the item store
*/
export function postItem (item, type, id) {
return (dispatch) => {
if (id) {
item.id = id;
}
return coralApi(`/${type}`, {method: 'POST', body: item})
.then((json) => {
dispatch(addItem({...item, id:json.id}, type));
return json;
});
};
}
/*
* PostAction
* Posts an action to an item
*
* @params
* id - the id of the item on which the action is taking place
* action - the action object.
* Must include an 'action_type' string.
* May optionally include a `metadata` object with arbitrary action data.
* user - the user performing the action
* host - the coral host
*
* @returns
* A promise resolving to null or an error
*
*/
export function postAction (item_id, item_type, action) {
return (dispatch) => {
return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action})
.then((json) => {
dispatch(updateItem(action.item_id, action.action_type, action.id, item_type));
return json;
});
};
}
/*
* DeleteAction
* Deletes an action to an item
*
* @params
* id - the id of the item on which the action is taking place
* action - the name of the action
* user - the user performing the action
* host - the coral host
*
* @returns
* A promise resolving to null or an error
*
*/
export function deleteAction (action_id) {
return () => {
return coralApi(`/actions/${action_id}`, {method: 'DELETE'});
};
}
-35
View File
@@ -1,7 +1,5 @@
import * as actions from '../constants/user';
import * as assetActions from '../constants/assets';
import {addNotification} from '../actions/notification';
import {addItem} from '../actions/items';
import coralApi from '../helpers/response';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -21,36 +19,3 @@ export const saveBio = (user_id, formData) => dispatch => {
})
.catch(error => dispatch(saveBioFailure(error)));
};
/**
*
* Get a list of comments by a single user
*
* @param {string} user_id
* @returns Promise
*/
export const fetchCommentsByUserId = userId => {
return (dispatch, getState) => {
dispatch({type: actions.COMMENTS_BY_USER_REQUEST});
return coralApi(`/comments?user_id=${userId}`)
.then(({comments, assets}) => {
const state = getState();
comments.forEach(comment => dispatch(addItem(comment, 'comments')));
assets.forEach(asset => {
const prevAsset = state.items.getIn(['assets', asset.id]);
if (prevAsset) {
// Include data such as hydrated comments from assets already in the system.
dispatch(addItem({...prevAsset.toJS(), ...asset}, 'assets'));
} else {
dispatch(addItem(asset, 'assets'));
}
});
dispatch({type: actions.COMMENTS_BY_USER_SUCCESS, comments: comments.map(comment => comment.id)});
dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)});
})
.catch(error => dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error}));
};
};
+14
View File
@@ -0,0 +1,14 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
export const client = new ApolloClient({
connectToDevTools: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
}
return null;
},
networkInterface: getNetworkInterface()
});
+10
View File
@@ -0,0 +1,10 @@
export const FETCH_ASSET_REQUEST = 'FETCH_ASSET_REQUEST';
export const FETCH_ASSET_FAILURE = 'FETCH_ASSET_FAILURE';
export const FETCH_ASSET_SUCCESS = 'FETCH_ASSET_SUCCESS';
export const UPDATE_ASSET_SETTINGS_REQUEST = 'UPDATE_ASSET_SETTINGS_REQUEST';
export const UPDATE_ASSET_SETTINGS_SUCCESS = 'UPDATE_ASSET_SETTINGS_SUCCESS';
export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
@@ -0,0 +1,21 @@
fragment commentView on Comment {
id
body
created_at
status
user {
id
name: displayName
settings {
bio
}
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
@@ -0,0 +1,3 @@
mutation deleteAction ($id: ID!) {
deleteAction(id:$id)
}
@@ -0,0 +1,44 @@
import {graphql} from 'react-apollo';
import POST_COMMENT from './postComment.graphql';
import POST_ACTION from './postAction.graphql';
import DELETE_ACTION from './deleteAction.graphql';
import commentView from '../fragments/commentView.graphql';
export const postComment = graphql(POST_COMMENT, {
options: () => ({
fragments: commentView
}),
props: ({mutate}) => ({
postItem: ({asset_id, body, parent_id} /* , type */ ) => {
return mutate({
variables: {
asset_id,
body,
parent_id
}
});
}}),
});
export const postAction = graphql(POST_ACTION, {
props: ({mutate}) => ({
postAction: (action) => {
return mutate({
variables: {
action
}
});
}}),
});
export const deleteAction = graphql(DELETE_ACTION, {
props: ({mutate}) => ({
deleteAction: (id) => {
return mutate({
variables: {
id
}
});
}}),
});
@@ -0,0 +1,5 @@
mutation CreateAction ($action: CreateActionInput!) {
createAction(action:$action) {
id
}
}
@@ -0,0 +1,7 @@
#import "../fragments/commentView.graphql"
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
...commentView
}
}
@@ -0,0 +1,24 @@
import {graphql} from 'react-apollo';
import STREAM_QUERY from './streamQuery.graphql';
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
let vars = query.split('&');
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) === variable) {
return decodeURIComponent(pair[1]);
}
}
}
export const queryStream = graphql(STREAM_QUERY, {
options: () => ({
variables: {
asset_url: getQueryVariable('asset_url')
}
})
});
export const myCommentHistory = graphql(MY_COMMENT_HISTORY, {});
@@ -0,0 +1,9 @@
query myCommentHistory {
me {
comments {
id
body
created_at
}
}
}
@@ -0,0 +1,28 @@
#import "../fragments/commentView.graphql"
query AssetQuery($asset_url: String!) {
asset(url: $asset_url) {
id
title
url
closedAt
created_at
settings {
moderation
infoBoxEnable
infoBoxContent
closeTimeout
closedMessage
charCountEnable
charCount
requireEmailConfirmation
}
commentCount
comments {
...commentView
replies {
...commentView
}
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
email: email => (/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
email: email => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
password: pass => (/^(?=.{8,}).*$/.test(pass)),
confirmPassword: () => true,
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName))
+9 -9
View File
@@ -1,17 +1,17 @@
import Notification from './modules/notification/Notification';
import store from './store';
import * as itemActions from './actions/items';
import pym from './PymConnection';
import I18n from './modules/i18n/i18n';
import * as notificationActions from './actions/notification';
import * as authActions from './actions/auth';
import * as configActions from './actions/config';
import * as assetActions from './actions/asset';
import * as notificationActions from './actions/notification';
import Notification from './modules/notification/Notification';
export {
Notification,
store,
itemActions,
pym,
I18n,
notificationActions,
store,
authActions,
configActions
assetActions,
Notification,
notificationActions
};
+25
View File
@@ -0,0 +1,25 @@
import {Map} from 'immutable';
import * as actions from '../constants/asset';
const initialState = Map({
closedAt: null,
settings: null,
title: null,
url: null,
features: Map({}),
status: 'open',
moderation: null
});
export default function asset (state = initialState, action) {
switch (action.type) {
case actions.FETCH_ASSET_SUCCESS:
return state
.merge(action.asset);
case actions.UPDATE_ASSET_SETTINGS_SUCCESS:
return state
.setIn(['settings'], action.settings);
default:
return state;
}
}
-29
View File
@@ -1,29 +0,0 @@
import {Map} from 'immutable';
import * as actions from '../constants/config';
const initialState = Map({
features: Map({}),
status: 'open',
moderation: null
});
export default (state = initialState, action) => {
switch(action.type) {
case actions.UPDATE_CONFIG:
return state
.merge(Map(action.config));
case actions.UPDATE_CONFIG_SUCCESS:
return state
.merge(Map(action.config));
case actions.OPEN_COMMENTS:
return state
.set('status', 'open');
case actions.CLOSE_COMMENTS:
return state
.set('status', 'closed');
case actions.ADD_ITEM:
return action.item_type === 'assets' ? state.set('status', (action.item && action.item.closedAt && new Date(action.item.closedAt).getTime() <= new Date().getTime()) ? 'closed' : 'open') : state;
default:
return state;
}
};
+7 -16
View File
@@ -1,20 +1,11 @@
/* @flow */
import {combineReducers} from 'redux';
import config from './config';
import items from './items';
import notification from './notification';
import auth from './auth';
import user from './user';
import asset from './asset';
import notification from './notification';
/**
* Expose the combined main reducer
*/
export default combineReducers({
config,
items,
notification,
export default {
auth,
user
});
user,
asset,
notification
};
-30
View File
@@ -1,30 +0,0 @@
/* Items Reducer */
import {fromJS} from 'immutable';
import * as actions from '../actions/items';
const initialState = fromJS({
comments: {},
users: {},
assets: {},
actions: {}
});
export default (state = initialState, action) => {
switch (action.type) {
case actions.ADD_ITEM:
return state.setIn([action.item_type, action.id], fromJS(action.item));
case actions.UPDATE_ITEM:
return state.setIn([action.item_type, action.id, action.property], fromJS(action.value));
case actions.APPEND_ITEM_ARRAY:
return state.updateIn([action.item_type, action.id, action.property], (prop) => {
if (action.add_to_front) {
return prop ? prop.unshift(fromJS(action.value)) : fromJS([action.value]);
} else {
return prop ? prop.push(fromJS(action.value)) : fromJS([action.value]);
}
});
default:
return state;
}
};
+19 -4
View File
@@ -1,9 +1,24 @@
import {createStore, applyMiddleware} from 'redux';
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from './reducers';
import {client} from './client';
const middlewares = [
applyMiddleware(client.middleware()),
applyMiddleware(thunk)
];
if (window.devToolsExtension) {
// we can't have the last argument of compose() be undefined
middlewares.push(window.devToolsExtension());
}
export default createStore(
mainReducer,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk)
combineReducers({
...mainReducer,
apollo: client.reducer()
}),
{},
compose(...middlewares)
);
+14
View File
@@ -0,0 +1,14 @@
import {print} from 'graphql-tag/printer';
// quick way to add the subscribe and unsubscribe functions to the network interface
const addGraphQLSubscriptions = (networkInterface, wsClient) => Object.assign(networkInterface, {
subscribe: (request, handler) => wsClient.subscribe({
query: print(request.query),
variables: request.variables,
}, handler),
unsubscribe: (id) => {
wsClient.unsubscribe(id);
},
});
export default addGraphQLSubscriptions;
+11
View File
@@ -0,0 +1,11 @@
import {createNetworkInterface} from 'apollo-client';
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
return new createNetworkInterface({
uri: apiUrl,
opts: {
credentials: 'same-origin',
headers,
},
});
}
@@ -36,8 +36,8 @@ export default class AuthorName extends Component {
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
>
{author && author.displayName}
{ showTooltip && <Tooltip>
{author && author.name}
{ showTooltip && author.settings.bio && <Tooltip>
<div className={`${packagename}-bio`}>
{author.settings.bio}
</div>
@@ -1,29 +1,18 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import has from 'lodash/has';
import reduce from 'lodash/reduce';
const name = 'coral-plugin-comment-count';
const CommentCount = ({items, id}) => {
let count = 0;
if (has(items, `assets.${id}.comments`)) {
count += items.assets[id].comments.length;
}
// lodash reduce works on {}
count += reduce(items.comments, (accum, comment) => {
if (comment.children) {
accum += comment.children.length;
}
return accum;
}, 0);
const CommentCount = ({count}) => {
return <div className={`${name}-text`}>
{`${count} ${count === 1 ? lang.t('comment') : lang.t('comment-plural')}`}
</div>;
};
CommentCount.propTypes = {
count: PropTypes.number.isRequired
};
export default CommentCount;
const lang = new I18n(translations);
+70 -37
View File
@@ -8,11 +8,16 @@ const name = 'coral-plugin-commentbox';
class CommentBox extends Component {
static propTypes = {
postItem: PropTypes.func,
updateItem: PropTypes.func,
id: PropTypes.string,
comments: PropTypes.array,
reply: PropTypes.bool,
// updateItem: PropTypes.func,
// comments: PropTypes.array,
commentPostedHandler: PropTypes.func,
postItem: PropTypes.func.isRequired,
cancelButtonClicked: PropTypes.func,
assetId: PropTypes.string.isRequired,
parentId: PropTypes.string,
authorId: PropTypes.string.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
currentUser: PropTypes.object
}
@@ -24,73 +29,88 @@ class CommentBox extends Component {
postComment = () => {
const {
// child_id,
// updateItem,
// appendItemArray,
commentPostedHandler,
postItem,
updateItem,
id,
parent_id,
child_id,
assetId,
parentId,
addNotification,
appendItemArray,
premod,
author
authorId
} = this.props;
let comment = {
body: this.state.body,
asset_id: id,
author_id: author.id
asset_id: assetId,
author_id: authorId,
parent_id: parentId
};
let related;
let parent_type;
if (parent_id) {
comment.parent_id = parent_id;
related = 'children';
parent_type = 'comments';
} else {
related = 'comments';
parent_type = 'assets';
}
if (child_id || parent_id) {
updateItem(child_id || parent_id, 'showReply', false, 'comments');
}
// let related;
// let parent_type;
// if (parent_id) {
// comment.parent_id = parent_id;
// related = 'children';
// parent_type = 'comments';
// } else {
// related = 'comments';
// parent_type = 'assets';
// }
// if (child_id || parent_id) {
// updateItem(child_id || parent_id, 'showReply', false, 'comments');
// }
if (this.props.charCount && this.state.body.length > this.props.charCount) {
return;
}
postItem(comment, 'comments')
.then((postedComment) => {
const commentId = postedComment.id;
if (postedComment.status === 'rejected') {
.then(({data}) => {
const postedComment = data.createComment;
// const commentId = postedComment.id;
if (postedComment.status === 'REJECTED') {
addNotification('error', lang.t('comment-post-banned-word'));
} else if (premod === 'pre') {
} else if (postedComment.status === 'PREMOD') {
addNotification('success', lang.t('comment-post-notif-premod'));
} else {
appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type);
addNotification('success', 'Your comment has been posted.');
}
if (commentPostedHandler) {
commentPostedHandler();
}
})
.catch((err) => console.error(err));
this.setState({body: ''});
}
render () {
const {styles, reply, author, charCount} = this.props;
const {styles, isReply, authorId, charCount} = this.props;
let {cancelButtonClicked} = this.props;
const length = this.state.body.length;
if (isReply && typeof cancelButtonClicked !== 'function') {
console.warn('the CommentBox component should have a cancelButtonClicked callback defined if it lives in a Reply');
cancelButtonClicked = () => {};
}
return <div>
<div
className={`${name}-container`}>
<label
htmlFor={ reply ? 'replyText' : 'commentText'}
htmlFor={ isReply ? 'replyText' : 'commentText'}
className="screen-reader-text"
aria-hidden={true}>
{reply ? lang.t('reply') : lang.t('comment')}
{isReply ? lang.t('reply') : lang.t('comment')}
</label>
<textarea
className={`${name}-textarea`}
style={styles && styles.textarea}
value={this.state.body}
placeholder={lang.t('comment')}
id={reply ? 'replyText' : 'commentText'}
id={isReply ? 'replyText' : 'commentText'}
onChange={(e) => this.setState({body: e.target.value})}
rows={3}/>
</div>
@@ -101,7 +121,20 @@ class CommentBox extends Component {
}
</div>
<div className={`${name}-button-container`}>
{ author && (
{
isReply && (
<Button
cStyle='darkGrey'
className={`${name}-cancel-button`}
onClick={() => {
console.log('cancel button in comment box');
cancelButtonClicked('');
}}>
{lang.t('cancel')}
</Button>
)
}
{ authorId && (
<Button
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
className={`${name}-button`}
@@ -1,6 +1,7 @@
{
"en": {
"post": "Post",
"cancel": "Cancel",
"reply": "Reply",
"comment": "Comment",
"name": "Name",
@@ -11,6 +12,7 @@
},
"es": {
"post": "Publicar",
"cancel": "Cancelar",
"reply": "Respuesta",
"comment": "Comentario",
"name": "Nombre",
@@ -1,5 +1,5 @@
import React from 'react';
const name = 'coral-plugin-replies';
const name = 'coral-plugin-content';
const Content = ({body, styles}) => {
const textbreaks = body.split('\n');
+1 -1
View File
@@ -9,7 +9,7 @@ const getPopupMenu = [
() => {
return {
header: lang.t('step-2-header'),
itemType: 'user',
itemType: 'USERS',
field: 'bio',
options: [
{val: 'This bio is offensive', text: lang.t('bio-offensive')},
+34 -23
View File
@@ -14,22 +14,32 @@ class FlagButton extends Component {
reason: '',
note: '',
step: 0,
posted: false
posted: false,
localPost: null,
localDelete: false
}
// When the "report" button is clicked expand the menu
onReportClick = () => {
if (!this.props.currentUser) {
const {currentUser, flag, deleteAction} = this.props;
const {localPost, localDelete} = this.state;
const flagged = (flag && flag.current && !localDelete) || localPost;
if (!currentUser) {
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
return;
}
this.setState({showMenu: !this.state.showMenu});
if (flagged) {
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
deleteAction(localPost || flag.current.id);
} else {
this.setState({showMenu: !this.state.showMenu});
}
}
onPopupContinue = () => {
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
const {itemType, field, reason, step, note, posted} = this.state;
const {postAction, id, author_id} = this.props;
const {itemType, reason, step, posted} = this.state;
// Proceed to the next step or close the menu if we've reached the end
if (step + 1 >= this.props.getPopupMenu.length) {
@@ -40,32 +50,32 @@ class FlagButton extends Component {
// If itemType and reason are both set, post the action
if (itemType && reason && !posted) {
this.setState({posted: true});
// Set the text from the "other" field if it exists.
let item_id;
switch(itemType) {
case 'comments':
case 'COMMENTS':
item_id = id;
break;
case 'users':
case 'USERS':
item_id = author_id;
break;
}
const action = {
action_type: 'flag',
metadata: {
field,
reason,
note
// Note: Action metadata has been temporarily removed.
if (itemType === 'COMMENTS') {
this.setState({localPost: 'temp'});
}
postAction({
item_id,
item_type: itemType,
action_type: 'FLAG'
}).then(({data}) => {
if (itemType === 'COMMENTS') {
this.setState({localPost: data.createAction.id});
}
};
postAction(item_id, itemType, action)
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
updateItem(action.item_id, action.action_type, id, action.item_type);
this.setState({posted: true});
});
});
}
}
@@ -98,7 +108,8 @@ class FlagButton extends Component {
render () {
const {flag, getPopupMenu} = this.props;
const flagged = flag && flag.current_user;
const {localPost, localDelete} = this.state;
const flagged = (flag && flag.current && !localDelete) || localPost;
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
@@ -106,7 +117,7 @@ class FlagButton extends Component {
{
flagged
? <span className={`${name}-button-text`}>{lang.t('reported')}</span>
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged ? styles.flaggedIcon : {}}
+3 -3
View File
@@ -10,15 +10,15 @@ const getPopupMenu = [
return {
header: lang.t('step-1-header'),
options: [
{val: 'users', text: lang.t('flag-username')},
{val: 'comments', text: lang.t('flag-comment')}
{val: 'USERS', text: lang.t('flag-username')},
{val: 'COMMENTS', text: lang.t('flag-comment')}
],
button: lang.t('continue'),
sets: 'itemType'
};
},
(itemType) => {
const options = itemType === 'comments' ?
const options = itemType === 'COMMENTS' ?
[
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
+1 -2
View File
@@ -1,12 +1,11 @@
import React, {PropTypes} from 'react';
import styles from './Comment.css';
const Comment = props => {
return (
<div className={styles.myComment}>
<p className="myCommentAsset">
<a className={`${styles.assetURL} myCommentAnchor`} href={`${props.asset.url}#${props.comment.id}`}>{props.asset.url}</a>
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.url}</a>
</p>
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
</div>
@@ -7,11 +7,11 @@ const CommentHistory = props => {
<div className={`${styles.header} commentHistory`}>
<div className="commentHistory__list">
{props.comments.map((comment, i) => {
const asset = props.assets.find(asset => asset.id === comment.asset_id);
return <Comment
key={i}
comment={comment}
asset={asset} />;
link={props.link}
asset={props.asset} />;
})}
</div>
</div>
@@ -19,8 +19,8 @@ const CommentHistory = props => {
};
CommentHistory.propTypes = {
comments: PropTypes.arrayOf(PropTypes.object).isRequired,
assets: PropTypes.arrayOf(PropTypes.object).isRequired
comments: PropTypes.array.isRequired,
asset: PropTypes.object.isRequired
};
export default CommentHistory;
+66 -42
View File
@@ -1,52 +1,76 @@
import React from 'react';
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-likes';
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
showSignInDialog(offset);
return;
}
if (banned) {
return;
}
if (!liked) {
const action = {
action_type: 'like'
};
postAction(id, 'comments', action)
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
updateItem(action.item_id, action.action_type, id, 'comments');
});
} else {
deleteAction(liked.id)
.then(() => {
updateItem(like.id, 'count', like.count - 1, 'actions');
updateItem(like.id, 'current_user', false, 'actions');
});
}
};
class LikeButton extends Component {
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
{
liked
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
static propTypes = {
like: PropTypes.shape({
current: PropTypes.obect,
count: PropTypes.number
}),
id: PropTypes.string,
postAction: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
currentUser: PropTypes.shape({
banned: PropTypes.boolean
}),
}
state = {
localPost: null, // Set to the ID of an action if one is posted
localDelete: false // Set to true is the user deletes an action, unless localPost is already set.
}
render() {
const {like, id, postAction, deleteAction, showSignInDialog, currentUser} = this.props;
const {localPost, localDelete} = this.state;
const liked = (like && like.current && !localDelete) || localPost;
let count = like ? like.count : 0;
if (localPost) {count += 1;}
if (localDelete) {count -= 1;}
const onLikeClick = () => {
if (!currentUser) {
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
showSignInDialog(offset);
return;
}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
<span className={`${name}-like-count`}>{like && like.count > 0 && like.count}</span>
</button>
</div>;
};
if (currentUser.banned) {
return;
}
if (!liked) {
this.setState({localPost: 'temp', localDelete: false});
postAction({
item_id: id,
item_type: 'COMMENTS',
action_type: 'LIKE'
}).then(({data}) => {
this.setState({localPost: data.createAction.id});
});
} else {
this.setState((prev) => prev.localPost ? {...prev, localPost: null} : {...prev, localDelete: true});
deleteAction(localPost || like.current.id);
}
};
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
{
liked
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
<span className={`${name}-like-count`}>{count > 0 && count}</span>
</button>
</div>;
}
}
export default LikeButton;
+24 -11
View File
@@ -1,17 +1,30 @@
import React from 'react';
import React, {PropTypes} from 'react';
import CommentBox from '../coral-plugin-commentbox/CommentBox';
const name = 'coral-plugin-replies';
const ReplyBox = (props) => <div
className={`${name}-textarea`}
style={props.styles && props.styles.container}>
{
props.showReply && <CommentBox
{...props}
comments = {props.child}
reply = {true}/>
}
</div>;
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler, setActiveReplyBox}) => (
<div className={`${name}-textarea`} style={styles && styles.container}>
<CommentBox
commentPostedHandler={commentPostedHandler}
parentId={parentId}
cancelButtonClicked={setActiveReplyBox}
addNotification={addNotification}
authorId={authorId}
assetId={assetId}
postItem={postItem}
isReply={true} />
</div>
);
ReplyBox.propTypes = {
setActiveReplyBox: PropTypes.func.isRequired,
commentPostedHandler: PropTypes.func,
parentId: PropTypes.string,
addNotification: PropTypes.func.isRequired,
authorId: PropTypes.string.isRequired,
postItem: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired
};
export default ReplyBox;
+17 -13
View File
@@ -1,21 +1,25 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={`${name}-reply-button`}
onClick={() => {
if (props.banned) {
return;
}
props.updateItem(props.id, 'showReply', !props.showReply, 'comments');
}}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>reply</i>
</button>;
const ReplyButton = ({banned, onClick}) => {
return (
<button
className={`${name}-reply-button`}
onClick={onClick}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>{banned ? 'BANNED' : 'reply'}</i>
</button>
);
};
ReplyButton.propTypes = {
onClick: PropTypes.func.isRequired,
banned: PropTypes.bool.isRequired
};
export default ReplyButton;
@@ -0,0 +1,58 @@
import React, {Component} from 'react';
import {graphql} from 'react-apollo';
import gql from 'graphql-tag';
export class RileysAwesomeCommentBox extends Component {
postComment() {
console.log(this.props);
console.log('postComment', this.props.asset_id);
this.props.mutate({
variables: {
asset_id: this.props.asset_id,
body: this.textarea.value,
parent_id: null
}
}).then(({data}) => {
console.log('it workt');
console.log(data);
});
}
render() {
return <div>
<textarea ref={textarea => this.textarea = textarea}></textarea>
<button onClick={this.postComment.bind(this)}>POST</button>
</div>;
}
}
const postComment = gql`
fragment commentView on Comment {
id
body
user {
name: displayName
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
...commentView
}
}
`;
const RileysAwesomeCommentBoxWithData = graphql(
postComment
)(RileysAwesomeCommentBox);
export default RileysAwesomeCommentBoxWithData;
+98
View File
@@ -0,0 +1,98 @@
import React, {Component} from 'react';
import {graphql} from 'react-apollo';
import gql from 'graphql-tag';
import {fetchSignIn} from 'coral-framework/actions/auth';
import RileysAwesomeCommentBox from 'coral-plugin-stream/RileysAwesomeCommentBox';
const assetID = '6187a94b-0b6d-4a96-ac6b-62b529cd8410';
// MyComponent is a "presentational" or apollo-unaware component,
// It could be a simple React class:
class Stream extends Component {
constructor(props) {
super(props);
}
logMeIn() {
fetchSignIn({email: 'your@example.com', password: 'dfasidfaisdufoiausdfoiuaspdoifas'})(() => {});
}
render() {
console.log(this.props);
const {data} = this.props;
return <div>
<button onClick={this.logMeIn.bind(this)}>Login or whatever</button>
{
data.loading
? 'loading!'
: <div>
<RileysAwesomeCommentBox asset_id={data.asset.id} />
<p>Asset ID: {data.asset.id}</p>
<ul>
{
data.asset.comments.map(comment => {
return <li key={comment.id}>
{comment.body} [{comment.id}]
<ul>
{
comment.replies.map(reply => {
return <li key={reply.id}>{reply.body}</li>;
})
}
</ul>
</li>;
})
}
</ul>
</div>
}
</div>;
}
}
// Initialize GraphQL queries or mutations with the `gql` tag
const StreamQuery = gql`fragment commentView on Comment {
id
body
user {
name: displayName
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
query AssetQuery($asset_id: ID!) {
asset(id: $asset_id) {
id
title
url
commentCount
comments {
...commentView
replies {
...commentView
}
}
}
}`;
// We then can use `graphql` to pass the query results returned by MyQuery
// to MyComponent as a prop (and update them as the results change)
const StreamWithData = graphql(
StreamQuery, {
options: {
variables: {
asset_id: assetID
}
}
}
)(Stream);
export default StreamWithData;
@@ -1,10 +1,15 @@
import React from 'react';
import styles from './NotLoggedIn.css';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
export default ({showSignInDialog}) => (
<div className={styles.message}>
<SignInContainer noButton={true}/>
<div>
<a onClick={showSignInDialog}>Sign In</a> to access Settings
<a onClick={() => {
console.log('Signin click');
showSignInDialog();
}}>Sign In</a> to access Settings
</div>
<div>
From the Settings Page you can
@@ -4,7 +4,11 @@ import styles from './SettingsHeader.css';
export default ({userData}) => (
<div className={styles.header}>
<h1>{userData.displayName}</h1>
<h2>{userData.profiles.map(profile => profile.id)}</h2>
{
// Hiding display of users ID unless there's a use case for it.
// <h2>{userData.profiles.map(profile => profile.id)}</h2>
}
</div>
);
@@ -1,16 +1,18 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import React, {Component} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import {saveBio, fetchCommentsByUserId} from 'coral-framework/actions/user';
import {myCommentHistory} from 'coral-framework/graphql/queries';
import {saveBio} from 'coral-framework/actions/user';
import BioContainer from './BioContainer';
import {link} from 'coral-framework/PymConnection';
import NotLoggedIn from '../components/NotLoggedIn';
import {TabBar, Tab, TabContent} from '../../coral-ui';
import CommentHistory from 'coral-plugin-history/CommentHistory';
import {TabBar, Tab, TabContent, Spinner} from 'coral-ui';
import SettingsHeader from '../components/SettingsHeader';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import CommentHistory from 'coral-plugin-history/CommentHistory';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
@@ -24,12 +26,6 @@ class SettingsContainer extends Component {
this.handleTabChange = this.handleTabChange.bind(this);
}
componentWillMount () {
// Fetch commentHistory
this.props.fetchCommentsByUserId(this.props.userData.id);
}
handleTabChange(tab) {
this.setState({
activeTab: tab
@@ -37,54 +33,56 @@ class SettingsContainer extends Component {
}
render() {
const {loggedIn, userData, showSignInDialog, items, user} = this.props;
const {loggedIn, userData, asset, showSignInDialog, data, user} = this.props;
const {activeTab} = this.state;
const {me} = this.props.data;
const commentsMostRecentFirst = user
.myComments.map(id => items.comments[id])
.sort(({created_at:a}, {created_at:b}) => {
if (!loggedIn || !me) {
return <NotLoggedIn showSignInDialog={showSignInDialog}/>;
}
// descending order, created_at
// js date strings can be sorted lexigraphically.
const aLessThanB = a < b ? 1 : 0;
return a > b ? -1 : aLessThanB;
});
if (data.loading) {
return <Spinner/>;
}
return (
<RestrictedContent restricted={!loggedIn} restrictedComp={<NotLoggedIn showSignInDialog={showSignInDialog} />}>
<div>
<SettingsHeader {...this.props} />
<TabBar onChange={this.handleTabChange} activeTab={activeTab} cStyle='material'>
<Tab>All Comments ({user.myComments.length})</Tab>
<Tab>Profile Settings</Tab>
<Tab>{lang.t('allComments')} ({user.myComments.length})</Tab>
<Tab>{lang.t('profileSettings')}</Tab>
</TabBar>
<TabContent show={activeTab === 0}>
{
user.myComments.length && user.myAssets.length
? <CommentHistory
comments={commentsMostRecentFirst}
assets={user.myAssets.map(id => items.assets[id])} />
: <p>{lang.t('user-no-comment')}</p>
me.comments.length ?
<CommentHistory
comments={me.comments}
asset={asset}
link={link}
/>
:
<p>{lang.t('userNoComment')}</p>
}
</TabContent>
<TabContent show={activeTab === 1}>
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
</TabContent>
</RestrictedContent>
</div>
);
}
}
const mapStateToProps = state => ({
items: state.items.toJS(),
user: state.user.toJS()
user: state.user.toJS(),
asset: state.asset.toJS(),
auth: state.auth.toJS()
});
const mapDispatchToProps = dispatch => ({
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData)),
fetchCommentsByUserId: userId => dispatch(fetchCommentsByUserId(userId))
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData))
});
export default connect(
mapStateToProps,
mapDispatchToProps
export default compose(
connect(mapStateToProps, mapDispatchToProps),
myCommentHistory
)(SettingsContainer);
+8 -2
View File
@@ -1,8 +1,14 @@
{
"en":{
"user-no-comment": "This user has not yet left a comment."
"userNoComment": "This user has not yet left a comment.",
"allComments": "All Comments",
"profileSettings": "Profile Settings",
"myCommentHistory": "My comment History"
},
"es":{
"user-no-comment": "Aún no ha escrito ningún comentario."
"userNoComment": "Aún no ha escrito ningún comentario.",
"allComments": "Todos los comentarios",
"profileSettings": "Configuración del perfil",
"myCommentHistory": "Mi historial de comentarios"
}
}
@@ -12,7 +12,7 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => (
id="signInDialog"
open={open}
style={{
position: 'relative',
position: 'fixed',
top: offset !== 0 && offset
}}>
<span className={styles.close} onClick={handleClose}>×</span>
@@ -20,7 +20,7 @@ import {
invalidForm,
validForm,
checkLogin
} from '../../coral-framework/actions/auth';
} from 'coral-framework/actions/auth';
class SignInContainer extends Component {
initialState = {
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './Checkbox.css';
export default ({name, cStyle = 'base', onChange, label, className, info, checked = 'false'}) => (
export default ({name, cStyle = 'base', onChange, label, className, info, ...props}) => (
<label className={`${styles.label} ${styles[`type--${cStyle}`]} ${className}`} htmlFor={name}>
<input type="checkbox" id={name} name={name} onChange={onChange} checked={checked} />
<input type="checkbox" id={name} name={name} onChange={onChange} {...props} />
<span className={styles.checkbox}></span>
{label && <span>{label}</span>}
{info && (
+9 -1
View File
@@ -821,6 +821,14 @@ definitions:
current_user:
type: object
description: Will include the action performed by the currently logged in user if that user has taken an action on this item. Otherwise will return null.
created_at:
type: string
format: date-time
description: Oldest created_at of actions in this set.
updated_at:
type: string
format: date-time
description: Newest updated_at of actions in this set.
Action:
type: object
description: A single action taken by a user.
@@ -916,7 +924,7 @@ definitions:
type: array
items:
type: string
description: Roles occupied by the user (e.g. 'admin', 'moderator', etc.)
description: Roles occupied by the user (e.g. 'ADMIN', 'MODERATOR', etc.)
status:
type: string
description: The current status of the user in the system.
+5
View File
@@ -110,10 +110,15 @@ const ErrNotAuthorized = new APIError('not authorized', {
status: 401
});
// ErrSettingsNotInit is returned when the settings are required but not
// initialized.
const ErrSettingsNotInit = new Error('settings not initialized, run `./bin/cli setup` to setup the application first');
module.exports = {
ExtendableError,
APIError,
ErrPasswordTooShort,
ErrSettingsNotInit,
ErrMissingEmail,
ErrMissingPassword,
ErrMissingToken,
+23
View File
@@ -0,0 +1,23 @@
const loaders = require('./loaders');
const mutators = require('./mutators');
/**
* Stores the request context.
*/
class Context {
constructor({user = null}) {
// Load the current logged in user to `user`, otherwise this'll be null.
if (user) {
this.user = user;
}
// Create the loaders.
this.loaders = loaders(this);
// Create the mutators.
this.mutators = mutators(this);
}
}
module.exports = Context;
+14
View File
@@ -0,0 +1,14 @@
const schema = require('./schema');
const Context = require('./context');
module.exports = {
createGraphOptions: (req) => ({
// Schema is created already, so just include it.
schema,
// Load in the new context here, this'll create the loaders + mutators for
// the lifespan of this request.
context: new Context(req)
})
};
+41
View File
@@ -0,0 +1,41 @@
const DataLoader = require('dataloader');
const util = require('./util');
const ActionsService = require('../../services/actions');
const ActionModel = require('../../models/action');
/**
* Looks up actions based on the requested id's all bounded by the user.
* @param {Object} context the context of the request
* @param {Array} ids array of id's to get
* @return {Promise} resolves to the promises of the requested actions
*/
const genActionSummariessByItemID = ({user = {}}, item_ids) => {
return ActionsService
.getActionSummaries(item_ids, user.id)
.then(util.arrayJoinBy(item_ids, 'item_id'));
};
/**
* Search for actions based on their action_type and item_type and ensures that
* the actions returned have unique item id's.
* @param {String} action_type the action to search by
* @param {String} item_type the item id to search by
* @return {Promise} resolves to distinct items actions
*/
const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
return ActionModel.distinct('item_id', {action_type, item_type});
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
Actions: {
getSummariesByItemID: new DataLoader((ids) => genActionSummariessByItemID(context, ids)),
getByTypes: ({action_type, item_type}) => getItemIdsByActionTypeAndItemType(context, action_type, item_type)
}
});
+64
View File
@@ -0,0 +1,64 @@
const DataLoader = require('dataloader');
const url = require('url');
const errors = require('../../errors');
const scraper = require('../../services/scraper');
const util = require('./util');
const AssetModel = require('../../models/asset');
const AssetsService = require('../../services/assets');
/**
* Retrieves assets by an array of ids.
* @param {Object} context the context of the request
* @param {Array} ids array of ids to lookup
*/
const genAssetsByID = (context, ids) => AssetModel.find({
id: {
$in: ids
}
}).then(util.singleJoinBy(ids, 'id'));
/**
* This endpoint find or creates an asset at the given url when it is loaded.
* @param {Object} context the context of the request
* @param {String} asset_url the url passed in from the query
* @returns {Promise} resolves to the asset
*/
const findOrCreateAssetByURL = (context, asset_url) => {
// Verify that the asset_url is parsable.
let parsed_asset_url = url.parse(asset_url);
if (!parsed_asset_url.protocol) {
return Promise.reject(errors.ErrInvalidAssetURL);
}
return AssetsService.findOrCreateByUrl(asset_url)
.then((asset) => {
// If the asset wasn't scraped before, scrape it! Otherwise just return
// the asset.
if (!asset.scraped) {
return scraper.create(asset).then(() => asset);
}
return asset;
});
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
Assets: {
// TODO: decide whether we want to move these to mutators or not, as in fact
// this operation create a new asset if one isn't found.
getByURL: (url) => findOrCreateAssetByURL(context, url),
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
getAll: new util.SingletonResolver(() => AssetModel.find({}))
}
});
+145
View File
@@ -0,0 +1,145 @@
const util = require('./util');
const CommentModel = require('../../models/comment');
/**
* Returns the comment count for all comments that are public based on their
* asset ids.
* @param {Object} context graph context
* @param {Array<String>} asset_ids the ids of assets for which there are
* comments that we want to get
*/
const getCountsByAssetID = (context, asset_ids) => {
return CommentModel.aggregate([
{
$match: {
asset_id: {
$in: asset_ids
},
status: {
$in: [null, 'ACCEPTED']
},
parent_id: null
}
},
{
$group: {
_id: '$asset_id',
count: {
$sum: 1
}
}
}
])
.then(util.singleJoinBy(asset_ids, '_id'))
.then((results) => results.map((result) => result ? result.count : 0));
};
/**
* Returns the comment count for all comments that are public based on their
* parent ids.
* @param {Object} context graph context
* @param {Array<String>} parent_ids the ids of parents for which there are
* comments that we want to get
*/
const getCountsByParentID = (context, parent_ids) => {
return CommentModel.aggregate([
{
$match: {
parent_id: {
$in: parent_ids
},
status: {
$in: [null, 'ACCEPTED']
}
}
},
{
$group: {
_id: '$parent_id',
count: {
$sum: 1
}
}
}
])
.then(util.singleJoinBy(parent_ids, '_id'))
.then((results) => results.map((result) => result ? result.count : 0));
};
/**
* Retrieves comments based on the passed in query that is filtered by the
* current used passed in via the context.
* @param {Object} context graph context
* @param {Object} query query terms to apply to the comments query
*/
const getCommentsByQuery = ({user}, {ids, statuses, asset_id, parent_id, limit, cursor, sort}) => {
let comments = CommentModel.find();
// Only administrators can search for comments with statuses that are not
// `null`, or `'ACCEPTED'`.
if (user != null && user.hasRoles('ADMIN') && statuses) {
comments = comments.where({
status: {
$in: statuses
}
});
} else {
comments = comments.where({
status: {
$in: [null, 'ACCEPTED']
}
});
}
if (ids) {
comments = comments.find({
id: {
$in: ids
}
});
}
if (asset_id) {
comments = comments.where({asset_id});
}
// We perform the undefined check because, null, is a valid state for the
// search to be with, which indicates that it is at depth 0.
if (parent_id !== undefined) {
comments = comments.where({parent_id});
}
if (cursor) {
if (sort === 'REVERSE_CHRONOLOGICAL') {
comments = comments.where({
created_at: {
$lt: cursor
}
});
} else {
comments = comments.where({
created_at: {
$gt: cursor
}
});
}
}
return comments
.sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1})
.limit(limit);
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
Comments: {
getByQuery: (query) => getCommentsByQuery(context, query),
countByAssetID: new util.SharedCacheDataLoader('Comments.countByAssetID', 3600, (ids) => getCountsByAssetID(context, ids)),
countByParentID: new util.SharedCacheDataLoader('Comments.countByParentID', 3600, (ids) => getCountsByParentID(context, ids))
}
});
+28
View File
@@ -0,0 +1,28 @@
const _ = require('lodash');
const Actions = require('./actions');
const Assets = require('./assets');
const Comments = require('./comments');
const Settings = require('./settings');
const Users = require('./users');
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => {
// We need to return an object to be accessed.
return _.merge(...[
Actions,
Assets,
Comments,
Settings,
Users
].map((loaders) => {
// Each loader is a function which takes the context.
return loaders(context);
}));
};

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