Compare commits

...
418 Commits
Author SHA1 Message Date
David Erwin 35f01c2ecc Merge pull request #516 from coralproject/remove-rejected-comments
let ApolloClient know which types are interfaces
2017-04-20 20:08:50 -04:00
Riley Davis 945b0b5df8 Merge branch 'master' into remove-rejected-comments 2017-04-20 18:04:11 -06:00
Riley Davis a58e15c4c8 let ApolloClient know which types are interfaces 2017-04-20 18:00:06 -06:00
David Erwin 0dee09dd93 Merge pull request #515 from coralproject/mobilify-login-popup
Mobilify login popup
2017-04-20 17:42:00 -04:00
David Erwin e8c3bdc5b1 Merge branch 'master' into mobilify-login-popup 2017-04-20 17:28:54 -04:00
Riley Davis bcf31e5fa9 reporting a comment zooms the window 2017-04-20 15:19:19 -06:00
Riley Davis a2131c205a make the viewport non-scalable on mobile. make input font-size bigger to prevent mobile zoom effect 2017-04-20 15:14:15 -06:00
Riley Davis 4ae68b4bd3 make the login pop-up scale on mobile 2017-04-20 15:07:55 -06:00
David Erwin 267338c851 Merge pull request #514 from coralproject/premod-queue-fix
Premod queue fix
2017-04-20 16:57:43 -04:00
Riley Davis fddd8d63c7 Merge branch 'master' into premod-queue-fix 2017-04-20 14:53:51 -06:00
David Erwin bdd8ec3789 Merge pull request #505 from coralproject/default-resolve-type
Added defaultResolveType
2017-04-20 16:40:24 -04:00
Riley Davis 8708fcf2cd actually, we want to have the action_summaries on all requests, since a plugin could potentially pre-flag a pre-mod comment 2017-04-20 14:37:48 -06:00
David Erwin 1db168df8c Merge branch 'master' into default-resolve-type 2017-04-20 16:33:54 -04:00
Riley Davis 3dc2674f47 the pre-mod tab does not request action_summaries since there wouldn't be any 2017-04-20 14:33:49 -06:00
David Erwin 1ec30854c2 Merge pull request #510 from coralproject/login-popup
Login Popup
2017-04-20 16:10:18 -04:00
David Erwin 7c38b9b54f Merge branch 'master' into default-resolve-type 2017-04-20 16:03:38 -04:00
Chi Vinh Le 4519a550a2 Fix fresh login on profile 2017-04-21 02:27:26 +07:00
David Erwin 00e24ce88a Merge branch 'master' into login-popup 2017-04-20 15:15:51 -04:00
David Erwin 467a32038e Merge pull request #512 from coralproject/action-summary-fix
Action summary fix - Part Deux
2017-04-20 15:15:38 -04:00
Riley Davis 2d212393ff Merge branch 'master' into login-popup 2017-04-20 12:57:13 -06:00
Riley Davis d1729e5115 camel_case 2017-04-20 12:44:25 -06:00
David Erwin 85e552cf04 Merge branch 'master' into action-summary-fix 2017-04-20 14:40:16 -04:00
David Erwin 2c5f46b3ae Merge pull request #513 from coralproject/fix-ignored-user-error
Fix ignored users when not logged in
2017-04-20 14:39:24 -04:00
Chi Vinh Le 3ff9313b03 Fix ignored users when not logged in 2017-04-21 01:18:51 +07:00
Belén Curcio fc4b821f05 Merge branch 'master' into action-summary-fix 2017-04-20 15:04:09 -03:00
Chi Vinh Le 8fc2d5e8aa Merge branch 'login-popup' of ssh://github.com/coralproject/talk into login-popup 2017-04-21 00:54:45 +07:00
Chi Vinh Le f0e584f0b3 Restore login flow in ProfileContainer 2017-04-21 00:52:45 +07:00
Chi Vinh Le 6df2103da3 Remove obsolete offset and styling 2017-04-21 00:45:00 +07:00
David Erwin 900cb33122 Merge branch 'master' into default-resolve-type 2017-04-20 13:38:06 -04:00
David Erwin 6075317564 Merge pull request #511 from coralproject/build-fix
Added --force to rebuild native modules
2017-04-20 13:37:33 -04:00
David Erwin f3f651c562 Merge branch 'master' into build-fix 2017-04-20 13:33:45 -04:00
Belen Curcio 1ffa78dcfb Merge branch 'login-popup' of github.com:coralproject/talk into login-popup 2017-04-20 14:28:16 -03:00
David Erwin cbba3acf4a Merge pull request #508 from coralproject/action-label-fix
Quick fix action labels in Mobile
2017-04-20 13:27:37 -04:00
Chi Vinh Le f3f1cc53fe Fix safari popup resizing 2017-04-21 00:27:26 +07:00
David Erwin 0de87af07a Merge branch 'master' into login-popup 2017-04-20 13:25:00 -04:00
Belen Curcio c47047c3ff Classes 2017-04-20 14:23:44 -03:00
Belen Curcio c9207fd620 Merge branch 'login-popup' of github.com:coralproject/talk into login-popup 2017-04-20 14:19:04 -03:00
Belen Curcio 09cb518049 Merge branch 'master' of github.com:coralproject/talk into login-popup 2017-04-20 14:18:40 -03:00
Chi Vinh Le 5b7750f7a5 Reuse same store and action flows 2017-04-20 23:48:30 +07:00
Riley Davis 50774f1922 remove null checks 2017-04-20 10:04:28 -06:00
Riley Davis eda15538fe add kiwi's and wyatt's changes 2017-04-20 10:02:09 -06:00
Wyatt Johnson 50219922d5 Switched to lock at node 7.8 2017-04-20 09:35:49 -06:00
Chi Vinh Le b12d4ef4a2 WIP 2017-04-20 21:30:24 +07:00
Chi Vinh Le 348c01ae80 Fix linting errors 2017-04-20 20:58:45 +07:00
Chi Vinh Le 1b95fbd852 Remove SignInContainer from admin 2017-04-20 20:34:36 +07:00
Chi Vinh Le 3bf2187688 Fix crashing admin when using framework 2017-04-20 18:45:55 +07:00
Kiwi 51b866cd65 Merge branch 'master' into default-resolve-type 2017-04-20 16:55:04 +07:00
Kiwi b7ffbdb762 Merge branch 'master' into action-label-fix 2017-04-20 16:48:12 +07:00
Riley Davis ffdec225c7 fix RespectButton component code 2017-04-19 22:02:57 -06:00
Riley Davis 59fbff3caf fix how FlagActionSummaries works in the embed 2017-04-19 21:15:03 -06:00
Wyatt Johnson 53f7ab86c0 Merge branch 'master' into build-fix 2017-04-19 18:16:12 -06:00
Wyatt Johnson 4ac71c884b added --force to rebuild native modules 2017-04-19 18:10:16 -06:00
Belen Curcio 6155a46e71 Sign up popup 2017-04-19 18:10:09 -03:00
Belen Curcio 13965e1111 Adding actions, routes and login popup 2017-04-19 17:04:49 -03:00
Riley Davis 7b4e8ef204 Merge pull request #499 from coralproject/flag-detail
Flag detail
2017-04-19 13:38:01 -06:00
Riley Davis 1abafb7037 Merge branch 'master' into flag-detail 2017-04-19 11:21:55 -06:00
Riley Davis 1535f10450 Merge branch 'master' into action-label-fix 2017-04-19 11:11:21 -06:00
Riley Davis 82927b77be Merge pull request #507 from coralproject/premod-load-collapse
Fix collapsing UI
2017-04-19 09:57:23 -06:00
Riley Davis b34cd2d4c1 Merge branch 'master' into premod-load-collapse 2017-04-19 09:50:30 -06:00
Riley Davis bd0852bf68 Merge pull request #506 from coralproject/fix-load-more-premod
Fix load more in premod queue
2017-04-19 09:48:40 -06:00
Belén Curcio c72a5c6df9 Merge branch 'master' into action-label-fix 2017-04-19 12:05:59 -03:00
Belen Curcio 9ddc6c35fd Action labels default.css, temporary fix 2017-04-19 11:59:07 -03:00
Chi Vinh Le f724353cf0 Fix collapsing UI 2017-04-19 16:38:02 +07:00
Chi Vinh Le 7698bdf930 Fix load more in premod queue 2017-04-19 16:35:53 +07:00
Kiwi 1750510d34 Merge pull request #504 from coralproject/valid-notification-actions
Return a value for notification actions
2017-04-19 16:12:40 +07:00
Wyatt Johnson 2eda1cb1e4 Added defaultResolveType 2017-04-18 14:50:57 -06:00
Chi Vinh Le 4ce4c74fd9 Return a value for notification actions 2017-04-19 01:09:59 +07:00
Belén Curcio bfacc8f2f7 Merge pull request #479 from coralproject/plugin_examples
Hooks and Context for Comment Plugins And Off-Topic Plugin
2017-04-18 13:52:30 -03:00
Belén Curcio ad15b38a5d Merge branch 'master' into plugin_examples 2017-04-18 13:46:44 -03:00
Wyatt Johnson fd6282077f Adjusted test to conform to new form 2017-04-17 16:05:44 -06:00
Riley Davis b5210f3727 more translations 2017-04-17 14:42:18 -06:00
Riley Davis cc6882168b add translations 2017-04-17 14:31:45 -06:00
Riley Davis a5d11a05d1 Merge branch 'master' into flag-detail 2017-04-17 14:00:32 -06:00
Gabriela Rodríguez Berón 7526d64bc0 Merge pull request #493 from coralproject/143520971-stream-settings
Configure stream settings
2017-04-17 12:45:34 -07:00
Gabriela Rodríguez Berón 04a4918fd5 Update ConfigureStreamContainer.js 2017-04-17 12:35:29 -07:00
Gabriela Rodríguez Berón 371e3b951d Merge branch 'master' into 143520971-stream-settings 2017-04-17 12:25:40 -07:00
gaba f66e12be18 Merge branch 'master' into 143520971-stream-settings 2017-04-17 12:07:19 -07:00
gaba fb010bfa67 Rename settings of the state to dirtySettings and add TO DO item to be sure we refactor this next time we do a refactor. 2017-04-17 11:51:24 -07:00
Belen Curcio e729a673a3 Merge branch 'master' of github.com:coralproject/talk into plugin_examples 2017-04-17 14:17:42 -03:00
Riley Davis d6b2d6d876 Merge branch 'master' into flag-detail 2017-04-14 15:38:15 -06:00
Riley Davis 55cdc98303 add some proptypes 2017-04-14 15:36:29 -06:00
Riley Davis d5ff9d3eeb show the details 2017-04-14 15:32:06 -06:00
Wyatt Johnson 404301bd43 Merge pull request #501 from coralproject/docs-update
Added note for production based deploy
2017-04-14 15:22:45 -06:00
Wyatt Johnson 8c478d1c13 Merge branch 'master' into docs-update 2017-04-14 15:18:44 -06:00
Wyatt Johnson bee27da725 Added note for production based deploy 2017-04-14 15:16:42 -06:00
David Erwin a873034288 Merge pull request #498 from coralproject/docs-update
Docs update
2017-04-14 17:16:26 -04:00
Wyatt Johnson 8164fa7813 Merge branch 'master' into docs-update 2017-04-14 15:07:02 -06:00
David Erwin d1bc747009 Merge pull request #500 from coralproject/username-profanity-fix
Username Profanity Fix
2017-04-14 17:05:27 -04:00
Wyatt Johnson e028e30afd Added line about SSL certificates 2017-04-14 15:04:35 -06:00
Riley Davis ff3cffdc8f load actions 2017-04-14 14:59:51 -06:00
Wyatt Johnson d4fc8a474d Should resolve bug relating to usernames being set as "undefined" 2017-04-14 14:57:13 -06:00
Wyatt Johnson a19fe02782 Added setup/usage docs 2017-04-14 14:49:23 -06:00
Gabriela Rodríguez Berón 81e040397e Merge branch 'master' into docs-update 2017-04-14 12:09:45 -07:00
Riley Davis 07ea548d46 Merge branch 'master' into flag-detail 2017-04-14 12:56:11 -06:00
Gabriela Rodríguez Berón 99b0f8f2ad Merge pull request #497 from coralproject/docs-metrics
EXTRA-SHORT: A note on the from/to parameters.
2017-04-14 11:29:08 -07:00
Wyatt Johnson cb652dbb86 Merge branch 'master' into docs-update 2017-04-14 12:23:43 -06:00
gaba fa944927e4 A note on the from/to parameters. 2017-04-14 10:41:46 -07:00
Gabriela Rodríguez Berón fb6ba37366 Merge branch 'master' into docs-metrics 2017-04-14 10:40:28 -07:00
gaba 9cf31e0bfa A note on the from/to parameters. 2017-04-14 10:37:05 -07:00
Wyatt Johnson 061e775227 Spell checks + doc updates 2017-04-14 10:26:06 -06:00
Kim Gardner 258321201b Merge pull request #496 from coralproject/add_translations
Add translations
2017-04-13 20:15:07 -04:00
Kim Gardner 71e1c01552 Merge branch 'master' into add_translations 2017-04-13 20:10:19 -04:00
Kim Gardner 1fc0bf6ec1 Merge pull request #494 from coralproject/git-client-ignore
ignore node_modules in plugins
2017-04-13 20:09:52 -04:00
Kim Gardner e58cf8e933 Merge branch 'master' into git-client-ignore 2017-04-13 20:05:13 -04:00
Kim Gardner 7e34bc1d13 Merge pull request #477 from gobengo/ignore
Ignore User
2017-04-13 20:05:02 -04:00
Kim Gardner 193ed8c863 Merge branch 'master' into ignore 2017-04-13 20:00:59 -04:00
Wyatt Johnson 642a9b87ca Merge branch 'master' into git-client-ignore 2017-04-13 17:33:49 -06:00
gaba d9c8882d8f More translations in the configuration. 2017-04-13 16:19:21 -07:00
gaba 902071c0fe Adds title of most flagged in dashboard. 2017-04-13 16:11:25 -07:00
Riley Davis 018e88f7cf Merge pull request #495 from coralproject/143357607-close-stream
Bug on closing the stream
2017-04-13 15:28:43 -06:00
gaba c395bc8849 Adds closedAt to state... and check that closedTimeout settings exists for the Asset. 2017-04-13 14:21:45 -07:00
Riley Davis bd782b98ab Merge branch 'master' into 143520971-stream-settings 2017-04-13 15:17:45 -06:00
Riley Davis d5552df41f ignore node_modules in plugins 2017-04-13 14:46:11 -06:00
Wyatt Johnson 7aa262e97f Added new cluster style deployment docs 2017-04-13 13:46:37 -06:00
gaba fa72a661ad Going back to previous setting state as I do not want it to re-render. 2017-04-13 12:43:24 -07:00
Wyatt Johnson 90db03bc29 rearranged docs 2017-04-13 13:40:13 -06:00
Riley Davis 6aaca9130a saving my place 2017-04-13 12:56:48 -06:00
Wyatt Johnson 7bcd336f51 Merge pull request #492 from coralproject/plugins-install-fix
Ignore lifecycle hooks during plugin install
2017-04-13 12:56:45 -06:00
gaba 9dae72dd8a Good catch. Use setState. 2017-04-13 11:44:08 -07:00
Benjamin Goering 118da885fd Merge branch 'master' of github.com:coralproject/talk into ignore_rb2 2017-04-13 12:12:10 -05:00
gaba abc7aa7b30 Uh... wrong element. 2017-04-12 18:39:56 -07:00
Belen Curcio 7969985f59 Merge branch 'master' of github.com:coralproject/talk into plugin_examples 2017-04-12 22:27:46 -03:00
Belen Curcio 22fc8555ad Extending typeDefs with Off-Topic Plugin 2017-04-12 22:26:30 -03:00
Belen Curcio d9d3f5e85b Tests and cleanup, 2 to go 2017-04-12 21:17:07 -03:00
Belen Curcio 32defa66e6 Clean up 2017-04-12 21:04:47 -03:00
Belen Curcio 2e37512851 Deleting useless constructor 2017-04-12 20:54:11 -03:00
Belen Curcio 78286d3374 Running CommentBox reducers and triggering actions from Plugins! 2017-04-12 20:51:18 -03:00
Wyatt Johnson 941db338d0 Updates for plugin deployment 2017-04-12 16:51:20 -06:00
Belen Curcio 7424fc9774 Create comments with Tags 2017-04-12 19:42:39 -03:00
Wyatt Johnson d5016dff58 Updates to metadata service docs 2017-04-12 16:37:56 -06:00
Belen Curcio 7fca451263 Immutable plugins 2017-04-12 19:21:55 -03:00
Wyatt Johnson dbc95db805 Updates for docker images 2017-04-12 15:15:51 -06:00
Gabriela Rodríguez Berón 42a1026f9c Merge branch 'master' into 143520971-stream-settings 2017-04-12 14:12:15 -07:00
Wyatt Johnson 9e16e58d19 Merge branch 'master' into docs-update 2017-04-12 15:10:22 -06:00
gaba e04f0f7070 Moves settings into the state for the stream. Adds premodlinks to the query. 2017-04-12 14:08:55 -07:00
Wyatt Johnson 2fcfaa8a99 More updates 2017-04-12 15:08:07 -06:00
Wyatt Johnson f7ee08bd3a Ignore lifecycle hooks during plugin install 2017-04-12 14:39:11 -06:00
Wyatt Johnson 31960d67f3 Added more to docs 2017-04-12 14:25:43 -06:00
David Erwin b3ce4507a3 Merge pull request #476 from coralproject/metadata
MetadataService
2017-04-12 16:02:39 -04:00
David Erwin c7e1943272 Merge branch 'master' into metadata 2017-04-12 15:57:24 -04:00
Belen Curcio c0913d8e98 Disable postButton and enable 2017-04-12 16:55:43 -03:00
Belen Curcio e8d9f8aab8 fix createComment Mutations 2017-04-12 16:51:41 -03:00
Kim Gardner 3e882452db Merge pull request #491 from coralproject/fix-broken-links
Fix broken links to Stories page
2017-04-12 15:45:01 -04:00
Kim Gardner 29fe5e2901 Fix broken links to Stories page 2017-04-12 15:40:34 -04:00
Wyatt Johnson 6c0e00155f Initial update 2017-04-12 13:29:01 -06:00
Belen Curcio 3837c316d5 Merge branch 'plugin_examples' of github.com:coralproject/talk into plugin_examples 2017-04-12 16:25:48 -03:00
Belen Curcio 336dd261ca createCommentInput 2017-04-12 16:23:13 -03:00
Kim Gardner 29aacdc00e Merge pull request #490 from coralproject/fix-more-buttons
Fix load more buttons
2017-04-12 15:18:06 -04:00
Kim Gardner d892d1327d Merge branch 'master' into fix-more-buttons 2017-04-12 15:12:27 -04:00
Kim Gardner 309b41a3cf Merge pull request #485 from coralproject/143520139-permalink-show-all
Clicking '# comments' tab refresh all the comments back
2017-04-12 15:11:42 -04:00
gaba cbde4cc8bc Removes handleClick as it is working now with the changeTab (as the viewAllComments was added). /cc @kiwi 2017-04-12 11:58:41 -07:00
Chi Vinh Le 81bd983f0b Handle empty stream case 2017-04-13 01:34:27 +07:00
Chi Vinh Le 92205a2c6f Fix load more buttons 2017-04-13 01:17:46 +07:00
gaba 48795b29bf Removes isAdmin for refetching when changing tabs. 2017-04-12 10:41:15 -07:00
gaba f4f8e5053f Remove the binding in the constructor. 2017-04-12 10:12:00 -07:00
Gabriela Rodríguez Berón 51774323de Merge branch 'master' into 143520139-permalink-show-all 2017-04-12 10:10:17 -07:00
Belen Curcio e256c4a014 Using Arrays instead of objects for Hooks 2017-04-12 13:50:56 -03:00
Belen Curcio 0254086c28 Using Arrays instead of objects for Hooks 2017-04-12 13:45:42 -03:00
Wyatt Johnson 191026ecf2 Merge branch 'master' into metadata 2017-04-12 10:37:12 -06:00
Belén Curcio 19f402ddc8 Merge branch 'master' into plugin_examples 2017-04-12 11:28:08 -03:00
Wyatt Johnson c19e2979fc Merge pull request #489 from coralproject/fix-double-quote-typos
Double quotes should be single
2017-04-12 08:14:02 -06:00
Kim Gardner a19d6544a7 Don't need to escape single quotes within double quotes 2017-04-12 10:05:21 -04:00
Kim Gardner d74cdd938f Double quotes should be single 2017-04-12 09:51:32 -04:00
Wyatt Johnson d4aa2011ff Merge pull request #488 from coralproject/bump-version-update-description
Bump version and update description
2017-04-12 07:39:12 -06:00
Kim Gardner 8e76a34631 Bump version and update description 2017-04-12 08:45:19 -04:00
Belen Curcio ac632a69d2 unregister hooks names 2017-04-12 09:40:18 -03:00
Belen Curcio 72c698231b unregister hooks 2017-04-12 09:33:24 -03:00
Kim Gardner 143ce795c3 Merge branch 'master' into 143520139-permalink-show-all 2017-04-12 08:14:39 -04:00
Belen Curcio 83eb6492c2 Styles 2017-04-12 08:54:50 -03:00
Belén Curcio 7590cee986 Merge branch 'master' into plugin_examples 2017-04-12 08:52:03 -03:00
Kim Gardner 358743b3ab Merge pull request #486 from coralproject/styling-fixes
Styling fixes
2017-04-12 07:37:14 -04:00
Belén Curcio f313d2a7d2 Merge branch 'master' into plugin_examples 2017-04-12 08:32:39 -03:00
Belén Curcio 1f862cce74 Merge branch 'master' into 143520139-permalink-show-all 2017-04-12 08:27:37 -03:00
Kim Gardner 6f4d591d0b Merge branch 'master' into styling-fixes 2017-04-12 07:23:56 -04:00
Kim Gardner c084c92c31 Merge pull request #487 from coralproject/validator-fix
Fixed plugin validation
2017-04-12 07:23:05 -04:00
Belen Curcio 1a678a0cb3 Removing username -- with lintin 2017-04-12 08:21:39 -03:00
Belen Curcio cc0f757746 Removing username 2017-04-12 08:16:02 -03:00
Belén Curcio 2ecf64ae3e Merge branch 'master' into plugin_examples 2017-04-12 08:09:49 -03:00
Wyatt Johnson 6ac4bcf796 Fixed plugin validation 2017-04-11 22:25:13 -06:00
Belen Curcio 2e3021e66b Linting 2017-04-11 22:17:04 -03:00
Belen Curcio 63b630833a Fake Comment Style 2017-04-11 22:12:10 -03:00
Belen Curcio e4aa98b87a Hidding login as 2017-04-11 22:00:55 -03:00
gaba f900727455 Move the method to the class, changes name and removes wrapper. 2017-04-11 17:57:56 -07:00
Belen Curcio 3c6ce7a80f QuestionBox 2017-04-11 21:56:43 -03:00
Gabriela Rodríguez Berón f44798361e Merge branch 'master' into 143520139-permalink-show-all 2017-04-11 15:06:22 -07:00
gaba 56dedb551f Adds click to showAll and refresh query to the '# comments' tab. 2017-04-11 15:02:04 -07:00
Kim Gardner b15fd7acbf Merge pull request #484 from coralproject/plugin-fixes
Fixed bugs with plugin install
2017-04-11 17:56:13 -04:00
Kim Gardner 1dcfa269b3 Merge branch 'master' into plugin-fixes 2017-04-11 17:51:18 -04:00
Kim Gardner 36dff1afb8 Merge pull request #483 from coralproject/143202269-suspend-user-email
Adds missing template.
2017-04-11 17:51:02 -04:00
Benjamin Goering fbcea2de6d Merge branch 'master' into ignore 2017-04-11 14:49:56 -07:00
Kim Gardner 3307bf5ef5 Merge branch 'master' into 143202269-suspend-user-email 2017-04-11 17:46:58 -04:00
Kim Gardner f6cb97e29c Merge pull request #482 from coralproject/flash-messages
Flash Messages fix
2017-04-11 17:46:44 -04:00
Benjamin Goering 247583b858 Merge branch 'ignore' of github.com:gobengo/talk into ignore 2017-04-11 14:33:10 -07:00
Benjamin Goering 8cf2130e25 Update spanish translation for commentIsIgnored 2017-04-11 14:31:46 -07:00
Gabriela Rodríguez Berón 295f4ddec2 Merge branch 'master' into ignore 2017-04-11 14:26:53 -07:00
Kim Gardner 7ab8dbf3c6 Merge branch 'master' into flash-messages 2017-04-11 17:22:02 -04:00
Kim Gardner 4ce37a4bbb Merge pull request #480 from coralproject/infobox-style
Infobox Style
2017-04-11 17:21:53 -04:00
Benjamin Goering a7be2772c3 IgnoredCommentTombstone body text is more i18n 2017-04-11 14:20:39 -07:00
Benjamin Goering 847ad2263b Split IgnoreUserWizard into own file 2017-04-11 14:20:39 -07:00
Benjamin Goering eb1c1db09b Ignore users queries are excludeIgnored: Boolean instead of notIgnoredBy: String 2017-04-11 14:20:39 -07:00
Benjamin Goering 12e383ef13 refetch streamQuery on stopIgnoringUser 2017-04-11 14:20:39 -07:00
Benjamin Goering a43e713ac0 Move TopRightMenu down 5px 2017-04-11 14:20:38 -07:00
Benjamin Goering 0dff670b70 Slot component wraps in inline element, not block (which can affect other styles) 2017-04-11 14:20:38 -07:00
Benjamin Goering 58ffd61884 Move TopRightMenu component out of Comment and into own file 2017-04-11 14:20:38 -07:00
Benjamin Goering 4d2e5ed81f Comments by logged in user dont show TopRightMenu + IgnoreUserWizard 2017-04-11 14:20:38 -07:00
Benjamin Goering 7dfe12b874 Users cannot ignore themselves at service layer 2017-04-11 14:20:37 -07:00
Benjamin Goering 58736586e4 Asset commentCount and totalCommentCount can be personalized by notIgnoredBy 2017-04-11 14:20:37 -07:00
Benjamin Goering b561e9f643 Comment replyCount edge can now be filtered by notIgnoredBy 2017-04-11 14:20:37 -07:00
Benjamin Goering 873da1cea9 Embed doesn't show comments from ignored users 2017-04-11 14:20:37 -07:00
Benjamin Goering c7d8c40770 Rename embed's My Comments tab to 'My profile' 2017-04-11 14:19:34 -07:00
Benjamin Goering 80658f51d1 Handle errors when ignoring a user 2017-04-11 14:19:34 -07:00
Benjamin Goering 2656141d59 Add stopIgnoringUser mutation and Embed uses it 2017-04-11 14:19:34 -07:00
Benjamin Goering c59ddb72a6 Manage ignored users UI 2017-04-11 14:19:34 -07:00
Benjamin Goering 70ec5d5f54 Remove font-size: 100% for all elements in embed-stream 2017-04-11 14:19:33 -07:00
Benjamin Goering bc3ec84180 IgnoredCommentTombstone renders for replies too 2017-04-11 14:19:33 -07:00
Benjamin Goering 803db345ac TopRightMenu no longer shows a list of choices, just IgnoreUserWizard 2017-04-11 14:19:33 -07:00
Benjamin Goering 990b9794e9 When a user is ignored, replace their comments with a tombstone 2017-04-11 14:19:33 -07:00
Benjamin Goering a9412c17c9 Ignore User UI actually sends mutation 2017-04-11 14:19:32 -07:00
Benjamin Goering 545f176bb8 Add ignoreUser mutation and myIgnoredUsers root query 2017-04-11 14:19:32 -07:00
Benjamin Goering d3d7045227 IgnoreUserWizard steps and proper chevron 2017-04-11 14:19:32 -07:00
Benjamin Goering 2dc882a12e Start UI for TopRightMenu and IgnoreUserWizard on Comments 2017-04-11 14:19:32 -07:00
Wyatt Johnson 3a3b89b03b Fixed bugs with plugin install 2017-04-11 14:59:02 -06:00
Gabriela Rodríguez Berón 152a9417b2 Merge branch 'master' into plugin_examples 2017-04-11 13:56:27 -07:00
Kim Gardner 313e368565 Merge branch 'master' into flash-messages 2017-04-11 15:28:00 -04:00
Belén Curcio 8c627c5a7b Merge branch 'master' into 143202269-suspend-user-email 2017-04-11 16:25:44 -03:00
Kim Gardner 7838347861 Merge branch 'master' into infobox-style 2017-04-11 15:23:58 -04:00
gaba c5520e3fb6 Adds missing template. 2017-04-11 12:22:05 -07:00
Belén Curcio 972cce4e8f Merge pull request #481 from coralproject/143202225-search-community
Fix search box in Community Admin.
2017-04-11 16:21:09 -03:00
Belen Curcio 93ca0bf79d fixed flash messages 2017-04-11 16:14:02 -03:00
gaba a7493af979 Fix search box in Community Admin. 2017-04-11 11:58:19 -07:00
Belen Curcio 9335856474 Links style 2017-04-11 15:55:18 -03:00
Belen Curcio a27044b691 Ádding style for the infoBox 2017-04-11 15:49:10 -03:00
Belén Curcio a77a623e94 Merge branch 'master' into plugin_examples 2017-04-11 15:13:51 -03:00
Belen Curcio 5d81429673 Clean up 2017-04-11 14:47:39 -03:00
Belen Curcio 536b2ed371 Liniting 2017-04-11 13:46:27 -03:00
Belen Curcio aeaa9ae80f Adding preSubmit hoojs 2017-04-11 12:42:48 -03:00
Belen Curcio 014c23bb95 Support for hooks, preSubmit and postSubmit 2017-04-11 12:41:18 -03:00
Kim Gardner 826f051175 Merge pull request #472 from coralproject/fix-respect-button-bugs
Fix quick mutations and multiple respects
2017-04-11 11:29:39 -04:00
Kim Gardner 4c1ae22765 Merge branch 'master' into fix-respect-button-bugs 2017-04-11 11:24:23 -04:00
Kim Gardner b7c2861a28 Merge pull request #475 from coralproject/fix-fetch-more
Adjust fetchMore for apollo 1.0
2017-04-11 11:24:12 -04:00
Kim Gardner 97666df1e0 Merge branch 'master' into fix-fetch-more 2017-04-11 11:18:19 -04:00
Kim Gardner 34486028e5 Merge pull request #478 from coralproject/disable-profanity-username-check
Temporarily disable username profanity check
2017-04-11 11:18:07 -04:00
David Erwin 2c8d03bd8d Temporarily disable username profanity check 2017-04-11 11:05:55 -04:00
Belen Curcio 6aad4411d9 Styling for plugins 2017-04-11 11:57:08 -03:00
Belen Curcio 917be12667 Merge branch 'master' of github.com:coralproject/talk into plugin_examples 2017-04-11 11:55:12 -03:00
Kim Gardner b2ee1f0c57 Merge branch 'master' into fix-respect-button-bugs 2017-04-11 10:48:41 -04:00
Belen Curcio e888161fe4 off topic plugins, and examples 2017-04-11 11:23:23 -03:00
Wyatt Johnson f648d923a3 Merge branch 'master' into metadata 2017-04-10 13:38:24 -06:00
Wyatt Johnson 8e33d25a28 Some bug fixes + validation updates 2017-04-10 12:12:07 -06:00
Chi Vinh Le b4e8648177 Adjust fetchMore for apollo 1.0 2017-04-10 21:16:07 +07:00
Kim Gardner bcf31cd19a Merge pull request #474 from coralproject/losowsky-patch-1
Took the Trello board out of the doc
2017-04-10 07:44:03 -04:00
Andrew Losowsky 63850acf73 Took the Trello board out of the doc
We haven't been using that. We use google doc instead, which isn't public because it contains a lot of WaPo notes.
2017-04-07 19:25:49 -04:00
Wyatt Johnson 735aa6fbd3 Added new Metadata Service and models 2017-04-07 16:40:30 -06:00
David Erwin 0a0f82cd5a Merge pull request #464 from coralproject/optional-google-fonts
do not load google fonts in embed
2017-04-07 13:50:39 -04:00
Belen Curcio aef0f433c9 Slots and off topic plugin 2017-04-07 14:12:51 -03:00
Belen Curcio c6189e8b3a Slots and off topic plugin 2017-04-07 14:05:23 -03:00
Riley Davis ca04db80dc Merge branch 'master' into optional-google-fonts 2017-04-07 10:42:20 -06:00
Kim Gardner 029b00033e Merge pull request #473 from coralproject/import-fix
Fixed support for imports
2017-04-07 12:27:49 -04:00
Wyatt Johnson a4d631546a Fixed support for imports 2017-04-07 09:37:56 -06:00
Chi Vinh Le dfe8e88faa Fix quick mutations and multiple respects 2017-04-07 21:40:36 +07:00
Belén Curcio 79d9e24cb6 Merge pull request #455 from coralproject/plugin
Plugin Architecture and Respect Button Plugin
2017-04-06 22:12:06 -03:00
Wyatt Johnson 5a4441e309 Adjusted loaders, webpack 2017-04-06 17:42:50 -06:00
Belen Curcio 5245dfaef9 Rolling back since its not needed 2017-04-06 19:44:26 -03:00
Belen Curcio 8b9e69e8a6 Merge branch 'plugin' of github.com:coralproject/talk into plugin 2017-04-06 19:06:34 -03:00
Belen Curcio 16f15c5faf Adding todo 2017-04-06 19:05:46 -03:00
Wyatt Johnson 4377a48e03 Fixes to validation 2017-04-06 16:02:31 -06:00
Belen Curcio 3bd3bca09a Ádding proptypes to RespectButton 2017-04-06 19:02:02 -03:00
Belen Curcio a79a5128c9 plugins default json 2017-04-06 18:57:11 -03:00
Belen Curcio 27408e1fa1 Linting and Patch 2017-04-06 18:49:12 -03:00
Belen Curcio 5e2d64709c Linting and Patch 2017-04-06 18:46:49 -03:00
Belen Curcio f779a55096 Merge branch 'master' of github.com:coralproject/talk into plugin 2017-04-06 18:33:17 -03:00
Belen Curcio a2df5df58c Merge branch 'plugin' of github.com:coralproject/talk into plugin 2017-04-06 18:30:51 -03:00
Belen Curcio e1ebba5b1c Ádding PropTypes to Slot 2017-04-06 18:26:40 -03:00
Belen Curcio ad8c6cc140 Refator plugin loaders 2017-04-06 18:26:24 -03:00
Belen Curcio ae5bcf6900 getActionSummary exp 2017-04-06 18:15:02 -03:00
Wyatt Johnson b2563ca766 Merge pull request #469 from coralproject/plugin-validation
Added joi plugin validation
2017-04-06 15:10:20 -06:00
Riley Davis acb5686c66 Merge branch 'master' into optional-google-fonts 2017-04-06 15:10:05 -06:00
Wyatt Johnson 54518af2a5 Merge branch 'master' into plugin-validation 2017-04-06 15:04:24 -06:00
Kim Gardner 3b24e3c045 Merge pull request #470 from coralproject/plugins-default-json
Added plugins.default.json and TALK_PLUGINS_JSON
2017-04-06 17:04:11 -04:00
Chi Vinh Le 449152d400 When using updateQueries no refetch is needed 2017-04-07 03:43:14 +07:00
Belén Curcio 10836f7918 Merge branch 'master' into plugin 2017-04-06 17:04:38 -03:00
Chi Vinh Le e6ada54d0e Fix optimistic ui 2017-04-07 02:55:54 +07:00
Kim Gardner 770a4e6fe2 Fix typo 2017-04-06 14:38:16 -04:00
Wyatt Johnson 4bc7848e24 comment updates 2017-04-06 12:00:36 -06:00
Wyatt Johnson 943d4cf98a Spelling 2017-04-06 12:00:00 -06:00
Wyatt Johnson a9c88f52ce Optionally load plugins config from environment 2017-04-06 11:58:18 -06:00
Wyatt Johnson fb8b33b2eb Added plugins.default.json 2017-04-06 11:43:18 -06:00
Wyatt Johnson 28ce77a002 Moved validation to one place 2017-04-06 10:56:32 -06:00
Riley Davis 9cab34da73 Merge branch 'master' into optional-google-fonts 2017-04-06 10:40:04 -06:00
Wyatt Johnson 1f367e666b Added joi plugin validation 2017-04-06 10:39:36 -06:00
Chi Vinh Le e92fdc97bb Refetch Me after facebook login 2017-04-06 22:49:20 +07:00
David Erwin e70cfdfd00 Merge pull request #459 from coralproject/support-lazy-assets
Added support for a plugin which disables lazy assets
2017-04-06 10:54:32 -04:00
David Erwin 58f296ef88 Merge branch 'support-lazy-assets' of https://github.com/coralproject/talk into support-lazy-assets 2017-04-06 10:49:30 -04:00
David Erwin 79d5c28d00 Pass asset_id in title route 2017-04-06 10:47:56 -04:00
Chi Vinh Le 7c2b16521a Merge branch 'master' into plugin
Conflicts:
	.eslintignore
	.gitignore
	graph/resolvers/index.js
	plugins.json
2017-04-06 21:22:40 +07:00
Wyatt Johnson 26fff5b950 Merge branch 'master' into support-lazy-assets 2017-04-06 08:21:49 -06:00
David Erwin 2b23a6625f Merge pull request #466 from coralproject/stream-refetch-tweak
only refetch the comments if you're an admin
2017-04-06 10:13:46 -04:00
David Erwin 6f8b375519 Merge branch 'master' into stream-refetch-tweak 2017-04-06 10:03:00 -04:00
David Erwin 73344fa528 Merge pull request #467 from coralproject/fix-collapsing-comments
Fix collapsing comment tree
2017-04-06 10:02:29 -04:00
David Erwin 6117dd3358 Merge branch 'master' into fix-collapsing-comments 2017-04-06 09:57:35 -04:00
Chi Vinh Le 31cdf964f7 Simple camelCase for slot naming 2017-04-06 20:08:41 +07:00
Chi Vinh Le 025df1918d Move slot config to index 2017-04-06 20:06:38 +07:00
Chi Vinh Le 92146f326f camelCase for slot names 2017-04-06 19:55:24 +07:00
Chi Vinh Le a018a8f9bb Fix lint and git ignores 2017-04-06 19:45:09 +07:00
Chi Vinh Le 5cb9fdccf9 Refactor plugin 2017-04-06 19:26:29 +07:00
Chi Vinh Le b64e46c007 Improve performance of RespectButton 2017-04-06 18:31:02 +07:00
Chi Vinh Le 4d51fb276d Fix collapsing comment tree 2017-04-06 17:51:36 +07:00
Chi Vinh Le be64905426 Fix graph not importing plugin resolvers 2017-04-06 17:45:34 +07:00
Chi Vinh Le f91f2b143f Fix undefined client 2017-04-06 17:17:29 +07:00
Chi Vinh Le d32c2f5177 Fix collapsing comment tree 2017-04-06 16:44:50 +07:00
Wyatt Johnson 990fe34873 Merge branch 'master' into support-lazy-assets 2017-04-05 16:21:16 -06:00
Riley Davis 7b7686bc79 Merge branch 'master' into optional-google-fonts 2017-04-05 16:10:41 -06:00
Riley Davis ca2782ba06 Merge branch 'master' into stream-refetch-tweak 2017-04-05 15:40:48 -06:00
David Erwin bbf0839890 Merge pull request #465 from coralproject/graph-fix
Fixed for resolver plugin syntax
2017-04-05 17:00:16 -04:00
Wyatt Johnson dfcb661c21 Syntax 2017-04-05 14:47:27 -06:00
Riley Davis e4e2ff03b3 only refetch the comments if you're an admin 2017-04-05 14:41:06 -06:00
Wyatt Johnson c5ce22c5e6 Fixed for syntax 2017-04-05 14:36:35 -06:00
Kim Gardner 44aa98eb28 Merge branch 'master' into optional-google-fonts 2017-04-05 16:05:13 -04:00
David Erwin 5dbeb695a4 Merge pull request #463 from coralproject/facebook-auth-plugin
Facebook Auth Plugin
2017-04-05 15:31:53 -04:00
Wyatt Johnson a270aef42e fixed license 2017-04-05 13:07:56 -06:00
Wyatt Johnson 2bbbf2bbf7 Fixed bug in plugin 2017-04-05 12:33:24 -06:00
Chi Vinh Le b0effe8731 Merge with 'master' 2017-04-06 01:26:48 +07:00
Riley Davis b24ee81e34 remove Lato from google fonts 2017-04-05 12:14:51 -06:00
Chi Vinh Le baf5a34001 Fully working redux+apollo powered plugin 2017-04-06 00:58:16 +07:00
Chi Vinh Le 582f799b6c Revert "Merge"
This reverts commit d9835a17d3, reversing
changes made to 18d6846f4e.
2017-04-06 00:53:21 +07:00
Wyatt Johnson 21eb532c6e Added postinstall hook 2017-04-05 11:49:22 -06:00
Chi Vinh Le 9505ffdedd Revert "to Master"
This reverts commit eef64f5f1b.
2017-04-06 00:44:12 +07:00
Wyatt Johnson 8a06118d94 Added coral facebook auth plugin, some circ dep fixes 2017-04-05 11:39:54 -06:00
Belen Curcio eef64f5f1b to Master 2017-04-05 12:12:33 -03:00
Belen Curcio d9835a17d3 Merge 2017-04-05 12:08:07 -03:00
Wyatt Johnson 877372814a Merge branch 'master' into support-lazy-assets 2017-04-05 08:14:31 -06:00
David Erwin 97727118dd Merge pull request #461 from coralproject/report-apollo-errors
Implement apollo reporter middleware
2017-04-05 09:30:57 -04:00
David Erwin 74596b9e08 Merge branch 'master' into report-apollo-errors 2017-04-05 09:24:41 -04:00
David Erwin 9c3450ebad Merge pull request #462 from coralproject/adjust-editorconfig
Adjust .editorconfig
2017-04-05 09:22:57 -04:00
Chi Vinh Le 4aa8b5f234 Adjust .editorconfig 2017-04-05 19:49:36 +07:00
Chi Vinh Le 18d6846f4e Implement apollo reporter middleware 2017-04-05 19:37:10 +07:00
Chi Vinh Le 4a49a910aa Remove plugin love 2017-04-05 19:33:50 +07:00
Chi Vinh Le 18a29bf0df Restructure frontend plugins 2017-04-05 19:33:28 +07:00
Chi Vinh Le b3d1053fd7 Get rid of plugin actions in the framework 2017-04-05 18:57:12 +07:00
Chi Vinh Le f6a5d9f721 Remove context for plugins 2017-04-05 18:46:54 +07:00
Chi Vinh Le 765bee40b4 Update apollo 2017-04-05 18:46:16 +07:00
Chi Vinh Le 528f5738c8 Implement apollo reporter middleware 2017-04-05 11:18:45 +07:00
David Erwin a4ec278125 Merge pull request #458 from coralproject/docker-plugins
Plugin Router/Passport/Deployment
2017-04-04 16:56:02 -04:00
Chi Vinh Le 6e2fd0851e Better graphQL id 2017-04-04 18:30:41 +07:00
Chi Vinh Le bdc3a79b27 Adjust respect button styles 2017-04-04 13:04:58 +07:00
Chi Vinh Le 5609715e77 Support object in plugins.json 2017-04-04 13:03:46 +07:00
Wyatt Johnson 2546902bcf Still grab the url when we have the id 2017-04-03 16:59:31 -06:00
Wyatt Johnson 52948a4165 Added support for a plugin which disables lazy assets 2017-04-03 16:51:14 -06:00
Wyatt Johnson c1dac11a2b Updated docs, hook names 2017-04-03 16:11:34 -06:00
Wyatt Johnson 8d3524dd61 Added support for auth based plugins 2017-04-03 16:06:38 -06:00
Wyatt Johnson c5098a3e91 Added docs 2017-04-03 15:37:31 -06:00
Wyatt Johnson febe264320 Added route support 2017-04-03 15:31:51 -06:00
Wyatt Johnson 368838df98 Spelling 2017-04-03 15:16:05 -06:00
Wyatt Johnson b5280b1221 Added support for plugin relative imports 2017-04-03 13:02:32 -06:00
Wyatt Johnson 9c806cb6e4 Added support for plugin upgrades 2017-04-03 11:58:54 -06:00
Wyatt Johnson 881f79a571 Adjusted error handling 2017-04-03 08:59:28 -06:00
Wyatt Johnson d4c7dac945 Adjusted plugins.json format, added docs 2017-04-03 08:53:40 -06:00
Chi Vinh Le 96dcd5f74a Rename module to getModule 2017-04-03 14:56:53 +07:00
Chi Vinh Le 37230ce7fa Add more docs to plugins loader 2017-04-03 14:48:16 +07:00
Chi Vinh Le 24aba5084b Only bind action creators once 2017-04-03 14:41:12 +07:00
Chi Vinh Le 512405b3f0 Fix module loading 2017-04-03 14:41:11 +07:00
Chi Vinh Le 62c4072631 Lazy load all plugin parts 2017-04-03 14:25:06 +07:00
Chi Vinh Le 73e2b899d8 Use custom plugins loader 2017-04-03 14:19:51 +07:00
Chi Vinh Le e0540a5031 Better comments 2017-04-03 12:11:30 +07:00
Chi Vinh Le e0a3e8a3c1 Merge branch 'plugin' into plugin-kiwi
Conflicts:
	plugins/coral-plugin-respect/index.js
2017-04-03 12:10:07 +07:00
Chi Vinh Le a2f1f8018f Defer loading plugin components 2017-04-03 12:03:28 +07:00
Wyatt Johnson c221dbb4a0 Added credits 2017-03-31 16:45:52 -06:00
Wyatt Johnson 1b8489165b Merge branch 'master' into docker-plugins 2017-03-31 16:41:34 -06:00
Wyatt Johnson f1239b4b19 Added support for local plugin dep install 2017-03-31 16:37:24 -06:00
Wyatt Johnson c4edab9bbd cleaned up cli 2017-03-31 16:09:56 -06:00
Wyatt Johnson 0108673566 Added remote plugin reconciliation 2017-03-31 15:54:39 -06:00
Chi Vinh Le a5e463ea0c Fix outdated comment 2017-04-01 00:54:35 +07:00
Chi Vinh Le cc2af70962 Rename getSlotComponents to getSlotElements 2017-04-01 00:52:00 +07:00
Chi Vinh Le 5a5f1bc508 Refractor plugins helper 2017-04-01 00:48:59 +07:00
Chi Vinh Le 56a1503e3a Read plugin typeDefs from graphql file 2017-04-01 00:48:17 +07:00
Belen Curcio c4decbd844 Adding eslinrc 2017-03-31 14:31:44 -03:00
Belen Curcio 0a77fe92bd Adding translations and fully working respect button and styles 2017-03-31 14:23:03 -03:00
Wyatt Johnson 1da5c570bf Fixed syntax error 2017-03-31 09:41:18 -06:00
Belen Curcio 004e8d5fe9 Using wrapResponse 2017-03-31 12:37:11 -03:00
Wyatt Johnson e5da8354d6 Added frozen lockfile support 2017-03-31 09:34:38 -06:00
Belen Curcio 173e1c16ea Plugin now support statefull components 2017-03-30 22:51:16 -03:00
Belen Curcio e1c4b69038 Plugin now support statefull components 2017-03-30 22:10:24 -03:00
Belen Curcio 1ee49f76f6 Styles, utils and state 2017-03-30 20:08:53 -03:00
Belen Curcio 3d245c2029 Working respect count 2017-03-30 19:41:40 -03:00
Wyatt Johnson 78d072a04a Fixed server plugi 2017-03-30 16:28:06 -06:00
Belen Curcio cd08987502 postRespect 2017-03-30 19:17:22 -03:00
Belen Curcio b7e00f1da4 Merge branch 'plugin' of github.com:coralproject/talk into plugin 2017-03-30 19:05:35 -03:00
Wyatt Johnson 492d919053 Added babelrc to plugin 2017-03-30 15:59:49 -06:00
Wyatt Johnson 5fec7c3a11 Added support for resolveType post hooks 2017-03-30 15:55:41 -06:00
Belen Curcio 2d99206b51 Plugin api, state, actions, context 2017-03-30 18:25:25 -03:00
Wyatt Johnson da4eceafe9 Fix for deploy script 2017-03-30 14:19:42 -06:00
Belen Curcio 1bb3ed2399 importer renamed, and properties changed 2017-03-30 14:52:17 -03:00
Wyatt Johnson 126387becc Adjusted docker build script 2017-03-30 11:37:18 -06:00
Belen Curcio 79cbdaf92b Merge branch 'plugin' of github.com:coralproject/talk into plugin 2017-03-30 13:33:00 -03:00
Belén Curcio 5fe96cf47c Merge branch 'master' into plugin 2017-03-30 13:22:29 -03:00
Belen Curcio 52b6ce06e6 Linting 2017-03-30 13:07:32 -03:00
Belen Curcio a7def6fc2b Adding the state of plugins to the main 2017-03-30 12:57:05 -03:00
Belen Curcio 7efbdc8b59 Plugins can trigger own actions with core reducers 2017-03-30 12:40:04 -03:00
Belen Curcio 679962168c Plugins can trigger own actions with core reducers 2017-03-30 12:24:19 -03:00
Belen Curcio 17a8f48b4f Plugins have access to ParentComponent Properties 2017-03-30 11:15:20 -03:00
Belen Curcio ab6f455ec7 Plugins: Reducers and Actions - Working 2017-03-30 11:10:11 -03:00
Belen Curcio 659c63e5cb Adds graph functionality for plugins 2017-03-29 22:14:56 -03:00
Belen Curcio 9df334866d Adds graph functionality for plugins 2017-03-29 19:11:43 -03:00
Belen Curcio 4ca2a48bc9 typeDefs for respect added 2017-03-29 17:40:29 -03:00
Belen Curcio 635df54a2c Extended context 2017-03-29 17:21:31 -03:00
Belen Curcio cfb28c4c2a Merge 2017-03-29 16:42:59 -03:00
Wyatt Johnson 0dc105405c Added new dockerfile for plugin development 2017-03-29 13:37:36 -06:00
Belen Curcio 5dc9811e06 Stable Version with reducers and actions 2017-03-29 16:24:09 -03:00
Belen Curcio 6bb9d23fdc Stable Version with reducers and actions 2017-03-29 14:46:45 -03:00
Belen Curcio 520392829b Stable Version with reducers and actions 2017-03-29 14:45:59 -03:00
Belen Curcio 2513ef2df5 add plugins 2017-03-28 15:56:24 -03:00
Belen Curcio b76fa94354 Stable version 2017-03-28 14:21:06 -03:00
Belen Curcio 1a115e4f2c Stable version 2017-03-28 14:20:12 -03:00
Belen Curcio 697d2f0ec1 Importer, Slots and suport for multiple plugins and props! 2017-03-28 09:58:33 -03:00
Belen Curcio e85ebba21b Working plugin importer 2017-03-27 13:02:57 -03:00
Belen Curcio 3be9b13691 First draft of plugins working :) 2017-03-23 16:59:11 -03:00
Belen Curcio 2e80d2a36e fixing and separating broken tests /client /server 2017-03-23 12:27:30 -03:00
Belen Curcio 341a7320f9 Adding dynamic imports 2017-03-23 11:48:32 -03:00
193 changed files with 4327 additions and 995 deletions
+15
View File
@@ -1,3 +1,18 @@
# excluded because we'll likely need to rebuild this.
node_modules
# scripts are used during development and testing, not
# production.
scripts
# documentation should not be visable in production.
docs
# static assets are rebuild in the docker container.
dist
# tests are not run in the docker container.
test
# we won't use the .git folder in production.
.git
+1 -4
View File
@@ -2,14 +2,11 @@ root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,.*rc,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+4
View File
@@ -1,3 +1,7 @@
dist
client/lib
**/*.html
plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-respect
node_modules
+16 -13
View File
@@ -1,20 +1,23 @@
node_modules
npm-debug.log*
dist
!dist/coral-admin
dist/coral-admin/bundle.js
test/e2e/reports
.DS_Store
*.iml
*.swp
npm-debug.log*
dump.rdb
.env
*.cfg
.idea/
coverage/
.tags
.tags1
# remove plugin folders
plugins
.idea/
*.swp
test/e2e/reports
coverage/
plugins.json
plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
**/node_modules/*
+1 -1
View File
@@ -1,5 +1,5 @@
{
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*"],
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql"
}
+3 -2
View File
@@ -10,10 +10,11 @@ By contributing to this project you agree to the [Code of Conduct](https://coral
You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625.
You can view product ideas and our longer term roadmap here https://trello.com/b/ILND751a/talk.
If you'd like to see our longer term roadmap, [get in touch.](https://coralproject.net/contact.html)
## Contribute to the documentation
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
We are looking for _documentarians_ to:
@@ -55,7 +56,7 @@ Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early
To get an idea of where the Coral Team is going, see:
* our [product/design Trello board](https://trello.com/b/ILND751a/talk),
* our [Slack channel](https://coralprojectslackin.herokuapp.com/), where you can talk directly with the Team and other community members,
* our [current stories](https://www.pivotaltracker.com/n/projects/1863625), and
* our [issues](https://github.com/coralproject/talk/issues).
+11 -6
View File
@@ -1,20 +1,25 @@
FROM node:7
FROM node:7.8
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Setup the environment
ENV NODE_ENV production
ENV PATH /usr/src/app/bin:$PATH
ENV TALK_PORT 5000
EXPOSE 5000
# Install app dependencies
COPY package.json yarn.lock /usr/src/app/
RUN yarn install --production
# Bundle app source
COPY . /usr/src/app
# Install app dependencies and build static assets.
RUN yarn install --frozen-lockfile && \
cli plugins reconcile && \
yarn build && \
yarn install --production && \
yarn cache clean
# Ensure the runtime of the container is in production mode.
ENV NODE_ENV production
CMD ["yarn", "start"]
+14
View File
@@ -0,0 +1,14 @@
FROM coralproject/talk:latest
# Bundle app source
ONBUILD COPY . /usr/src/app
# At this stage, we need to install the development dependancies again because
# we need to have webpack available. We then build the new dependancies and
# clear out the development dependancies again. After this we of course need to
# clear out the yarn cache, this saves quite a lot of size.
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
NODE_ENV=production cli plugins reconcile && \
NODE_ENV=production yarn build && \
NODE_ENV=production yarn install --production --force && \
yarn cache clean
+218 -37
View File
@@ -1,37 +1,204 @@
## Contents
- [Installation](#installation) - install the application on a machine
- [Via Docker](#installation-from-docker)
- [Via Source](#installation-from-source)
- [Setup](#setup) - setup the application for first use
- [Usage](#usage) - connect the application to a website
# Installation
## Requirements
### System
- Any flavour of Linux, OSX or Windows
- 1GB memory (minimum)
- 5GB storage (minimum)
- [MongoDB](https://www.mongodb.com/) v3.4 or later
- [Redis](https://redis.io/) v3.2 or later
- SSL Certificate
- This application assumes that you will be serving this application in a
production environment, and therefore requires that you serve it behind a
webserver with a valid SSL certificate. This is chosen in order to secure
user's sessions.
## Installation From Docker
We currently support packaging the Talk application via Docker, which automates
the dependency install and asset build process. This is the recommended way to
deploy the application when used in production.
Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Docker Hub.
Images are tagged using the following notation:
- `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting
new features occasionally and all the bug fixes, this is the tag for you.
- `x.y` (where `y` is the minor version number): any patch updates will be
included with this tag. If you like getting fixes and having features change
only when you want, this is the tag for you. **(recommended)**
- `x.y.z` (where `z` is the patch version): this tag never gets updated, and
essentially freezes your version, this should only be used when you are either
extending Talk or are sure of a specific version you want to freeze.
We provide tags with `*-onbuild` that can be used for easy plugin integration and
acts as a customization endpoint. Instructions are provided in the `PLUGINS.md`
document as to how to use it.
### Requirements
There are some runtime requirements for running Talk for Docker:
- [Docker](https://www.docker.com/) v1.13.0 or later
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
_Please be sure to check the versions of these requirements. Incorrect versions
of these may lead to unexpected errors!_
### Installing
An example docker-compose.yml:
```yaml
version: '2'
services:
talk:
image: coralproject/talk:1.5
restart: always
ports:
- "5000:5000"
depends_on:
- mongo
- redis
environment:
- TALK_MONGO_URL=mongodb://mongo/talk
- TALK_REDIS_URL=redis://redis
mongo:
image: mongo:3.2
restart: always
volumes:
- mongo:/data/db
redis:
image: redis:3.2
restart: always
volumes:
- redis:/data
volumes:
mongo:
external: false
redis:
external: false
```
At this stage, you should refer to the `README.md` for configuration variables
that are specific to your installation. Some pre-defined fields have been filled
in the above example which are consistent with Docker Compose naming conventions
for [Docker Links](https://docs.docker.com/compose/networking/#links).
### Scaling
If you are interested in splitting apart services, you can simply adjust the
command being executed in the container to optimize for your use case. An
example would be if you wanted to run the API server and the job processor
on different machines. You can achieve this easily with docker compose:
```yaml
version: '2'
services:
talk-api:
image: coralproject/talk:1.5
command: cli serve
restart: always
ports:
- "5000:5000"
depends_on:
- mongo
- redis
environment:
- TALK_MONGO_URL=mongodb://mongo/talk
- TALK_REDIS_URL=redis://redis
talk-jobs:
image: coralproject/talk:1.5
command: cli jobs process
restart: always
ports:
- "5001:5000"
depends_on:
- mongo
- redis
environment:
- TALK_MONGO_URL=mongodb://mongo/talk
- TALK_REDIS_URL=redis://redis
mongo:
image: mongo:3.2
restart: always
volumes:
- mongo:/data/db
redis:
image: redis:3.2
restart: always
volumes:
- redis:/data
volumes:
mongo:
external: false
redis:
external: false
```
Note that the only difference is in the `command` key. From this, you are able
to discretely control which modules are running in order to have the maximum
flexibility when managing your application.
### Running
If you're using docker compose:
```bash
# Start the services using compose
docker-compose up -d
```
If you're using plain docker:
```bash
docker run -d -P coralproject/talk:latest
```
## Installation From Source
This provides information on how to setup the application from source. Note that
this is not recommended for production deploys, but will work for development
and testing purposes.
### Requirements
There are some runtime requirements for running Talk from source:
- [Node](https://nodejs.org/) v7 or later
- [MongoDB](https://www.mongodb.com/) v3.4 or later
- [Redis](https://redis.io/) v3.2 or later
- [Yarn](https://yarnpkg.com/) v0.19.1 or later
- [Node](https://nodejs.org/) ~7.8
- [Yarn](https://yarnpkg.com/) ^0.22.0
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
_Please be sure to check the versions of these requirements. Incorrect versions
of these may lead to unexpected errors!_
### Installing
#### Download
It is highly recommended that you download a released version as the code
available in `master` may not be stable. You can download the latest release
from the [releases page](https://github.com/coralproject/talk/releases).
You can also clone the git repository via:
```bash
# Download the tarball containing the repository
curl -L https://github.com/coralproject/talk/tarball/master -o coralproject-talk.tar.gz
git clone https://github.com/coralproject/talk.git
```
# Untar that file and change to that directory
tar xpf coralproject-talk.tar.gz
mv coralproject-talk-* coralproject-talk
cd coralproject-talk
#### Building
We now have to install the dependencies and build the static assets.
```bash
# Install package dependancies
yarn
@@ -39,6 +206,17 @@ yarn
yarn build
```
After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin
docs) file, you can re-run the following to install their dependencies:
```bash
# Reconcile plugins
./bin/cli plugins reconcile
# Build static files
yarn build
```
### Running
Refer to the `README.md` file for required configuration variables to add to the
@@ -50,45 +228,48 @@ You can start the server after configuring the server using the command:
yarn start
```
This will setup the server to serve everything on a single node.js process and
is designed to be used in production.
You can see other scripts we've made available by consulting the `package.json`
file under the `scripts` key including:
- `yarn test` run unit tests
- `yarn e2e` run end to end tests
- `yarn build-watch` watch for changes to client files and build static assets
- `yarn dev-start` watch for changes to server files and reload the server
- `yarn dev-start` watch for changes to server files and reload the server while
also sourcing a `.env` file in your local directory for configuration
## Installation From Docker Hub
# Setup
### Requirements
Once you've installed Talk (either via Docker or source), you still need to
setup the application. If you are unfamiliar with any terminoligy used in the
setup process, refer to the `TERMINOLOGY.md` document.
There are some runtime requirements for running Talk for Docker:
## Via Web
- [MongoDB](https://www.mongodb.com/) v3.2 or later
- [Redis](https://redis.io/) v3.2 or later
- [Docker](https://www.docker.com/) v1.13.0 or later
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
If you want to perform your setup via the web, you can navigate to your
installation of Talk at the path `/admin/install`. There you will be asked a
series of questions for your installation.
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
## Via CLI
### Installing
If you want to perform your setup through the terminal, you can simply run:
```bash
# Create a directory for talk
mkdir coralproject-talk
cd coralproject-talk
# Download the docker-compose.yml file from the repository
curl -LO https://raw.githubusercontent.com/coralproject/talk/master/docker-compose.yml
cli setup
```
At this stage, you should refer to the `README.md` file for required
configuration variables to add to the environment key for the `talk` service
listed in the `docker-compose.yml` file.
And follow the instructions to perform initial setup and create your first user
account.
### Running
```bash
# Start the services using compose
docker-compose up -d
```
# Usage
After setup is complete, you can then refer to the `/admin/configure` path to
get the embed code that you can copy/paste onto your blog or website in order to
start using Talk.
_In order for the embed to work correctly, you will need to whitelist the domain
that is allowed to embed your site on the `/admin/configure` page, failure to do
so will result in the comment stream not loading._
+176 -5
View File
@@ -3,13 +3,20 @@
Plugins for Talk can take various forms, currently we are only supporting server
side plugins.
## Plugin Registration: `plugins.json`
## Plugin Registration
All plugins must be registered in the root file `plugins.json`.
The parsing order for the plugin registration is as follows:
The format for this file is thus:
- `TALK_PLUGINS_JSON` environment variable
- `plugins.json` file
- `plugins.default.json` file
```js
If you need to "disable all plugins", you can simply provide `{}` as the
contents of `process.env.TALK_PLUGINS_JSON` or the `plugins.json`.
The format for this is thus:
```json
{
"server": [
"people"
@@ -18,12 +25,94 @@ The format for this file is thus:
```
Where we have a `server` key with an array of plugins that match the folder
name in the `plugins/` folder. For example, the above `plugins.json` would
name in the `plugins/` folder. For example, the above config would
require a plugin from `plugins/people`, which must provide a `index.js` file
that returns an object that matches the Plugin Specification.
If the package is external (available on NPM) you can specify the string for
the version by using an object instead, for example:
```json
{
"server": [
{"people": "^1.2.0"}
]
}
```
External plugins can be resolved by running:
```bash
./bin/cli plugins reconcile
```
This achieves two things:
1. It will traverse into local plugin folders and install their dependencies.
_Note that if the plugin is already installed and available in the node_modules folder, it will not be
fetched again unless there is a version mismatch._ This will result in the
project `package.json` and `yarn.lock` files to be modified, this is normal as
this ensures that repeated deployments (with the same config) will have the
same config, these changes should not be committed to source control.
2. It will seek out dependencies that are listed in the object notation and try
to install them from npm.
## Plugin Dependencies
You may also include additional external dependencies in your local packages by
specifying a `package.json` at your plugin root which will result in a
`node_modules` folder being generated at the plugin root with your specific
dependencies.
## Deployment Solutions
Plugins can be deployed with a production instance of Talk.
### Source
Source deployments can just modify the `plugins.json` file and include any
local plugins into the `plugins/` directory. After including the config, you
need to reconcile the plugins and build the static assets:
```bash
# get plugin dependancies and remote plugins
./bin/cli plugins reconcile
# build staic assets (including enabled client side plugins)
yarn build
```
Then the application can be started as is.
### Docker
If you deploy using Docker, you can extend from the `*-onbuild` image, an
example `Dockerfile` for your project could be:
```Dockerfile
FROM coralproject/talk:latest-onbuild
```
Where the directory for your instance would contain a `plugins.json` file
describing the plugin requirements and a `plugins` directory containing any
other local plugins that should be included.
Onbuild triggers will execute when the image is building with your custom
configuration and will ensure that the image is ready to use by building all
assets inside the image as well.
## Server Plugins
### API
You can access any API available inside the talk directory in a plugin by simply
importing the file relative to the talk project root. An example would be if you
wanted to import the `MetadataService`, you would simply write:
```javascript
const MetadataService = require('services/metadata');
```
### Specification
Each plugin should export a single object with all hooks available on it.
@@ -170,6 +259,88 @@ If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
#### Field: `router`
```js
(router) => {
router.get('/api/v1/people', (req, res) => {
res.json({people: [{name: 'Bob'}]});
});
}
```
The Router hook allows you to create a function that accepts the base express
router where you can mount any amount of middleware/routes to do any form of
action needed by external applications. We also provide the authorization
middleware via:
```js
const authorization = require('middleware/authorization');
module.exports = {
router(router) {
router.get('/api/v1/people', authorization.needed('ADMIN'), (req, res) => {
res.json({people: [{name: 'SECRET PEOPLE'}]});
});
}
}
```
#### Field: `passport`
```js
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
module.exports = {
passport(passport) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err);
}
return ValidateUserLogin(profile, user, done);
}));
},
router(router) {
// Note that we have to import the passport instance here, it is
// instantiated after all the strategies have been mounted.
const {passport} = require('services/passport');
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
});
}
};
```
This is a full example including the routes hook to add the required components
to the application router to support a different auth strategy.
### Full Example
Contents of `plugins.json`:
+1 -6
View File
@@ -28,12 +28,6 @@ The Talk application looks for the following configuration values either as envi
secure cookies.
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
available in the format: `<scheme>://<host>` without the path.
- `TALK_FACEBOOK_APP_ID` (*required for login via fb*) - the Facebook app id for your Facebook
Login enabled app.
- `TALK_FACEBOOK_APP_SECRET` (*required for login via fb*) - the Facebook app secret for your
Facebook Login enabled app.
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
SMTP provider.
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
@@ -43,6 +37,7 @@ Facebook Login enabled app.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
- `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
- `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
- `TALK_PLUGINS_JSON` (_optional_) - used to specify the plugin config via the environment
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
+13 -1
View File
@@ -3,7 +3,8 @@ const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const helmet = require('helmet');
const passport = require('./services/passport');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const session = require('express-session');
const enabled = require('debug').enabled;
const RedisStore = require('connect-redis')(session);
@@ -75,6 +76,17 @@ app.use(session(session_opts));
// PASSPORT MIDDLEWARE
//==============================================================================
const passportDebug = require('debug')('talk:passport');
// Install the passport plugins.
plugins.get('server', 'passport').forEach((plugin) => {
passportDebug(`added plugin '${plugin.plugin.name}'`);
// Pass the passport.js instance to the plugin to allow it to inject it's
// functionality.
plugin.passport(passport);
});
// Setup the PassportJS Middleware.
app.use(passport.initialize());
app.use(passport.session());
+1
View File
@@ -13,6 +13,7 @@ program
.command('setup', 'setup the application')
.command('jobs', 'work with the job queues')
.command('users', 'work with the application auth')
.command('plugins', 'provides utilities for interacting with the plugin system')
.parse(process.argv);
/**
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
// Interface heavily inspired by the yarn package manager:
// https://yarnpkg.com/
const program = require('./commander');
// Make things colorful!
require('colors');
const emoji = require('node-emoji');
const dir = process.cwd();
const fs = require('fs');
const path = require('path');
const spawn = require('cross-spawn');
const semver = require('semver');
const resolve = require('resolve');
const {plugins, itteratePlugins, isInternal} = require('../plugins');
function existsInNodeModules(name) {
try {
resolve.sync(name, {basedir: dir});
return true;
} catch (e) {
return false;
}
}
function versionMatch(name, version) {
try {
let matched = false;
resolve.sync(name, {
basedir: dir,
packageFilter: (pkg) => {
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
matched = true;
}
return pkg;
}
});
return matched;
} catch (e) {
return false;
}
}
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
function reconcilePackages({quiet = false, upgradeRemote = false}) {
const fetchable = [];
const local = [];
const upgradable = [];
if (!quiet) {
console.log();
console.log(' +local (l) packages in your project');
console.log(' +external (e) packages are external');
console.log(' +outofdate (oe) packages are external but are out of date');
console.log(' +missing (m) packages are not found');
console.log();
}
for (let i in plugins) {
let section = itteratePlugins(plugins[i]);
for (let j in section) {
let {name, version} = section[j];
let namespaced = name.charAt(0) === '@';
let dep = name.split('/')
.slice(0, namespaced ? 2 : 1)
.join('/');
// Ignore relative modules, which aren't installed by NPM
if (!dep.match(EXTERNAL) && !namespaced) {
return;
}
if (isInternal(dep)) {
if (!quiet) {
console.log(` l ${name}`);
}
local.push({name, version});
continue;
}
if (!existsInNodeModules(dep)) {
if (!quiet) {
console.log(` m ${name}`);
}
fetchable.push({name, version});
} else if (!versionMatch(dep, version)) {
// A plugin was found, yet the current version does not match the
// current version installed. We should warn if upgradeRemote is
// not enabled that it is currently not supported.
if (!upgradeRemote) {
if (!quiet) {
console.warn(` oe ${name} (package upgrade may be required)`.bgRed);
}
continue;
}
console.log(` oe ${name} (package upgrade may be required)`);
upgradable.push({name, version});
} else {
if (!quiet) {
console.log(` e ${name}`);
}
if (upgradeRemote) {
upgradable.push({name, version});
}
}
}
}
if (!quiet) {
console.log();
}
return {local, fetchable, upgradable};
}
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow);
const {fetchable, upgradable} = reconcilePackages({upgradeRemote});
console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
if (fetchable.length > 0) {
console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
if (!dryRun) {
let args = [
'add',
'--ignore-scripts',
...fetchable.map(({name, version}) => `${name}@${version}`)
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
}
console.log(output.stdout.toString());
}
}
if (upgradable.length > 0) {
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
if (!dryRun) {
let args = [
'upgrade',
...upgradable.map(({name, version}) => `${name}@${version}`)
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
}
console.log(output.stdout.toString());
}
}
return {upgradable, fetchable};
}
async function reconcileLocalPlugins({skipRemote, dryRun}) {
console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow);
const {local} = reconcilePackages({quiet: true});
for (let i in local) {
let {name} = local[i];
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
continue;
}
let wd = path.join(dir, 'plugins', name);
console.log(`$ cd ${wd.cyan} && yarn`);
if (!dryRun) {
let args = [];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit'],
cwd: wd
});
if (output.status) {
throw new Error('Could not install local plugin dependencies, errors occured during install');
}
console.log(output.stdout.toString());
}
}
}
// This traverses the local plugins and installs any dependencies listed there,
// this only is really needed for plugins that are installed via docker because
// core plugins will have their dependencies already included in core.
async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) {
let startTime = new Date();
// We don't need to do anything if we skip everything....
if (skipLocal && skipRemote) {
return;
}
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({skipRemote, dryRun});
}
// Locate any external plugins and install them.
if (!skipRemote) {
let results = [];
try {
results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote});
} catch (e) {
throw e;
}
let status;
if (dryRun) {
status = '[dry-run] success'.green;
} else {
status = 'success'.green;
}
let message;
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
message = 'Already up-to-date.';
} else if (results.upgradable.length === 0) {
message = `Fetched ${results.fetchable.length} new plugins.`;
} else if (results.fetchable.length === 0) {
message = `Upgraded ${results.upgradable.length} new plugins.`;
} else {
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`;
}
console.log(`\n${status} ${message}`);
}
let endTime = new Date();
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
console.log(`✨ Done in ${totalTime}s.`);
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('list')
.description('')
.action(reconcilePackages);
program
.command('reconcile')
.description('reconciles local plugin dependencies and downloads external plugins')
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
.option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation')
.option('--skip-local', 'skips the local dependancy reconciliation')
.option('--skip-remote', 'skips the remote plugin reconciliation')
.action(reconcilePluginDeps);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
}
+4 -3
View File
@@ -1,6 +1,6 @@
machine:
node:
version: 7.6
version: 7.9
services:
- docker
- redis
@@ -20,6 +20,7 @@ dependencies:
# - sudo service mongod restart
# Install node dependencies.
- yarn --version
- yarn
cache_directories:
- ~/.cache/yarn
@@ -47,9 +48,9 @@ deployment:
release:
tag: /v[0-9]+(\.[0-9]+)*/
commands:
- bash ./scripts/deploy.sh
- bash ./scripts/docker.sh deploy
latest:
branch: master
commands:
- bash ./scripts/deploy.sh
- bash ./scripts/docker.sh deploy
-23
View File
@@ -1,23 +0,0 @@
# Coral Admin
This app handles moderation for Talk (and maybe more later on)
## Installation
$ yarn install
$ cp config.sample.json config.json
Then change `config.json` to adjust it to your project
## Building for production
$ yarn build
The public folder has everything you need for deployment. You can just copy that folder to your favorite static web server
## Development
$ yarn start
A development server will be running at http://localhost:4132
@@ -28,11 +28,12 @@ class CommunityContainer extends Component {
super(props);
this.state = {
searchValue: ''
searchValue: '',
timer: null
};
this.onKeyDownHandler = this.onKeyDownHandler.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
this.onHeaderClickHandler = this.onHeaderClickHandler.bind(this);
this.onNewPageHandler = this.onNewPageHandler.bind(this);
}
@@ -48,9 +49,16 @@ class CommunityContainer extends Component {
}
}
onChangeHandler(e) {
this.setState({
searchValue: e.target.value
onSearchChange(e) {
const value = e.target.value;
this.setState((prevState) => {
prevState.searchValue = value;
clearTimeout(prevState.timer);
const fetchAccounts = this.props.fetchAccounts;
prevState.timer = setTimeout(() => {
fetchAccounts({value});
}, 350);
return prevState;
});
}
@@ -86,11 +94,11 @@ class CommunityContainer extends Component {
isFetching={community.isFetchingPeople}
commenters={community.accounts}
searchValue={searchValue}
onSearchChange={this.onSearchChange}
error={community.errorPeople}
totalPages={community.totalPagesPeople}
page={community.pagePeople}
onKeyDown={this.onKeyDownHandler}
onChange={this.onChangeHandler}
onHeaderClickHandler={this.onHeaderClickHandler}
onNewPageHandler={this.onNewPageHandler}
/>
@@ -5,7 +5,7 @@ import translations from 'coral-admin/src/translations.json';
import styles from './Community.css';
import Table from './Table';
import Loading from './Loading';
import {Pager} from 'coral-ui';
import {Pager, Icon} from 'coral-ui';
import EmptyCard from '../../components/EmptyCard';
const lang = new I18n(translations);
@@ -29,28 +29,22 @@ const tableHeaders = [
}
];
const People = ({isFetching, commenters, ...props}) => {
const People = ({isFetching, commenters, searchValue, onSearchChange, ...props}) => {
const hasResults = !isFetching && !!commenters.length;
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
<form action="">
<div className={`${styles.searchBox}`}>
<label htmlFor="commenters-search">
<i className="material-icons">search</i>
</label>
<div className="">
<input
id="commenters-search"
className={`${styles.searchInput}`}
type="text"
value={props.searchValue}
onKeyDown={props.onKeyDownHandler}
onChange={props.onChangeHandler}
/>
</div>
</div>
</form>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
<input
id="commenters-search"
type="text"
className={styles.searchBoxInput}
value={searchValue}
onChange={onSearchChange}
placeholder={lang.t('streams.search')}
/>
</div>
</div>
<div className={styles.mainContent}>
{ isFetching && <Loading /> }
@@ -10,7 +10,7 @@ const FlagWidget = ({assets}) => {
return (
<div className={styles.widget}>
<h2 className={styles.heading}>Articles with the most flags</h2>
<h2 className={styles.heading}>{lang.t('dashboard.most_flags')}</h2>
<div className={styles.widgetHead}>
<p>{lang.t('streams.article')}</p>
<p>{lang.t('dashboard.flags')}</p>
@@ -116,7 +116,7 @@ class ModerationContainer extends Component {
let asset;
if (data.loading) {
if (!('premodCount' in data)) {
return <div><Spinner/></div>;
}
@@ -10,6 +10,7 @@ import FlagBox from './FlagBox';
import CommentType from './CommentType';
import ActionButton from 'coral-admin/src/components/ActionButton';
import BanUserButton from 'coral-admin/src/components/BanUserButton';
import {getActionSummary} from 'coral-framework/utils';
const linkify = new Linkify();
@@ -17,25 +18,27 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
const Comment = ({actions = [], ...props}) => {
const links = linkify.getMatches(props.comment.body);
const Comment = ({actions = [], comment, ...props}) => {
const links = linkify.getMatches(comment.body);
const linkText = links ? links.map(link => link.raw) : [];
const actionSummaries = props.comment.action_summaries;
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction');
return (
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>
{props.comment.user.name}
{comment.user.name}
</span>
<span className={styles.created}>
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
{timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
<BanUserButton user={props.comment.user} onClick={() => props.showBanUserDialog(props.comment.user, props.comment.id, props.comment.status !== 'REJECTED')} />
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
<CommentType type={props.commentType} />
</div>
{props.comment.user.status === 'banned' ?
{comment.user.status === 'banned' ?
<span className={styles.banned}>
<Icon name='error_outline'/>
{lang.t('comment.banned_user')}
@@ -43,16 +46,16 @@ const Comment = ({actions = [], ...props}) => {
: null}
</div>
<div className={styles.moderateArticle}>
Story: {props.comment.asset.title}
Story: {comment.asset.title}
{!props.currentAsset && (
<Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate &rarr;</Link>
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate </Link>
)}
</div>
<div className={styles.itemBody}>
<p className={styles.body}>
<Highlighter
searchWords={[...props.suspectWords, ...props.bannedWords, ...linkText]}
textToHighlight={props.comment.body} />
textToHighlight={comment.body} />
</p>
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
@@ -60,16 +63,20 @@ const Comment = ({actions = [], ...props}) => {
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={props.comment.user}
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
user={comment.user}
acceptComment={() => props.acceptComment({commentId: comment.id})}
rejectComment={() => props.rejectComment({commentId: comment.id})}
/>
)}
</div>
</div>
</div>
</div>
{actionSummaries && <FlagBox actionSummaries={actionSummaries} />}
{
flagActions && flagActions.length
? <FlagBox actions={flagActions} actionSummaries={flagActionSummaries} />
: null
}
</li>
);
};
@@ -83,6 +90,7 @@ Comment.propTypes = {
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
actions: PropTypes.array,
created_at: PropTypes.string.isRequired,
user: PropTypes.shape({
status: PropTypes.string
@@ -53,3 +53,17 @@
font-size: 12px;
}
}
.lessDetail {
display: inline-block;
margin-right: 10px;
}
.subDetail {
font-weight: normal;
color: #888;
span {
color: black;
}
}
@@ -1,6 +1,17 @@
import React, {Component, PropTypes} from 'react';
import {Icon} from 'coral-ui';
import styles from './FlagBox.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
const shortReasons = {
'This comment is offensive': lang.t('modqueue.offensive'),
'This looks like an ad/marketing': lang.t('modqueue.spam/ads'),
'This user is impersonating': lang.t('modqueue.impersonating'),
'I don\'t like this username': lang.t('modqueue.dont-like-username'),
'Other': lang.t('modqueue.other')
};
class FlagBox extends Component {
constructor () {
@@ -16,27 +27,50 @@ class FlagBox extends Component {
}));
}
reasonMap = (reason) => {
const shortReason = shortReasons[reason];
// if the short reason isn't found, just return the long one.
return shortReason ? shortReason : reason;
}
render() {
const {props} = this;
const {actionSummaries, actions} = this.props;
const {showDetail} = this.state;
return (
<div className={styles.flagBox}>
<div className={styles.container}>
<div className={styles.header}>
<Icon name='flag'/><h3>Flags ({props.actionSummaries.length}):</h3>
<Icon name='flag'/><h3>Flags ({actionSummaries.length}):</h3>
<ul>
{props.actionSummaries.map((action, i) =>
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
{actionSummaries.map((action, i) =>
<li key={i} className={styles.lessDetail}>{this.reasonMap(action.reason)} (<strong>{action.count}</strong>)</li>
)}
</ul>
{/* <a onClick={this.toggleDetail} className={styles.moreDetail}>More detail</a>*/}
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? lang.t('modqueue.less-detail') : lang.t('modqueue.more-detail')}</a>
</div>
{this.state.showDetail && (<div className={styles.detail}>
<ul>
{props.actionSummaries.map((action, i) =>
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
)}
</ul>
</div>)}
{showDetail && (
<div className={styles.detail}>
<ul>
{actionSummaries.map((summary, i) => {
const actionList = actions.filter(a => a.reason === summary.reason);
return (
<li key={i}>
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
<ul>
{
actionList.map((action, j) => <li key={`${i}_${j}`} className={styles.subDetail}><span>{action.user.username}</span> {action.message}</li>)
}
</ul>
</li>
);
})}
</ul>
</div>
)}
</div>
</div>
);
@@ -44,7 +78,14 @@ class FlagBox extends Component {
}
FlagBox.propTypes = {
actionSummaries: PropTypes.array.isRequired
actionSummaries: PropTypes.arrayOf(PropTypes.shape({
reason: PropTypes.string,
count: PropTypes.number
})).isRequired,
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired
};
export default FlagBox;
@@ -14,13 +14,13 @@ const ModerationHeader = props => (
<span>{props.asset.title}</span>
<Icon className={styles.settingsButton} name="open_in_new"/>
</a>
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
<Link className="mdl-tabs__tab" to="/admin/stories">Select Stream</Link>
</div>
:
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
<a className="mdl-tabs__tab" />
<a className="mdl-tabs__tab">All Streams</a>
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
<Link className="mdl-tabs__tab" to="/admin/stories">Select Stream</Link>
</div>
}
</div>
@@ -6,7 +6,7 @@ const NotFound = props => (
<div className={`mdl-card mdl-shadow--2dp ${styles.notFound}`}>
<p>
The provided asset id <Link to={`/admin/moderate/${props.assetId}`}>{props.assetId}</Link> does not exist.
<Link className={styles.goToStreams} to="/admin/streams">Go to Streams</Link>
<Link className={styles.goToStreams} to="/admin/stories">Go to Streams</Link>
</p>
</div>
);
@@ -12,4 +12,19 @@ fragment commentView on Comment {
id
title
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
actions {
... on FlagAction {
reason
message
user {
username
}
}
}
}
@@ -55,7 +55,7 @@ export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) =>
statuses,
asset_id
},
updateQuery: (oldData, {fetchMoreResult:{data:{comments}}}) => {
updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
return {
...oldData,
[tab]: [
@@ -15,12 +15,6 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
sort: $sort
}) {
...commentView
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
rejected: comments(query: {
statuses: [REJECTED],
@@ -1,7 +1,9 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
import fragmentMatcher from './fragmentMatcher';
export const client = new ApolloClient({
fragmentMatcher,
addTypename: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
@@ -0,0 +1,33 @@
import {IntrospectionFragmentMatcher} from 'apollo-client';
// TODO this is a short-term fix
// we need to set up something to query the server for the schema before ApolloClient initialization
// https://github.com/apollographql/apollo-client/issues/1555#issuecomment-295834774
const fm = new IntrospectionFragmentMatcher({
introspectionQueryResultData: {
__schema: {
types: [
{
kind: 'INTERFACE',
name: 'Action',
possibleTypes: [
{name: 'FlagAction'},
{name: 'LikeAction'},
{name: 'DontAgreeAction'}
],
},
{
kind: 'INTERFACE',
name: 'ActionSummary',
possibleTypes: [
{name: 'FlagActionSummary'},
{name: 'LikeActionSummary'},
{name: 'DontAgreeActionSummary'}
],
}
],
},
}
});
export default fm;
+29 -5
View File
@@ -51,7 +51,14 @@
"singleview": "Toggle single comment edit view",
"thismenu": "Open this menu",
"emptyqueue": "No more comments to moderate! You're all caught up. Go have some ☕️",
"showshortcuts": "Show Shortcuts"
"showshortcuts": "Show Shortcuts",
"more-detail": "More detail",
"less-detail": "Less detail",
"dont-like-username": "Don't like username",
"impersonating": "Impersonating",
"offensive": "Offensive",
"spam/ads": "Spam/Ads",
"other": "Other"
},
"comment": {
"flagged": "flagged",
@@ -69,6 +76,10 @@
},
"configure": {
"closed-stream-settings": "Closed Stream Message",
"open-stream-configuration": "This comment stream is currently open. By closing this comment stream, no new comments may be submitted and all previous comments will still be displayed.",
"close-stream-configuration": "This comment stream is currently closed. By opening this comment stream, new comments may be submitted and displayed",
"close-stream": "Close Stream",
"open-stream": "Open Stream",
"stream-settings": "Stream Settings",
"moderation-settings": "Moderation Settings",
"tech-settings": "Tech Settings",
@@ -139,7 +150,8 @@
"no_likes": "There have been no likes in the last 5 minutes. All quiet.",
"flags": "Flags",
"no_activity": "There haven't been any comments anywhere in the last five minutes.",
"comment_count": "comments"
"comment_count": "comments",
"most_flags": "Articles with the most flags"
},
"streams": {
"empty_result": "No assets match this search. Maybe try widening your search?",
@@ -216,7 +228,14 @@
"shortcuts": "Atajos de teclado",
"close": "Cerrar",
"emptyqueue": "No se encontro ningún usuario. Están escondidos.",
"showshortcuts": "Mostrar atajos"
"showshortcuts": "Mostrar atajos",
"more-detail": "Mas detalle",
"less-detail": "Menos detalle",
"dont-like-username": "No me gusta ese nombre de usuario",
"impersonating": "Suplantación",
"offensive": "Ofensivo",
"spam/ads": "Spam/Propaganda",
"other": "Otros"
},
"comment": {
"flagged": "marcado",
@@ -231,6 +250,10 @@
},
"configure": {
"closed-stream-settings": "Mensaje a enviar cuando los comentarios están cerrados en el artículo",
"open-stream-configuration": "Este hilo de comentarios esta abierto. Al cerrarlo, ningún nuevo comentario será publicado y todos los comentarios anteriores serán mostrados.",
"close-stream-configuration": "Este hilo de comentario está en este momento cerrado. Al abrirlo, nuevos comentarios serán publicaods y mostrados.",
"close-stream": "Cerrar Comentarios",
"open-stream": "Abrir Comentarios",
"stream-settings": "Configuración de Comentarios",
"moderation-settings": "Configuración de Moderación",
"tech-settings": "Configuración Técnica",
@@ -283,14 +306,15 @@
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
},
"dashbord": {
"dashboard": {
"next-update": "{0} minutos hasta la siguiente actualización.",
"auto-update": "Los datos se actualizan automaticamente cada 5 minutos o cuando recargas.",
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
"no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
"flags": "Marcados",
"no_activity": "No hubo comentarios en los ultimos 5 minutos",
"comment_count": "comentarios"
"comment_count": "comentarios",
"most_flags": "Articulos con más reportes"
},
"streams": {
"empty_result": "No se encuentro articulo con esta busqueda. ¿Tal vez puedas extender la busqueda?",
@@ -1,23 +1,25 @@
import React from 'react';
import {Button} from 'coral-ui';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
const lang = new I18n(translations);
export default ({status, onClick}) => (
status === 'open' ? (
<div className="close-comments-intro-wrapper">
<p>
This comment stream is currently open. By closing this comment stream,
no new comments may be submitted and all previous comments will still
be displayed.
{lang.t('configure.open-stream-configuration')}
</p>
<Button onClick={onClick}>Close Stream</Button>
<Button onClick={onClick}>{lang.t('configure.close-stream')}</Button>
</div>
) : (
<div className="close-comments-intro-wrapper">
<p>
This comment stream is currently closed. By opening this comment stream,
new comments may be submitted and displayed
{lang.t('configure.close-stream-configuration')}
</p>
<Button onClick={onClick}>Open Stream</Button>
<Button onClick={onClick}>{lang.t('configure.open-stream')}</Button>
</div>
)
);
@@ -9,12 +9,12 @@
right: 0;
}
ul {
.wrapper ul {
list-style: none;
padding: 0;
}
ul ul {
.wrapper ul ul {
padding-left: 20px
}
@@ -23,12 +23,12 @@ ul ul {
margin: 12px 12px 12px 0;
}
h4 {
.wrapper h4 {
font-size: 14px;
margin-bottom: 5px;
}
p {
.wrapper p {
max-width: 380px;
}
@@ -7,7 +7,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
const lang = new I18n(translations);
export default ({handleChange, handleApply, changed, updateQuestionBoxContent, ...props}) => (
export default ({handleChange, handleApply, changed, ...props}) => (
<form onSubmit={handleApply}>
<div className={styles.wrapper}>
<div className={styles.container}>
@@ -38,9 +38,9 @@ export default ({handleChange, handleApply, changed, updateQuestionBoxContent, .
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premodLinks"
name="plinksenable"
onChange={handleChange}
defaultChecked={props.premodLinks}
defaultChecked={props.premodLinksEnable}
info={{
title: lang.t('configureCommentStream.enablePremodLinks'),
description: lang.t('configureCommentStream.enablePremodLinksDescription')
@@ -57,11 +57,10 @@ export default ({handleChange, handleApply, changed, updateQuestionBoxContent, .
title: lang.t('configureCommentStream.enableQuestionBox'),
description: lang.t('configureCommentStream.enableQuestionBoxDescription')
}} />
<div className={`${props.questionBoxEnable ? null : styles.hidden}`} >
<TextField
id="qboxcontent"
onChange={updateQuestionBoxContent}
onChange={handleChange}
rows={3}
value={props.questionBoxContent}
label={lang.t('configureCommentStream.includeQuestionHere')}
@@ -15,13 +15,14 @@ class ConfigureStreamContainer extends Component {
super(props);
this.state = {
changed: false
changed: false,
dirtySettings: props.asset.settings,
closedAt: (props.asset.closedAt === null ? 'open' : 'closed')
};
this.toggleStatus = this.toggleStatus.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleApply = this.handleApply.bind(this);
this.updateQuestionBoxContent = this.updateQuestionBoxContent.bind(this);
}
handleApply (e) {
@@ -31,7 +32,7 @@ class ConfigureStreamContainer extends Component {
const questionBoxEnable = elements.qboxenable.checked;
const questionBoxContent = elements.qboxcontent.value;
const premodLinksEnable = elements.premodLinks.checked;
const premodLinksEnable = elements.plinksenable.checked;
const {changed} = this.state;
const newConfig = {
@@ -48,39 +49,52 @@ class ConfigureStreamContainer extends Component {
changed: false
});
}, 300);
// this.props.loadAsset(this.props.data.asset);
}
}
handleChange (e) {
// TODO: Dont directly manipulate state and make state change immutable.
if (e.target && e.target.id === 'qboxenable') {
this.props.asset.settings.questionBoxEnable = e.target.checked;
this.state.dirtySettings.questionBoxEnable = e.target.checked;
}
if (e.target && e.target.id === 'qboxcontent') {
this.state.dirtySettings.questionBoxContent = e.target.value;
}
if (e.target && e.target.id === 'plinksenable') {
this.state.dirtySettings.premodLinksEnable = e.target.value;
}
this.setState({
changed: true
});
}
updateQuestionBoxContent(e) {
this.props.asset.settings.questionBoxContent = e.target.value;
this.handleChange(e);
}
toggleStatus () {
// update the closedAt status for the asset
this.props.updateStatus(
this.props.asset.closedAt === null ? 'closed' : 'open'
this.state.closedAt === 'open' ? 'closed' : 'open'
);
this.setState({
closedAt: (this.state.closedAt === 'open' ? 'closed' : 'open')
});
}
getClosedIn () {
const {closedTimeout} = this.props.asset.settings;
const {created_at} = this.props.asset;
return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
}
render () {
const {settings, closedAt} = this.props.asset;
const status = closedAt === null ? 'open' : 'closed';
const premod = settings.moderation === 'PRE';
const {dirtySettings} = this.state;
const premod = dirtySettings.moderation === 'PRE';
const {closedAt} = this.state;
const closedTimeout = dirtySettings.closedTimeout;
return (
<div>
@@ -88,18 +102,17 @@ class ConfigureStreamContainer extends Component {
handleChange={this.handleChange}
handleApply={this.handleApply}
changed={this.state.changed}
premodLinks={settings.premodLinks}
premodLinksEnable={dirtySettings.premodLinksEnable}
premod={premod}
updateQuestionBoxContent={this.updateQuestionBoxContent}
questionBoxEnable={settings.questionBoxEnable}
questionBoxContent={settings.questionBoxContent}
questionBoxEnable={dirtySettings.questionBoxEnable}
questionBoxContent={dirtySettings.questionBoxContent}
/>
<hr />
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
{status === 'open' ? <p>The comment stream will close in {this.getClosedIn()}.</p> : ''}
<h3>{closedAt === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
{(closedAt === 'open' && closedTimeout) ? <p>The comment stream will close in {this.getClosedIn()}.</p> : ''}
<CloseCommentsInfo
onClick={this.toggleStatus}
status={status}
status={closedAt}
/>
</div>
);
+2 -2
View File
@@ -1,8 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {render} from 'react-dom';
import {GraphQLDocs} from 'graphql-docs';
import fetcher from './services/fetcher';
// Render the application into the DOM
ReactDOM.render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
@@ -0,0 +1,16 @@
import React from 'react';
import {Router, Route, browserHistory} from 'react-router';
import Embed from './Embed';
import SignInContainer from 'coral-sign-in/containers/SignInContainer';
const routes = (
<div>
<Route exact path="/embed/stream/login" component={SignInContainer}/>
<Route exact path="*" component={Embed}/>
</div>
);
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
export default AppRouter;
+11
View File
@@ -12,3 +12,14 @@
filter: blur(2px);
pointer-events: none;
}
.topRightMenu {
float: right;
text-align: right;
cursor: pointer;
margin-top: 5px;
}
.topRightMenu > * {
text-align: initial;
}
+64 -29
View File
@@ -19,11 +19,13 @@ import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
import Slot from 'coral-framework/components/Slot';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu';
import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils';
import styles from './Comment.css';
const getActionSummary = (type, comment) => comment.action_summaries
.filter((a) => a.__typename === type)[0];
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
// hold actions links (e.g. Like, Reply) along the comment footer
@@ -83,11 +85,17 @@ class Comment extends React.Component {
}).isRequired
}).isRequired,
// given a comment, return whether it should be rendered as ignored
commentIsIgnored: React.PropTypes.func,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
}
render () {
@@ -110,20 +118,29 @@ class Comment extends React.Component {
deleteAction,
addCommentTag,
removeCommentTag,
ignoreUser,
disableReply,
commentIsIgnored,
} = this.props;
const like = getActionSummary('LikeActionSummary', comment);
const flag = getActionSummary('FlagActionSummary', comment);
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
const likeSummary = getActionSummary('LikeActionSummary', comment);
const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary('DontAgreeActionSummary', comment);
let myFlag = null;
if (iPerformedThisAction('FlagActionSummary', comment)) {
myFlag = flagSummary.find(s => s.current_user);
} else if (iPerformedThisAction('DontAgreeActionSummary', comment)) {
myFlag = dontAgreeSummary.find(s => s.current_user);
}
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async () => {
const notifyOnError = (fn, errorToMessage) => async function (...args) {
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
try {
return await fn();
return await fn(...args);
} catch (error) {
addNotification('error', errorToMessage(error));
throw error;
@@ -157,12 +174,25 @@ class Comment extends React.Component {
? <TagLabel><BestIndicator /></TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<Slot fill="commentInfoBar" comment={comment} commentId={comment.id} inline/>
{ (currentUser && (comment.user.id !== currentUser.id))
? <span className={styles.topRightMenu}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
</span>
: null
}
<Content body={comment.body} />
<div className="commentActionsLeft comment__action-container">
<ActionButton>
{/* TODO implmement iPerformedThisAction for the like */}
<LikeButton
like={like}
totalLikes={getTotalActionCount('LikeActionSummary', comment)}
like={likeSummary[0]}
id={comment.id}
postLike={postLike}
deleteAction={deleteAction}
@@ -187,6 +217,7 @@ class Comment extends React.Component {
removeBest={removeBestTag} />
</IfUserCanModifyBest>
</ActionButton>
<Slot fill="commentDetail" comment={comment} commentId={comment.id} inline/>
</div>
<div className="commentActionsRight comment__action-container">
<ActionButton>
@@ -194,7 +225,8 @@ class Comment extends React.Component {
</ActionButton>
<ActionButton>
<FlagComment
flag={flag && flag.current_user ? flag : dontagree}
flaggedByCurrentUser={!!myFlag}
flag={myFlag}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
@@ -222,26 +254,29 @@ class Comment extends React.Component {
{
comment.replies &&
comment.replies.map(reply => {
return <Comment
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply} />;
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply} />;
})
}
{
+66 -42
View File
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React from 'react';
import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import isEqual from 'lodash/isEqual';
@@ -8,13 +8,13 @@ const lang = new I18n(translations);
import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui';
const {logout, showSignInDialog, requestConfirmEmail} = authActions;
const {logout, showSignInDialog, requestConfirmEmail, openSignInPopUp, checkLogin} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset';
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
@@ -26,7 +26,6 @@ import {ModerationLink} from 'coral-plugin-moderation';
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 ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUsernameContainer';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
@@ -36,14 +35,22 @@ import HighlightedComment from './Comment';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
class Embed extends Component {
class Embed extends React.Component {
state = {activeTab: 0, showSignInDialog: false, activeReplyBox: ''};
constructor(props) {
super(props);
this.state = {
activeTab: 0,
showSignInDialog: false,
activeReplyBox: ''
};
}
changeTab = (tab) => {
// Everytime the comes from another tab, the Stream needs to be updated.
if (tab === 0) {
this.props.viewAllComments();
this.props.data.refetch();
}
@@ -63,10 +70,14 @@ class Embed extends Component {
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
}
componentDidMount () {
pym.sendMessage('childReady');
this.props.checkLogin();
}
componentWillUnmount () {
@@ -78,10 +89,12 @@ class Embed extends Component {
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
loadAsset(nextProps.data.asset);
const {getCounts, updateCountCache} = this.props;
const {getCounts, updateCountCache, asset: {countCache}} = this.props;
const {asset} = nextProps.data;
updateCountCache(asset.id, asset.commentCount);
if (!countCache) {
updateCountCache(asset.id, asset.commentCount);
}
this.setState({
countPoll: setInterval(() => {
@@ -106,8 +119,7 @@ class Embed extends Component {
setActiveReplyBox = (reactKey) => {
if (!this.props.auth.user) {
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
this.props.showSignInDialog();
} else {
this.setState({activeReplyBox: reactKey});
}
@@ -116,8 +128,8 @@ class Embed extends Component {
render () {
const {activeTab} = this.state;
const {closedAt, countCache = {}} = this.props.asset;
const {loading, asset, refetch, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
const {asset, refetch, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog} = this.props.auth;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
const highlightedComment = comment && comment.parent ? comment.parent : comment;
@@ -126,11 +138,17 @@ class Embed extends Component {
const banned = user && user.status === 'BANNED';
const hasOlderComments = !!(
asset &&
asset.lastComment &&
asset.lastComment.id !== asset.comments[asset.comments.length - 1].id
);
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 200
} : {};
if (loading || !asset) {
if (!asset) {
return <Spinner />;
}
@@ -139,12 +157,14 @@ class Embed extends Component {
? asset.comments[0].created_at
: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
const userBox = <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>;
return (
<div style={expandForLogin}>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.totalCommentCount}/></Tab>
<Tab>{lang.t('MY_COMMENTS')}</Tab>
<Tab>{lang.t('myProfile')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
@@ -157,8 +177,8 @@ class Embed extends Component {
this.props.data.refetch();
}}>{lang.t('showAllComments')}</Button>
}
{loggedIn && <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>}
<TabContent show={activeTab === 0}>
{ loggedIn ? userBox : null }
{
openStream
? <div id="commentBox">
@@ -197,11 +217,10 @@ class Embed extends Component {
</div>
: <p>{asset.settings.closedMessage}</p>
}
{!loggedIn && <SignInContainer
requireEmailConfirmation={asset.settings.requireEmailConfirmation}
refetch={refetch}
offset={signInOffset}/>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
{!loggedIn && <Button id='coralSignInButton' onClick={this.props.showSignInDialog} full>Sign in to comment</Button>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} user={user} />}
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
{/* the highlightedComment is isolated after the user followed a permalink */}
@@ -249,35 +268,37 @@ class Embed extends Component {
postDontAgree={this.props.postDontAgree}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
ignoreUser={this.props.ignoreUser}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
comments={asset.comments}
ignoredUsers={this.props.userData.ignoredUsers} />
</div>
<LoadMore
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={countCache[asset.id] > asset.comments.length}
moreComments={hasOlderComments}
loadMore={this.props.loadMore} />
</div>
}
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
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>
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
loggedIn={loggedIn}
userData={this.props.userData}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
{ loggedIn ? userBox : null }
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
</div>
</div>
);
@@ -287,7 +308,7 @@ class Embed extends Component {
const mapStateToProps = state => ({
auth: state.auth.toJS(),
userData: state.user.toJS(),
asset: state.asset.toJS()
asset: state.asset.toJS(),
});
const mapDispatchToProps = dispatch => ({
@@ -296,11 +317,13 @@ const mapDispatchToProps = dispatch => ({
addNotification: (type, text) => addNotification(type, text),
clearNotification: () => dispatch(clearNotification()),
editName: (username) => dispatch(editName(username)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
showSignInDialog: () => dispatch(showSignInDialog()),
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
viewAllComments: () => dispatch(viewAllComments()),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
openSignInPopUp: cb => dispatch(openSignInPopUp(cb)),
checkLogin: () => dispatch(checkLogin()),
dispatch: d => dispatch(d),
});
export default compose(
@@ -311,6 +334,7 @@ export default compose(
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
queryStream
queryStream,
)(Embed);
@@ -0,0 +1,14 @@
.IgnoreUserWizard {
background-color: #2E343B;
color: white;
padding: 1em;
max-width: 220px;
}
.IgnoreUserWizard header {
font-weight: bold;
}
.IgnoreUserWizard .textAlignRight {
text-align: right;
}
@@ -0,0 +1,66 @@
import React, {PropTypes} from 'react';
import styles from './IgnoreUserWizard.css';
import {Button} from 'coral-ui';
// Guides the user through ignoring another user, including confirming their decision
export class IgnoreUserWizard extends React.Component {
static propTypes = {
// comment on which this menu appears
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
cancel: PropTypes.func.isRequired,
// actually submit the ignore. Provide {id: user id to ignore}
ignoreUser: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
// what step of the wizard is the user on
step: 1
};
this.onClickCancel = this.onClickCancel.bind(this);
}
onClickCancel() {
this.props.cancel();
}
render() {
const {user, ignoreUser} = this.props;
const goToStep = (stepNum) => this.setState({step: stepNum});
const step1 = (
<div>
<header>Ignore User</header>
<p>When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.</p>
<div className={styles.textAlignRight}>
<Button cStyle='cancel' onClick={this.onClickCancel}>Cancel</Button>
<Button onClick={() => goToStep(2)}>Ignore user</Button>
</div>
</div>
);
const onClickIgnoreUser = async () => {
await ignoreUser({id: user.id});
};
const step2Confirmation = (
<div>
<header>Ignore User</header>
<p>Are you sure you want to ignore { user.name }?</p>
<div className={styles.textAlignRight}>
<Button cStyle='cancel' onClick={this.onClickCancel}>Cancel</Button>
<Button onClick={onClickIgnoreUser}>Ignore user</Button>
</div>
</div>
);
const elsForStep = [step1, step2Confirmation];
const {step} = this.state;
const elForThisStep = elsForStep[step - 1];
return (
<div className={styles.IgnoreUserWizard}>
{ elForThisStep }
</div>
);
}
}
@@ -0,0 +1,22 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
// Render in place of a Comment when the author of the comment is ignored
const IgnoredCommentTombstone = () => (
<div>
<hr aria-hidden={true} />
<p style={{
backgroundColor: '#F0F0F0',
textAlign: 'center',
padding: '1em',
color: '#3E4F71',
}}>
{lang.t('commentIsIgnored')}
</p>
</div>
);
export default IgnoredCommentTombstone;
+2 -2
View File
@@ -17,10 +17,10 @@ const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, upd
const NewCount = (props) => {
const newComments = props.commentCount - props.countCache;
return <div className='coral-new-comments'>
return <div className='coral-new-comments coral-load-more'>
{
props.countCache && newComments > 0 ?
<button onClick={onLoadMoreClick(props)} className='coral-load-more'>
<button onClick={onLoadMoreClick(props)}>
{newComments === 1
? lang.t('newCount', newComments, lang.t('comment'))
: lang.t('newCount', newComments, lang.t('comments'))}
+40 -22
View File
@@ -1,5 +1,6 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
class Stream extends React.Component {
@@ -19,6 +20,12 @@ class Stream extends React.Component {
// dispatch action to remove a tag from a comment
removeCommentTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
// list of user ids that should be rendered as ignored
ignoredUsers: React.PropTypes.arrayOf(React.PropTypes.string)
}
constructor(props) {
@@ -41,33 +48,44 @@ class Stream extends React.Component {
deleteAction,
showSignInDialog,
addCommentTag,
removeCommentTag
removeCommentTag,
pluginProps,
ignoreUser,
ignoredUsers,
} = this.props;
const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id);
return (
<div id='stream'>
{
comments.map(comment =>
<Comment
disableReply={!open}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment} />
commentIsIgnored(comment)
? <IgnoredCommentTombstone
key={comment.id}
/>
: <Comment
disableReply={!open}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
pluginProps={pluginProps}
/>
)
}
</div>
@@ -0,0 +1,24 @@
.Toggleable:focus {
outline: none;
}
/**
* Up/Down Chevrons for the top right menu
*/
.chevron {
}
.chevron:before {
content: '⌃';
display: inline-block;
position: relative;
top: 0.25em;
}
/* Down Arrow */
.chevron.down:before {
display: inline-block;
position: relative;
transform: rotate(180deg);
top: 0;
/*top: -0.25em;*/
}
@@ -0,0 +1,94 @@
import React, {PropTypes} from 'react';
import classnames from 'classnames';
import {IgnoreUserWizard} from './IgnoreUserWizard';
import styles from './TopRightMenu.css';
// TopRightMenu appears as a dropdown in the top right of the comment.
// when you click the down cehvron, it expands and shows IgnoreUserWizard
// when you click 'cancel' in the wizard, it closes the menu
export class TopRightMenu extends React.Component {
static propTypes = {
// comment on which this menu appears
comment: PropTypes.shape({
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
}).isRequired,
ignoreUser: PropTypes.func,
// show notification to the user (e.g. for errors)
addNotification: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
timesReset: 0
};
}
render() {
const {comment, ignoreUser, addNotification} = this.props;
// timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable)
const reset = () => this.setState({timesReset: this.state.timesReset + 1});
const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => {
// close menu
reset();
// ignore user
try {
await ignoreUser({id});
} catch (error) {
addNotification('error', 'Failed to ignore user');
throw error;
}
};
return (
<Toggleable key={this.state.timesReset}>
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
<IgnoreUserWizard
user={comment.user}
cancel={reset}
ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError}
/>
</div>
</Toggleable>
);
}
}
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
class Toggleable extends React.Component {
constructor(props) {
super(props);
this.toggle = this.toggle.bind(this);
this.close = this.close.bind(this);
this.state = {
isOpen: false
};
}
toggle() {
this.setState({isOpen: ! this.state.isOpen});
}
close() {
this.setState({isOpen: false});
}
render() {
const {children} = this.props;
const {isOpen} = this.state;
return (
// /*onBlur={ this.close } */
<span className={styles.Toggleable} tabIndex="0" >
<span className={styles.toggler}
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
{isOpen ? children : null}
</span>
);
}
}
+5 -3
View File
@@ -3,13 +3,15 @@ import {render} from 'react-dom';
import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/services/client';
import store from 'coral-framework/services/store';
import localStore from 'coral-framework/services/store';
import Embed from './Embed';
import AppRouter from './AppRouter';
const store = (window.opener && window.opener.coralStore) ? window.opener.coralStore : localStore;
render(
<ApolloProvider client={client} store={store}>
<Embed />
<AppRouter />
</ApolloProvider>
, document.querySelector('#coralStream')
);
+54 -14
View File
@@ -1,8 +1,6 @@
* {
font-weight: inherit;
font-family: inherit;
font-style: inherit;
font-size: 100%;
}
html, body {
@@ -11,7 +9,7 @@ html, body {
}
body {
font-family: 'Lato', sans-serif;
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
width: 100%;
font-size: 14px;
margin: 0px;
@@ -80,8 +78,8 @@ hr {
.coral-plugin-infobox-info {
top: 0;
border: 0;
background: rgb(35,118,216);
color: white;
background: #DEEDFF;
color: #2a2a2a;
width: 100%;
text-align: left;
padding: 10px;
@@ -92,6 +90,24 @@ hr {
}
.coral-plugin-infobox-info em{
font-style: italic;
}
.coral-plugin-infobox-info strong{
font-weight: bold;
}
.coral-plugin-infobox-info blockquote{
border-left: solid 2px #2a2a2a;
padding-left: 10px;
}
.coral-plugin-infobox-info a{
color: #2a2a2a;
}
.commentStream .material-icons {
vertical-align: middle;
width: 1em;
@@ -111,15 +127,15 @@ hr {
margin-bottom: 0px;
font-weight: bold;
font-size: 14px;
display: block;
overflow: hidden;
height: 50px;
min-height: 50px;
display: flex;
}
.coral-plugin-questionbox-icon.bubble{
position: absolute;
top: 11px;
left: 15px;
left: 10px;
color: #949393;
font-size: 20px;
z-index: 0;
@@ -128,7 +144,7 @@ hr {
.coral-plugin-questionbox-icon.person{
z-index: 2;
top: 20px;
left: 20px;
left: 15px;
position: absolute;
font-size: 24px;
color: white;
@@ -143,12 +159,19 @@ hr {
margin-left: 0px !important;
margin-right: 10px;
display: inline-block;
width: 15px;
height: 100%;
padding: 3px 20px;
width: 10px;
min-height: 100%;
padding: 5px 20px;
vertical-align: middle;
}
.coral-plugin-questionbox-content {
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.hidden {
visibility: hidden;
display: none;
@@ -255,13 +278,13 @@ hr {
.commentActionsRight, .replyActionsRight {
display: flex;
justify-content: flex-end;
width: 50%;
width: 30%;
}
.commentActionsLeft, .replyActionsLeft {
display: flex;
justify-content: flex-start;
float: left;
width: 50%;
width: 70%;
}
.comment__action-container .material-icons {
@@ -373,6 +396,7 @@ button.comment__action-button[disabled],
margin-left: 20px;
margin-top: 5px;
width: 75%;
font-size: 16px;
}
/* Close comments */
@@ -452,3 +476,19 @@ button.comment__action-button[disabled],
.coral-load-more-replies button.coral-load-more, .coral-new-comments button.coral-load-more{
width: initial;
}
@media (min-device-width : 300px) and (max-device-width : 420px) {
.commentActionsLeft.comment__action-container .coral-plugin-likes-button-text,
.commentActionsLeft.comment__action-container > div span {
display: none;
}
.commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button {
visibility: collapse;
margin-left: -30px;
}
.commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button .coral-plugin-replies-icon {
visibility: visible;
}
}
+28 -38
View File
@@ -1,5 +1,8 @@
import pym from 'pym.js';
import {stringify} from 'querystring';
// TODO: Styles should live in a separate file
const snackbarStyles = {
position: 'fixed',
cursor: 'default',
@@ -18,7 +21,8 @@ const snackbarStyles = {
opacity: 0,
transform: 'translate(-50%, 20px)',
bottom: 0,
boxSizing: 'border-box'
boxSizing: 'border-box',
fontFamily: 'Helvetica, \'Helvetica Neue\', Verdana, sans-serif'
};
// This function should return value of window.Coral
@@ -26,30 +30,21 @@ const Coral = {};
const Talk = Coral.Talk = {};
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, asset_url, comment, asset_id) {
let iframeArray = [
function buildStreamIframeUrl(talkBaseUrl, query) {
let url = [
talkBaseUrl,
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?asset_url=',
encodeURIComponent(asset_url)
];
'embed/stream?'
].join('');
if (comment) {
iframeArray.push('&comment_id=');
iframeArray.push(encodeURIComponent(comment));
}
url += stringify(query);
if (asset_id) {
iframeArray.push('&asset_id=');
iframeArray.push(encodeURIComponent(asset_id));
}
return iframeArray.join('');
return url;
}
// Set up postMessage listeners/handlers on the pymParent
// e.g. to resize the iframe, and navigate the host page
function configurePymParent(pymParent, asset_url) {
function configurePymParent(pymParent) {
let notificationOffset = 200;
let ready = false;
let cachedHeight;
@@ -117,8 +112,8 @@ function configurePymParent(pymParent, asset_url) {
if (ready) {
window.clearInterval(interval);
// @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place?
pymParent.sendMessage('DOMContentLoaded', asset_url);
// TODO: It's weird to me that this is sent here
pymParent.sendMessage('DOMContentLoaded');
}
}, 100);
});
@@ -166,42 +161,37 @@ Talk.render = function (el, opts) {
}
opts = opts || {};
// @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
// TODO: infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
if (!opts.talk) {
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
}
// ensure el has an id, as pym can't directly accept the HTMLElement
// Ensure el has an id, as pym can't directly accept the HTMLElement.
if (!el.id) {
el.id = `_${Math.random()}`;
}
let asset_url = opts.asset_url;
if (!asset_url) {
// Compose the query to send down to the Talk API so it knows what to load.
let query = {};
query.comment_id = window.location.hash.slice(1);
query.asset_id = opts.asset_id;
query.asset_url = opts.asset_url;
if (!query.asset_url) {
try {
asset_url = document.querySelector('link[rel="canonical"]').href;
query.asset_url = document.querySelector('link[rel="canonical"]').href;
} catch (e) {
console.warn('This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.');
asset_url = window.location.origin + window.location.pathname;
query.asset_url = window.location.origin + window.location.pathname;
}
}
let comment = window.location.hash.slice(1);
let query = {
configurePymParent(new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, query), {
title: opts.title,
asset_url: asset_url,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
};
if (opts.asset_id && opts.asset_id.length > 0) {
query.asset_id = opts.asset_id;
}
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset_url, comment), query);
configurePymParent(pymParent, asset_url);
}));
};
export default Coral;
+1 -1
View File
@@ -3,7 +3,7 @@ import coralApi from '../helpers/response';
import {addNotification} from '../actions/notification';
import {pym} from 'coral-framework';
import I18n from '../../coral-framework/modules/i18n/i18n';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
+66 -10
View File
@@ -1,3 +1,5 @@
import {gql} from 'react-apollo';
import client from 'coral-framework/services/client';
import I18n from '../../coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
@@ -5,9 +7,48 @@ import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {pym} from 'coral-framework';
const ME_QUERY = gql`
query Me {
me {
status
comments {
id
body
asset {
id
title
url
}
created_at
}
}
}
`;
function fetchMe() {
return client.query({
fetchPolicy: 'network-only',
query: ME_QUERY});
}
// Dialog Actions
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
export const showSignInDialog = () => dispatch => {
const signInPopUp = window.open(
'/embed/stream/login',
'Login',
'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
);
signInPopUp.onbeforeunload = () => {
dispatch(checkLogin());
fetchMe();
};
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
};
export const hideSignInDialog = () => dispatch => {
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
window.close();
};
export const createUsernameRequest = () => ({type: actions.CREATE_USERNAME_REQUEST});
export const showCreateUsernameDialog = () => ({type: actions.SHOW_CREATEUSERNAME_DIALOG});
@@ -31,28 +72,39 @@ export const createUsername = (userId, formData) => dispatch => {
});
};
export const changeView = view => dispatch =>
export const changeView = view => dispatch => {
dispatch({
type: actions.CHANGE_VIEW,
view
});
switch(view) {
case 'SIGNUP':
window.resizeTo(500, 800);
break;
case 'FORGOT':
window.resizeTo(500, 400);
break;
default:
window.resizeTo(500, 550);
}
};
export const cleanState = () => ({type: actions.CLEAN_STATE});
// Sign In Actions
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
// TODO: revisit login redux flow.
// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
//
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
return coralApi('/auth/local', {method: 'POST', body: formData})
.then(({user}) => {
const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
})
.then(() => dispatch(hideSignInDialog()))
.catch(error => {
if (error.metadata) {
@@ -104,6 +156,7 @@ export const facebookCallback = (err, data) => dispatch => {
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(showCreateUsernameDialog());
dispatch(hideSignInDialog());
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -151,7 +204,10 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
.then(() => dispatch(logOutSuccess()))
.then(() => {
dispatch(logOutSuccess());
fetchMe();
})
.catch(error => dispatch(logOutFailure(error)));
};
+9
View File
@@ -0,0 +1,9 @@
import * as authActions from './auth';
import * as assetActions from './asset';
import * as notificationActions from './notification';
export default {
authActions,
assetActions,
notificationActions,
};
@@ -1,9 +1,12 @@
import {pym} from '../../coral-framework';
import pym from '../services/PymConnection';
import * as actions from '../constants/notification';
export const addNotification = (notifType, text) => {
pym.sendMessage('coral-alert', `${notifType}|${text}`);
return {type: actions.ADD_NOTIFICATION, notifType, text};
};
export const clearNotification = () => {
pym.sendMessage('coral-clear-notification');
return {type: actions.CLEAR_NOTIFICATION};
};
@@ -0,0 +1,3 @@
.inline {
display: inline-block;
}
+20
View File
@@ -0,0 +1,20 @@
import React, {Component} from 'react';
import {getSlotElements} from 'coral-framework/helpers/plugins';
import styles from './Slot.css';
class Slot extends Component {
render() {
const {fill, inline = false, ...rest} = this.props;
return (
<div className={inline ? styles.inline : ''}>
{getSlotElements(fill, rest)}
</div>
);
}
}
Slot.propTypes = {
fill: React.PropTypes.string
};
export default Slot;
@@ -0,0 +1,2 @@
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
+2
View File
@@ -6,3 +6,5 @@ export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS';
export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const UPDATE_USERNAME = 'UPDATE_USERNAME';
export const IGNORE_USER_SUCCESS = 'IGNORE_USER_SUCCESS';
export const STOP_IGNORING_USER_SUCCESS = 'STOP_IGNORING_USER_SUCCESS';
@@ -0,0 +1,7 @@
mutation ignoreUser ($id: ID!) {
ignoreUser(id:$id) {
errors {
translation_key
}
}
}
@@ -6,6 +6,12 @@ import POST_DONT_AGREE from './postDontAgree.graphql';
import DELETE_ACTION from './deleteAction.graphql';
import ADD_COMMENT_TAG from './addCommentTag.graphql';
import REMOVE_COMMENT_TAG from './removeCommentTag.graphql';
import IGNORE_USER from './ignoreUser.graphql';
import STOP_IGNORING_USER from './stopIgnoringUser.graphql';
import MY_IGNORED_USERS from '../queries/myIgnoredUsers.graphql';
import STREAM_QUERY from '../queries/streamQuery.graphql';
import {variablesForStreamQuery} from '../queries';
import commentView from '../fragments/commentView.graphql';
@@ -14,12 +20,11 @@ export const postComment = graphql(POST_COMMENT, {
fragments: commentView
}),
props: ({ownProps, mutate}) => ({
postItem: ({asset_id, body, parent_id}) =>
mutate({
postItem: comment => {
const {asset_id, body, parent_id, tags = []} = comment;
return mutate({
variables: {
asset_id,
body,
parent_id
comment
},
optimisticResponse: {
createComment: {
@@ -33,14 +38,14 @@ export const postComment = graphql(POST_COMMENT, {
parent_id,
asset_id,
action_summaries: [],
tags: [],
tags,
status: null,
id: 'pending'
}
}
},
updateQueries: {
AssetQuery: (oldData, {mutationResult:{data:{createComment:{comment}}}}) => {
AssetQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => {
if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return oldData;
@@ -56,8 +61,8 @@ export const postComment = graphql(POST_COMMENT, {
...oldData.asset,
comments: oldData.asset.comments.map((oldComment) => {
return oldComment.id === parent_id
? {...oldComment, replies: [...oldComment.replies, comment]}
: oldComment;
? {...oldComment, replies: [...oldComment.replies, comment]}
: oldComment;
})
}
};
@@ -77,7 +82,8 @@ export const postComment = graphql(POST_COMMENT, {
return updatedAsset;
}
}
})
});
}
}),
});
@@ -148,3 +154,40 @@ export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, {
});
}}),
});
export const ignoreUser = graphql(IGNORE_USER, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
return mutate({
variables: {
id,
},
refetchQueries: [{
query: MY_IGNORED_USERS,
}]
});
}}),
});
export const stopIgnoringUser = graphql(STOP_IGNORING_USER, {
props: ({mutate, ownProps}) => {
return {
stopIgnoringUser: ({id}) => {
return mutate({
variables: {
id,
},
refetchQueries: [
{
query: MY_IGNORED_USERS,
},
{
query: STREAM_QUERY,
variables: variablesForStreamQuery(ownProps),
}
]
});
}
};
}
});
@@ -1,7 +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) {
mutation CreateComment ($comment: CreateCommentInput!) {
createComment(comment: $comment) {
comment {
...commentView
replyCount
@@ -0,0 +1,7 @@
mutation stopIgnoringUser ($id: ID!) {
stopIgnoringUser(id:$id) {
errors {
translation_key
}
}
}
+32 -19
View File
@@ -3,6 +3,7 @@ import STREAM_QUERY from './streamQuery.graphql';
import LOAD_MORE from './loadMore.graphql';
import GET_COUNTS from './getCounts.graphql';
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
import MY_IGNORED_USERS from './myIgnoredUsers.graphql';
import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
@@ -28,15 +29,15 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => {
variables: {
asset_id,
limit,
sort
sort,
excludeIgnored: data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{data}}) => {
updateQuery: (oldData, {fetchMoreResult:{asset}}) => {
return {
...oldData,
asset: {
...oldData.asset,
commentCount: data.asset.commentCount
commentCount: asset.commentCount
}
};
}
@@ -52,10 +53,10 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s
cursor, // the date of the first/last comment depending on the sort order
parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
asset_id, // the id of the asset we're currently on
sort // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
excludeIgnored: data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => {
updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => {
let updatedAsset;
if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment
@@ -120,21 +121,25 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s
});
};
export const variablesForStreamQuery = ({auth}) => {
// where the query string is from the embeded iframe url
let comment_id = getQueryVariable('comment_id');
let has_comment = comment_id != null;
return {
asset_id: getQueryVariable('asset_id'),
asset_url: getQueryVariable('asset_url'),
comment_id: has_comment ? comment_id : 'no-comment',
has_comment,
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
};
};
// load the comment stream.
export const queryStream = graphql(STREAM_QUERY, {
options: () => {
// where the query string is from the embeded iframe url
let comment_id = getQueryVariable('comment_id');
let has_comment = comment_id != null;
options: (props) => {
return {
variables: {
asset_id: getQueryVariable('asset_id'),
asset_url: getQueryVariable('asset_url'),
comment_id: has_comment ? comment_id : 'no-comment',
has_comment
}
variables: variablesForStreamQuery(props)
};
},
props: ({data}) => ({
@@ -145,3 +150,11 @@ export const queryStream = graphql(STREAM_QUERY, {
});
export const myCommentHistory = graphql(MY_COMMENT_HISTORY, {});
export const myIgnoredUsers = graphql(MY_IGNORED_USERS, {
props: ({data}) => {
return ({
myIgnoredUsersData: data
});
}
});
@@ -1,9 +1,9 @@
#import "../fragments/commentView.graphql"
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER) {
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort}) {
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
...commentView
replyCount
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3) {
...commentView
}
@@ -0,0 +1,6 @@
query myIgnoredUsers {
myIgnoredUsers {
id,
username,
}
}
@@ -1,18 +1,18 @@
#import "../fragments/commentView.graphql"
query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) {
query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!, $excludeIgnored: Boolean) {
# the comment here is for loading one comment and it's children, probably after following a permalink
# $has_comment is derived from the comment_id query param in the iframe url,
# which is in turn pulled from the host page url
comment(id: $comment_id) @include(if: $has_comment) {
...commentView
replyCount
replyCount(excludeIgnored: $excludeIgnored)
replies {
...commentView
}
parent {
...commentView
replyCount
replyCount(excludeIgnored: $excludeIgnored)
replies {
...commentView
}
@@ -28,6 +28,7 @@ query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comm
moderation
infoBoxEnable
infoBoxContent
premodLinksEnable
questionBoxEnable
questionBoxContent
closeTimeout
@@ -36,12 +37,15 @@ query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comm
charCount
requireEmailConfirmation
}
commentCount
totalCommentCount
comments(limit: 10) {
lastComment {
id
}
commentCount(excludeIgnored: $excludeIgnored)
totalCommentCount(excludeIgnored: $excludeIgnored)
comments(limit: 10, excludeIgnored: $excludeIgnored) {
...commentView
replyCount
replies(limit: 3) {
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3, excludeIgnored: $excludeIgnored) {
...commentView
}
}
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import merge from 'lodash/merge';
import flatten from 'lodash/flatten';
import plugins from 'pluginsConfig';
export const pluginReducers = merge(
...plugins
.filter(o => o.module.reducer)
.map(o => ({...o.module.reducer}))
);
/**
* Returns React Elements for given slot.
*/
export function getSlotElements(slot, props = {}) {
const components = flatten(plugins
.filter(o => o.module.slots[slot])
.map(o => o.module.slots[slot]));
return components
.map((component, i) => React.createElement(component, {...props, key: i}));
}
+6 -9
View File
@@ -1,15 +1,12 @@
import store from './services/store';
import pym from './services/PymConnection';
import I18n from './modules/i18n/i18n';
import * as authActions from './actions/auth';
import * as assetActions from './actions/asset';
import * as notificationActions from './actions/notification';
import actions from './actions';
export {
// TODO (bc): Deprecate old actions. Spreading actions is now needed.
export default {
pym,
I18n,
store,
authActions,
assetActions,
notificationActions
actions,
...actions
};
@@ -0,0 +1,33 @@
/**
* Executes `source` to retrieve plugins configuration
* and loads the `index.js` of specified plugins.
*
* Outputs a module that looks like the following:
*
* module.exports = [{plugin: string, module: object}, ...]
*
*/
const {stripIndent} = require('common-tags');
function getPluginList(config) {
if (config && config.client) {
return config.client.map(x => typeof x === 'string' ? x : Object.keys(x)[0]);
}
return [];
}
module.exports = function(source) {
this.cacheable();
const config = this.exec(source, this.resourcePath);
const plugins = getPluginList(config).map((plugin) => `{
module: require('${plugin}/client'),
plugin: '${plugin}'
}`);
return stripIndent`
module.exports = [
${plugins.join(',')}
];
`;
};
+1 -2
View File
@@ -28,8 +28,7 @@ export default function auth (state = initialState, action) {
switch (action.type) {
case actions.SHOW_SIGNIN_DIALOG :
return state
.set('showSignInDialog', true)
.set('signInOffset', action.offset);
.set('showSignInDialog', true);
case actions.HIDE_SIGNIN_DIALOG :
return state.merge(Map({
isLoading: false,
+4
View File
@@ -1,9 +1,13 @@
import auth from './auth';
import user from './user';
import asset from './asset';
import {reducer as commentBox} from '../../coral-plugin-commentbox';
import {pluginReducers} from '../helpers/plugins';
export default {
auth,
user,
asset,
commentBox,
...pluginReducers
};
+12 -4
View File
@@ -1,4 +1,4 @@
import {Map} from 'immutable';
import {Map, Set} from 'immutable';
import * as authActions from '../constants/auth';
import * as actions from '../constants/user';
import * as assetActions from '../constants/assets';
@@ -8,7 +8,8 @@ const initialState = Map({
profiles: [],
settings: {},
myComments: [],
myAssets: [] // the assets from which myComments (above) originated
myAssets: [], // the assets from which myComments (above) originated
ignoredUsers: Set(),
});
const purge = user => {
@@ -38,7 +39,14 @@ export default function user (state = initialState, action) {
return state.set('myAssets', action.assets);
case actions.LOGOUT_SUCCESS:
return initialState;
default :
return state;
case 'APOLLO_MUTATION_RESULT':
switch (action.operationName) {
case 'ignoreUser':
return state.updateIn(['ignoredUsers'], i => i.add(action.variables.id));
case 'stopIgnoringUser':
return state.updateIn(['ignoredUsers'], i => i.delete(action.variables.id));
}
break;
}
return state;
}
+3 -1
View File
@@ -6,9 +6,11 @@ export const client = new ApolloClient({
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 `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
}
return null;
},
networkInterface: getNetworkInterface()
});
export default client;
+16 -3
View File
@@ -3,9 +3,19 @@ import thunk from 'redux-thunk';
import mainReducer from '../reducers';
import {client} from './client';
const apolloErrorReporter = () => next => action => {
if (action.type === 'APOLLO_QUERY_ERROR') {
console.error(action.error);
}
return next(action);
};
const middlewares = [
applyMiddleware(client.middleware()),
applyMiddleware(thunk)
applyMiddleware(
client.middleware(),
thunk,
apolloErrorReporter,
),
];
if (window.devToolsExtension) {
@@ -14,7 +24,7 @@ if (window.devToolsExtension) {
middlewares.push(window.devToolsExtension());
}
export default createStore(
const store = createStore(
combineReducers({
...mainReducer,
apollo: client.reducer()
@@ -22,3 +32,6 @@ export default createStore(
{},
compose(...middlewares)
);
export default store;
window.coralStore = store;
+4 -1
View File
@@ -2,6 +2,7 @@
"en": {
"MY_COMMENTS": "My Comments",
"profile": "Profile",
"myProfile": "My profile",
"successUpdateSettings": "The changes you have made have been applied to the comment stream on this article",
"successNameUpdate": "Your username has been updated",
"contentNotAvailable": "This content is not available",
@@ -20,6 +21,7 @@
"newCount": "View {0} new {1}",
"comment": "comment",
"comments": "comments",
"commentIsIgnored": "This comment is hidden because you ignored this user.",
"error": {
"emailNotVerified": "Email address {0} not verified.",
"email": "Not a valid E-Mail",
@@ -44,7 +46,7 @@
"es": {
"profile": "Pérfil",
"MY_COMMENTS": "Mis Comentarios",
"profile": "Pérfil",
"myProfile": "Mi pérfil",
"successUpdateSettings": "La configuración de este articulo fue actualizada",
"successBioUpdate": "Tu biografia fue actualizada",
"contentNotAvailable": "El contenido no se encuentra disponible",
@@ -57,6 +59,7 @@
"newCount": "Ver {0} {1} más",
"comment": "commentario",
"comments": "commentarios",
"commentIsIgnored": "Este comentario está escondido porque has ignorado al usuario.",
"showAllComments": "Mostrar todos los comentarios",
"error": {
"emailNotVerified": "E-mail {0} no verificado.",
+31
View File
@@ -0,0 +1,31 @@
export const getTotalActionCount = (type, comment) => {
return comment.action_summaries
.filter(s => s.__typename === type)
.reduce((total, summary) => {
return total + summary.count;
}, 0);
};
export const iPerformedThisAction = (type, comment) => {
// if there is a current_user on any of the ActionSummary(s), the user performed this action
return comment.action_summaries
.filter(a => a.__typename === type)
.some(a => a.current_user);
};
export const getMyActionSummary = (type, comment) => {
return comment.action_summaries
.filter(a => a.__typename === type)
.find(a => a.current_user);
};
/**
* getActionSummary
* retrieves the action summaries based on the type and the comment
* array could be length > 1, as in the case of FlagActionSummary
*/
export const getActionSummary = (type, comment) => {
return comment.action_summaries.filter(a => a.__typename === type);
};
+108 -38
View File
@@ -2,56 +2,57 @@ import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import {Button} from 'coral-ui';
import Slot from 'coral-framework/components/Slot';
import {connect} from 'react-redux';
const name = 'coral-plugin-commentbox';
class CommentBox extends Component {
static propTypes = {
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
}
constructor(props) {
super(props);
state = {
body: '',
username: ''
this.state = {
username: '',
body: '',
hooks: {
preSubmit: [],
postSubmit: []
}
};
}
postComment = () => {
const {
commentPostedHandler,
postItem,
assetId,
updateCountCache,
isReply,
countCache,
assetId,
parentId,
postItem,
countCache,
addNotification,
authorId
updateCountCache,
commentPostedHandler
} = this.props;
let comment = {
body: this.state.body,
asset_id: assetId,
author_id: authorId,
parent_id: parentId
parent_id: parentId,
body: this.state.body,
...this.props.commentBox
};
if (this.props.charCount && this.state.body.length > this.props.charCount) {
return;
}
!isReply && updateCountCache(assetId, countCache + 1);
// Execute preSubmit Hooks
this.state.hooks.preSubmit.forEach(hook => hook());
postItem(comment, 'comments')
.then(({data}) => {
const postedComment = data.createComment.comment;
// Execute postSubmit Hooks
this.state.hooks.postSubmit.forEach(hook => hook(data));
if (postedComment.status === 'REJECTED') {
addNotification('error', lang.t('comment-post-banned-word'));
!isReply && updateCountCache(assetId, countCache);
@@ -64,14 +65,67 @@ class CommentBox extends Component {
commentPostedHandler();
}
})
.catch((err) => console.error(err));
.catch((err) => console.error(err));
this.setState({body: ''});
}
registerHook = (hookType = '', hook = () => {}) => {
if (typeof hook !== 'function') {
return console.warn(`Hooks must be functions. Please check your ${hookType} hooks`);
} else if (typeof hookType === 'string') {
this.setState(state => ({
hooks: {
...state.hooks,
[hookType]: [
...state.hooks[hookType],
hook
]
}
}));
return {
hookType,
hook
};
} else {
return console.warn('hookTypes must be a string. Please check your hooks');
}
}
unregisterHook = hookData => {
const {hookType, hook} = hookData;
this.setState(state => {
let newHooks = state.hooks[newHooks];
const idx = state.hooks[hookType].indexOf(hook);
if (idx !== -1) {
newHooks = [
...state.hooks[hookType].slice(0, idx),
...state.hooks[hookType].slice(idx + 1)
];
}
return {
hooks: {
...state.hooks,
[hookType]: newHooks
}
};
});
}
handleChange = e => this.setState({body: e.target.value});
render () {
const {styles, isReply, authorId, charCount} = this.props;
let {cancelButtonClicked} = this.props;
const length = this.state.body.length;
const enablePostComment = !length || (charCount && length > charCount);
if (isReply && typeof cancelButtonClicked !== 'function') {
console.warn('the CommentBox component should have a cancelButtonClicked callback defined if it lives in a Reply');
@@ -93,33 +147,35 @@ class CommentBox extends Component {
value={this.state.body}
placeholder={lang.t('comment')}
id={isReply ? 'replyText' : 'commentText'}
onChange={(e) => this.setState({body: e.target.value})}
onChange={this.handleChange}
rows={3}/>
</div>
<div className={`${name}-char-count ${length > charCount ? `${name}-char-max` : ''}`}>
{
charCount &&
`${charCount - length} ${lang.t('characters-remaining')}`
}
{charCount && `${charCount - length} ${lang.t('characters-remaining')}`}
</div>
<div className={`${name}-button-container`}>
<Slot
fill="commentBoxDetail"
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
inline
/>
{
isReply && (
<Button
cStyle='darkGrey'
className={`${name}-cancel-button`}
onClick={() => {
cancelButtonClicked('');
}}>
onClick={() => cancelButtonClicked('')}>
{lang.t('cancel')}
</Button>
)
}
{ authorId && (
<Button
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
cStyle={enablePostComment ? 'lightGrey' : 'darkGrey'}
className={`${name}-button`}
onClick={this.postComment}>
onClick={this.postComment}
disabled={enablePostComment ? 'disabled' : ''}>
{lang.t('post')}
</Button>
)
@@ -129,6 +185,20 @@ class CommentBox extends Component {
}
}
export default CommentBox;
CommentBox.propTypes = {
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
};
const mapStateToProps = ({commentBox}) => ({commentBox});
export default connect(mapStateToProps, null)(CommentBox);
const lang = new I18n(translations);
@@ -0,0 +1,9 @@
export const addTag = tag => ({
type: 'ADD_TAG',
tag
});
export const removeTag = idx => ({
type: 'REMOVE_TAG',
idx
});
@@ -0,0 +1,2 @@
export const ADD_TAG = 'ADD_TAG';
export const REMOVE_TAG = 'REMOVE_TAG';
+5
View File
@@ -0,0 +1,5 @@
import reducer from './reducer';
export default {
reducer
};
+25
View File
@@ -0,0 +1,25 @@
import {ADD_TAG, REMOVE_TAG} from './constants';
const initialState = {
tags: []
};
export default function commentBox (state = initialState, action) {
switch (action.type) {
case ADD_TAG :
return {
...state,
tags: [...state.tags, action.tag]
};
case REMOVE_TAG :
return {
...state,
tags: [
...state.tags.slice(0, action.idx),
...state.tags.slice(action.idx + 1)
]
};
default :
return state;
}
}
@@ -0,0 +1,6 @@
.slot {
display: inline-block;
div {
display: inline-block;
}
}
+6 -7
View File
@@ -21,15 +21,14 @@ class FlagButton extends Component {
// When the "report" button is clicked expand the menu
onReportClick = () => {
const {currentUser, flag, deleteAction} = this.props;
const {currentUser, deleteAction, flaggedByCurrentUser, flag} = this.props;
const {localPost, localDelete} = this.state;
const flagged = (flag && flag.current_user && !localDelete) || localPost;
const localFlagged = (flaggedByCurrentUser && !localDelete) || localPost;
if (!currentUser) {
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
this.props.showSignInDialog();
return;
}
if (flagged) {
if (localFlagged) {
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
deleteAction(localPost || flag.current_user.id);
} else if (this.state.showMenu){
@@ -130,9 +129,9 @@ class FlagButton extends Component {
}
render () {
const {flag, getPopupMenu} = this.props;
const {getPopupMenu, flaggedByCurrentUser} = this.props;
const {localPost, localDelete} = this.state;
const flagged = (flag && flag.current_user && !localDelete) || localPost;
const flagged = (flaggedByCurrentUser && !localDelete) || localPost;
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
+4
View File
@@ -1,6 +1,7 @@
@custom-media --big-viewport (min-width: 780px);
.myComment {
margin: 1em 0;
border-bottom: 1px solid lightgrey;
display: flex;
align-items: baseline;
@@ -24,6 +25,9 @@
.sidebar {
ul {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
min-width: 136px;
}
+2 -3
View File
@@ -27,16 +27,15 @@ class LikeButton extends Component {
render() {
const {like, id, postLike, deleteAction, showSignInDialog, currentUser} = this.props;
let {totalLikes: count} = this.props;
const {localPost, localDelete} = this.state;
const liked = (like && like.current_user && !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);
showSignInDialog();
return;
}
if (currentUser.banned) {
@@ -7,7 +7,9 @@ const QuestionBox = ({enable, content}) =>
<i className={`${packagename}-icon material-icons bubble`}>chat_bubble</i>
<i className={`${packagename}-icon material-icons person`}>person</i>
</div>
{content}
<div className={`${packagename}-content`}>
{content}
</div>
</div>;
export default QuestionBox;
@@ -0,0 +1,24 @@
.ignoredUser {
display: table-row;
}
.ignoredUserList {
display: table;
}
.ignoredUser > * {
display: table-cell;
}
.stopListening {
color: #D0011B;
}
.link {
text-decoration: underline;
cursor: pointer;
}
.stopListening:before {
content: '\00a0\00a0\00a0\00a0';
}
@@ -0,0 +1,43 @@
import React, {Component, PropTypes} from 'react';
import styles from './IgnoredUsers.css';
export class IgnoredUsers extends Component {
static propTypes = {
users: PropTypes.arrayOf(PropTypes.shape({
username: PropTypes.string,
id: PropTypes.string,
})).isRequired,
// accepts { id }
stopIgnoring: PropTypes.func.isRequired,
}
render() {
const {users, stopIgnoring} = this.props;
return (
<div>
{
users.length
? <p>Because you ignored these, you do not see their comments.</p>
: null
}
<dl className={styles.ignoredUserList}>
{
users.map(({username, id}) => (
<span className={styles.ignoredUser} key={id}>
<dt key={id}>{ username }</dt>
<dd className={styles.stopListening}>
<a
onClick={() => stopIgnoring({id})}
className={styles.link}>Stop ignoring</a>
</dd>
</span>
))
}
</dl>
</div>
);
}
}
export default IgnoredUsers;
@@ -1,17 +1,13 @@
import React from 'react';
import styles from './NotLoggedIn.css';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import translations from '../translations';
import I18n from 'coral-framework/modules/i18n/i18n';
const lang = new I18n(translations);
export default ({showSignInDialog, requireEmailConfirmation}) => (
export default ({showSignInDialog}) => (
<div className={styles.message}>
<SignInContainer noButton={true} requireEmailConfirmation={requireEmailConfirmation}/>
<div>
<a onClick={() => {
showSignInDialog();
}}>{lang.t('signIn')}</a> {lang.t('toAccess')}
<a onClick={showSignInDialog}>{lang.t('signIn')}</a> {lang.t('toAccess')}
</div>
<div>
{lang.t('fromSettingsPage')}
@@ -1,8 +0,0 @@
.header h1 {
margin: 4px 0;
}
.header h2 {
font-size: 13px;
}
@@ -1,12 +0,0 @@
import React, {PropTypes} from 'react';
import styles from './ProfileHeader.css';
const ProfileHeader = ({username}) => (
<div className={styles.header}>
<h1>{username}</h1>
</div>
);
ProfileHeader.propTypes = {username: PropTypes.string.isRequired};
export default ProfileHeader;
@@ -2,15 +2,19 @@ import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import React, {Component} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import {bindActionCreators} from 'redux';
import {myCommentHistory} from 'coral-framework/graphql/queries';
import {myCommentHistory, myIgnoredUsers} from 'coral-framework/graphql/queries';
import {stopIgnoringUser} from 'coral-framework/graphql/mutations';
import {link} from 'coral-framework/services/PymConnection';
import NotLoggedIn from '../components/NotLoggedIn';
import IgnoredUsers from '../components/IgnoredUsers';
import {Spinner} from 'coral-ui';
import ProfileHeader from '../components/ProfileHeader';
import CommentHistory from 'coral-plugin-history/CommentHistory';
import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth';
import translations from '../translations';
const lang = new I18n(translations);
@@ -31,28 +35,46 @@ class ProfileContainer extends Component {
}
render() {
const {loggedIn, asset, showSignInDialog, data} = this.props;
const {loggedIn, asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props;
const {me} = this.props.data;
if (!loggedIn || !me) {
return <NotLoggedIn showSignInDialog={showSignInDialog} requireEmailConfirmation={asset.settings.requireEmailConfirmation}/>;
}
if (data.loading) {
return <Spinner/>;
}
if (!loggedIn || !me) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
const localProfile = this.props.user.profiles.find(p => p.provider === 'local');
const emailAddress = localProfile && localProfile.id;
return (
<div>
<ProfileHeader username={this.props.userData.username} />
{
<h2>{this.props.userData.username}</h2>
{ emailAddress
? <p>{ emailAddress }</p>
: null
}
// Hiding bio until moderation can get figured out
/* <TabBar onChange={this.handleTabChange} activeTab={activeTab} cStyle='material'>
<Tab>{lang.t('allComments')} ({user.myComments.length})</Tab>
<Tab>{lang.t('profileSettings')}</Tab>
</TabBar>
<TabContent show={activeTab === 0}> */
{
myIgnoredUsersData.myIgnoredUsers && myIgnoredUsersData.myIgnoredUsers.length
? (
<div>
<h3>Ignored users</h3>
<IgnoredUsers
users={myIgnoredUsersData.myIgnoredUsers}
stopIgnoring={stopIgnoringUser}
/>
</div>
)
: null
}
<hr />
<h3>My comments</h3>
{
me.comments.length ?
<CommentHistory
comments={me.comments}
@@ -61,14 +83,7 @@ class ProfileContainer extends Component {
/>
:
<p>{lang.t('userNoComment')}</p>
// Hiding user bio pending effective moderation system.
/* </TabContent>
<TabContent show={activeTab === 1}>
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
</TabContent> */
}
</div>
);
}
@@ -80,12 +95,12 @@ const mapStateToProps = state => ({
auth: state.auth.toJS()
});
const mapDispatchToProps = () => ({
// saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData))
});
const mapDispatchToProps = dispatch =>
bindActionCreators({showSignInDialog, checkLogin}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
myCommentHistory
myCommentHistory,
myIgnoredUsers,
stopIgnoringUser,
)(ProfileContainer);
@@ -10,16 +10,12 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const CreateUsernameDialog = ({open, handleClose, offset, formData, handleSubmitUsername, handleChange, ...props}) => {
const CreateUsernameDialog = ({open, handleClose, formData, handleSubmitUsername, handleChange, ...props}) => {
return (
<Dialog
className={styles.dialogusername}
id="createUsernameDialog"
open={open}
style={{
position: 'relative',
top: offset !== 0 && offset
}}>
open={open}>
<span className={styles.close} onClick={handleClose}>×</span>
<div>
<div className={styles.header}>
@@ -42,6 +38,7 @@ const CreateUsernameDialog = ({open, handleClose, offset, formData, handleSubmit
<div className={styles.saveusername}>
<TextField
id="username"
style={{fontSize: 16}}
type="string"
label={lang.t('createdisplay.username')}
value={formData.username}
@@ -28,10 +28,10 @@ class FakeComment extends React.Component {
author={{'name': username}}/>
<PubDate created_at={created_at} />
<Content body={body} />
<div className="commentActionsLeft">
<div className="commentActionsLeft comment__action-container">
<div className={`${'coral-plugin-likes'}-container`}>
<button className={`${'coral-plugin-likes'}-button`}>
<span className={`${'coral-plugin-likes'}-button-text`}>{lang.t('like')}</span>
<button className={'coral-plugin-likes-button'}>
<span className={'coral-plugin-likes-button-text'}>{lang.t('like')}</span>
<i className={`${'coral-plugin-likes'}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
</button>
@@ -43,16 +43,16 @@ class FakeComment extends React.Component {
banned={false}
/>
</div>
<div className="commentActionsRight">
<div className="commentActionsRight comment__action-container">
<div className="coral-plugin-permalinks-container">
<button className="coral-plugin-permalinks-button">
<span className={`comment__action-button comment__action-button--nowrap ${'coral-plugin-flags'}-button-text`}>{lang.t('permalink.permalink')}</span>
<i className="coral-plugin-permalinks-icon material-icons" aria-hidden={true}>link</i>
{lang.t('permalink.permalink')}
</button>
</div>
<div className={`${'coral-plugin-flags'}-container`}>
<button className={`${'coral-plugin-flags'}-button`}>
<span className={`${'coral-plugin-flags'}-button-text`}>{lang.t('report')}</span>
<span className={`comment__action-button comment__action-button--nowrap ${'coral-plugin-flags'}-button-text`}>{lang.t('report')}</span>
<i className={`${'coral-plugin-flags'}-icon material-icons`}
aria-hidden={true}>flag</i>
</button>
@@ -31,6 +31,7 @@ class ForgotContent extends React.Component {
<input
ref={input => this.emailInput = input}
type="text"
style={{fontSize: 16}}
id="email"
name="email" />
</div>
@@ -6,15 +6,11 @@ import SignInContent from './SignInContent';
import SignUpContent from './SignUpContent';
import ForgotContent from './ForgotContent';
const SignDialog = ({open, view, handleClose, offset, ...props}) => (
const SignDialog = ({open, view, handleClose, ...props}) => (
<Dialog
className={styles.dialog}
id="signInDialog"
open={open}
style={{
position: 'fixed',
top: offset !== 0 && offset
}}>
open={open}>
<span className={styles.close} onClick={handleClose}>×</span>
{view === 'SIGNIN' && <SignInContent {...props} />}
{view === 'SIGNUP' && <SignUpContent {...props} />}
@@ -20,8 +20,8 @@ const SignInContent = ({
}) => {
return (
<div>
<div className={styles.header}>
<div className="coral-sign-in">
<div className={`${styles.header} header`}>
<h1>
{auth.emailVerificationFailure ? lang.t('signIn.emailVerifyCTA') : lang.t('signIn.signIn')}
</h1>
@@ -42,7 +42,7 @@ const SignInContent = ({
{emailVerificationSuccess && <Success />}
</form>
: <div>
<div className={styles.socialConnections}>
<div className={`${styles.socialConnections} social-connections`}>
<Button cStyle="facebook" onClick={fetchSignInFacebook} full>
{lang.t('signIn.facebookSignIn')}
</Button>
@@ -58,6 +58,7 @@ const SignInContent = ({
type="email"
label={lang.t('signIn.email')}
value={formData.email}
style={{fontSize: 16}}
onChange={handleChange}
/>
<TextField
@@ -65,6 +66,7 @@ const SignInContent = ({
type="password"
label={lang.t('signIn.password')}
value={formData.password}
style={{fontSize: 16}}
onChange={handleChange}
/>
<div className={styles.action}>
@@ -80,7 +82,7 @@ const SignInContent = ({
</form>
</div>
}
<div className={styles.footer}>
<div className={`${styles.footer} footer`}>
<span><a onClick={() => changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span>
{lang.t('signIn.needAnAccount')}
@@ -83,6 +83,7 @@ class SignUpContent extends React.Component {
type="email"
label={lang.t('signIn.email')}
value={formData.email}
style={{fontSize: 16}}
showErrors={showErrors}
errorMsg={errors.email}
onChange={handleChange}
@@ -93,6 +94,7 @@ class SignUpContent extends React.Component {
label={lang.t('signIn.username')}
value={formData.username}
showErrors={showErrors}
style={{fontSize: 16}}
errorMsg={errors.username}
onChange={handleChange}
/>
@@ -102,6 +104,7 @@ class SignUpContent extends React.Component {
label={lang.t('signIn.password')}
value={formData.password}
showErrors={showErrors}
style={{fontSize: 16}}
errorMsg={errors.password}
onChange={handleChange}
minLength="8"
@@ -112,6 +115,7 @@ class SignUpContent extends React.Component {
type="password"
label={lang.t('signIn.confirmPassword')}
value={formData.confirmPassword}
style={{fontSize: 16}}
showErrors={showErrors}
errorMsg={errors.confirmPassword}
onChange={handleChange}
@@ -100,12 +100,11 @@ class ChangeUsernameContainer extends Component {
}
render() {
const {loggedIn, auth, offset} = this.props;
const {loggedIn, auth} = this.props;
return (
<div>
<CreateUsernameDialog
open={auth.showCreateUsernameDialog && auth.user.canEditName}
offset={offset}
handleClose={this.handleClose}
loggedIn={loggedIn}
handleSubmitUsername={this.handleSubmitUsername}
@@ -1,7 +1,6 @@
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux';
import SignDialog from '../components/SignDialog';
import Button from 'coral-ui/components/Button';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -13,7 +12,6 @@ import {
changeView,
fetchSignUp,
fetchSignIn,
showSignInDialog,
hideSignInDialog,
fetchSignInFacebook,
fetchSignUpFacebook,
@@ -147,23 +145,18 @@ class SignInContainer extends Component {
handleSignIn(e) {
e.preventDefault();
this.props.fetchSignIn(this.state.formData)
.then(this.props.refetch);
this.props.fetchSignIn(this.state.formData);
}
render() {
const {auth, showSignInDialog, noButton, offset, requireEmailConfirmation} = this.props;
const {auth, requireEmailConfirmation} = this.props;
const {emailVerificationLoading, emailVerificationSuccess} = auth;
return (
<div>
{!noButton && <Button id='coralSignInButton' onClick={showSignInDialog} full>
Sign in to comment
</Button>}
<SignDialog
open={auth.showSignInDialog}
open={true}
view={auth.view}
offset={offset}
emailVerificationEnabled={requireEmailConfirmation}
emailVerificationLoading={emailVerificationLoading}
emailVerificationSuccess={emailVerificationSuccess}
@@ -189,7 +182,6 @@ const mapDispatchToProps = dispatch => ({
fetchSignUpFacebook: () => dispatch(fetchSignUpFacebook()),
fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)),
requestConfirmEmail: (email, url) => dispatch(requestConfirmEmail(email, url)),
showSignInDialog: () => dispatch(showSignInDialog()),
changeView: view => dispatch(changeView(view)),
handleClose: () => dispatch(hideSignInDialog()),
invalidForm: error => dispatch(invalidForm(error)),

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