Compare commits

..
227 Commits
Author SHA1 Message Date
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
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
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
Wyatt Johnson 191026ecf2 Merge branch 'master' into metadata 2017-04-12 10:37:12 -06: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
Kim Gardner 143ce795c3 Merge branch 'master' into 143520139-permalink-show-all 2017-04-12 08:14:39 -04: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 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
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
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
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
Wyatt Johnson 3a3b89b03b Fixed bugs with plugin install 2017-04-11 14:59:02 -06: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
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
Kim Gardner b2ee1f0c57 Merge branch 'master' into fix-respect-button-bugs 2017-04-11 10:48:41 -04: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
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
116 changed files with 2154 additions and 535 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
+13 -13
View File
@@ -1,20 +1,20 @@
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
+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).
+10 -5
View File
@@ -5,16 +5,21 @@ 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 && \
yarn cache clean
+130 -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,10 +25,46 @@ 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 will also traverse into local plugin folders and install their
dependancies. _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._
## Plugin Dependencies
From your plugins you may import any component of server code relative to the
project root. An example could be:
```js
const cache = require('services/cache');
```
You may also include additional external depenancies 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.
## Server Plugins
### Specification
@@ -170,6 +213,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);
/**
+297
View File
@@ -0,0 +1,297 @@
#!/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 ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
if (!dryRun) {
let args = [
'add',
...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
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 /> }
@@ -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>
);
+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'));
+5 -1
View File
@@ -19,6 +19,7 @@ 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';
import styles from './Comment.css';
@@ -157,6 +158,7 @@ class Comment extends React.Component {
? <TagLabel><BestIndicator /></TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<Slot fill="commentInfoBar" commentId={comment.id} />
<Content body={comment.body} />
<div className="commentActionsLeft comment__action-container">
@@ -187,6 +189,7 @@ class Comment extends React.Component {
removeBest={removeBestTag} />
</IfUserCanModifyBest>
</ActionButton>
<Slot fill="commentDetail" commentId={comment.id} />
</div>
<div className="commentActionsRight comment__action-container">
<ActionButton>
@@ -241,7 +244,8 @@ class Comment extends React.Component {
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply} />;
comment={reply}
/>;
})
}
{
+27 -10
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';
@@ -36,14 +36,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();
}
@@ -78,10 +86,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(() => {
@@ -116,7 +126,7 @@ class Embed extends Component {
render () {
const {activeTab} = this.state;
const {closedAt, countCache = {}} = this.props.asset;
const {loading, asset, refetch, comment} = this.props.data;
const {asset, refetch, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
@@ -126,11 +136,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 />;
}
@@ -143,7 +159,8 @@ class Embed extends Component {
<div style={expandForLogin}>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.totalCommentCount}/></Tab>
<Tab><Count count={asset.totalCommentCount}/>
</Tab>
<Tab>{lang.t('MY_COMMENTS')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
@@ -258,7 +275,7 @@ class Embed extends Component {
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={countCache[asset.id] > asset.comments.length}
moreComments={hasOlderComments}
loadMore={this.props.loadMore} />
</div>
}
@@ -312,5 +329,5 @@ export default compose(
addCommentTag,
removeCommentTag,
deleteAction,
queryStream
queryStream,
)(Embed);
+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'))}
+5 -2
View File
@@ -41,7 +41,8 @@ class Stream extends React.Component {
deleteAction,
showSignInDialog,
addCommentTag,
removeCommentTag
removeCommentTag,
pluginProps
} = this.props;
return (
@@ -67,7 +68,9 @@ class Stream extends React.Component {
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment} />
comment={comment}
pluginProps={pluginProps}
/>
)
}
</div>
+37 -12
View File
@@ -11,7 +11,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 +80,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 +92,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 +129,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 +146,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 +161,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 +280,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 {
+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);
+22 -1
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,6 +7,20 @@ 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
}
}
`;
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});
@@ -52,6 +68,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
fetchMe();
})
.catch(error => {
if (error.metadata) {
@@ -104,6 +121,7 @@ export const facebookCallback = (err, data) => dispatch => {
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(showCreateUsernameDialog());
fetchMe();
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -151,7 +169,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,4 +1,4 @@
import {pym} from '../../coral-framework';
import pym from '../services/PymConnection';
export const addNotification = (notifType, text) => {
pym.sendMessage('coral-alert', `${notifType}|${text}`);
+19
View File
@@ -0,0 +1,19 @@
import React, {Component} from 'react';
import {getSlotElements} from 'coral-framework/helpers/plugins';
class Slot extends Component {
render() {
const {fill, ...rest} = this.props;
return (
<div>
{getSlotElements(fill, rest)}
</div>
);
}
}
Slot.propTypes = {
fill: React.PropTypes.string
};
export default Slot;
@@ -30,13 +30,13 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => {
limit,
sort
},
updateQuery: (oldData, {fetchMoreResult:{data}}) => {
updateQuery: (oldData, {fetchMoreResult:{asset}}) => {
return {
...oldData,
asset: {
...oldData.asset,
commentCount: data.asset.commentCount
commentCount: asset.commentCount
}
};
}
@@ -54,8 +54,7 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s
asset_id, // the id of the asset we're currently on
sort // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
},
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
@@ -1,6 +1,6 @@
#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!) {
# 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
@@ -38,6 +38,9 @@ query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comm
}
commentCount
totalCommentCount
lastComment {
id
}
comments(limit: 10) {
...commentView
replyCount
+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}));
}
+8 -7
View File
@@ -1,15 +1,16 @@
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';
import Slot from './components/Slot';
export {
// TODO (bc): Deprecate old actions. Spreading actions is now needed.
export default {
pym,
Slot,
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(',')}
];
`;
};
+2
View File
@@ -1,9 +1,11 @@
import auth from './auth';
import user from './user';
import asset from './asset';
import {pluginReducers} from '../helpers/plugins';
export default {
auth,
user,
asset,
...pluginReducers
};
+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;
+12 -2
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) {
+7
View File
@@ -0,0 +1,7 @@
/**
* getActionSummary
* retrieves the action summary based on the type and the comment
*/
export const getActionSummary = (type, comment) =>
comment.action_summaries.filter(a => a.__typename === type)[0];
@@ -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;
@@ -8,7 +8,6 @@ import {myCommentHistory} from 'coral-framework/graphql/queries';
import {link} from 'coral-framework/services/PymConnection';
import NotLoggedIn from '../components/NotLoggedIn';
import {Spinner} from 'coral-ui';
import ProfileHeader from '../components/ProfileHeader';
import CommentHistory from 'coral-plugin-history/CommentHistory';
import translations from '../translations';
@@ -44,7 +43,6 @@ class ProfileContainer extends Component {
return (
<div>
<ProfileHeader username={this.props.userData.username} />
{
// Hiding bio until moderation can get figured out
@@ -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>
+2 -2
View File
@@ -25,14 +25,14 @@
"emailInUse": "Email address already in use",
"emailORusernameInUse": "Email address or Username already in use",
"requiredField": "This field is required",
"passwordsDontMatch": "Passwords don\"t match.",
"passwordsDontMatch": "Passwords don't match.",
"specialCharacters": "Usernames can contain letters, numbers and _ only",
"checkTheForm": "Invalid Form. Please, check the fields"
},
"createdisplay": {
"writeyourusername": "Edit your username",
"yourusername": "Your username appears on every comment you post.",
"ifyoudontchangeyourname": "If you don\"t change your username at this step, your Facebook display name will appear alongside of all your comments.",
"ifyoudontchangeyourname": "If you don't change your username at this step, your Facebook display name will appear alongside of all your comments.",
"username": "Username",
"continue": "Continue with the same Facebook username",
"save": "Save",
+1 -1
View File
@@ -1,7 +1,7 @@
const loaders = require('./loaders');
const mutators = require('./mutators');
const plugins = require('../plugins');
const plugins = require('../services/plugins');
const debug = require('debug')('talk:graph:context');
/**
+31
View File
@@ -0,0 +1,31 @@
const errors = require('../../errors');
const {Error: {ValidationError}} = require('mongoose');
/**
* Wraps up a promise to return an object with the resolution of the promise
* keyed at `key` or an error caught at `errors`.
*/
const wrapResponse = (key) => (promise) => {
return promise.then((value) => {
let res = {};
if (key) {
res[key] = value;
}
return res;
}).catch((err) => {
if (err instanceof errors.APIError) {
return {
errors: [err]
};
} else if (err instanceof ValidationError) {
// TODO: wrap this with one of our internal errors.
throw err;
}
throw err;
});
};
module.exports = wrapResponse;
+83 -3
View File
@@ -1,5 +1,9 @@
const {forEachField} = require('graphql-tools');
const {
GraphQLObjectType,
GraphQLInterfaceType
} = require('graphql');
const debug = require('debug')('talk:graph:schema');
const Joi = require('joi');
/**
* XXX taken from graphql-js: src/execution/execute.js, because that function
@@ -22,6 +26,33 @@ const defaultResolveFn = (source, args, context, {fieldName}) => {
}
};
// This function is pretty much copied verbatim from the graphql-tools repo:
// https://github.com/apollographql/graphql-tools/blob/b12973c86e00be209d04af0184780998056051c4/src/schemaGenerator.ts#L180-L194
// With the small alteration that we look for the `resolveType` function on the
// schema so we can wrap post hooks around it to provide additional resolve
// points.
const forEachField = (schema, fn) => {
const typeMap = schema.getTypeMap();
Object.keys(typeMap).forEach((typeName) => {
const type = typeMap[typeName];
if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) {
// Here we capture the change to extract the resolve type. We pass this
// with the `isResolveType = true` to introduce the specific beheviour.
if ('resolveType' in type) {
fn(type, typeName, '__resolveType', true);
}
const fields = type.getFields();
Object.keys(fields).forEach((fieldName) => {
const field = fields[fieldName];
fn(field, typeName, fieldName);
});
}
});
};
/**
* Decorates the schema with pre and post hooks as provided by the Plugin
* Manager.
@@ -29,7 +60,7 @@ const defaultResolveFn = (source, args, context, {fieldName}) => {
* @param {Array} hooks hooks to apply to the schema
* @return {void}
*/
const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName) => {
const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName, isResolveType = false) => {
// Pull out the pre/post hooks from the available hooks.
const {
@@ -52,6 +83,8 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
Object.keys(hooks).forEach((hook) => {
switch (hook) {
case 'pre':
Joi.assert(hooks.pre, Joi.func().maxArity(4));
debug(`adding pre hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.pre !== 'function') {
@@ -61,6 +94,8 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
acc.pre.push(hooks.pre);
break;
case 'post':
Joi.assert(hooks.pre, Joi.func().maxArity(5));
debug(`adding post hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.post !== 'function') {
@@ -85,6 +120,51 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
return;
}
// If this is a resolve type, we need to do some specific things to handle
// this type of field.
if (isResolveType) {
// Warn if we have any pre hooks.
if (pre.length !== 0) {
throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`);
}
// This only needs to do something if post hooks are defined.
if (post.length === 0) {
return;
}
// Ensure it matches the format we expect.
Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`);
// Cache the original resolverType function.
let resolveType = field.resolveType;
// Return the function to handle the resolveType hooks.
field.resolveType = (obj, context, info) => {
let type = resolveType(obj, context, info);
// Only if a previous resolver was unable to resolve the field type do we
// progress to the hooks (in order!) to resolve the field name until we
// have resolved it.
if (typeof type !== 'undefined' && type != null) {
return type;
}
// We will walk through the post hooks until we find the right one. This
// follows what redux does to combine existing reducers.
for (let i = 0; i < post.length; i++) {
let resolveType = post[i];
type = resolveType(obj, context, info);
if (typeof type !== 'undefined' && type != null) {
return type;
}
}
};
return;
}
// Cache the original resolve function, this emulates the beheviour found in
// graphql-tools: https://github.com/apollographql/graphql-tools/blob/6e9cc124b10d673448386041e6c3d058bc205a02/src/schemaGenerator.ts#L423-L425
let resolve = field.resolve;
@@ -102,7 +182,7 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
await Promise.all(pre.map((pre) => pre(obj, args, context, info)));
// Resolve the field.
let result = resolve(obj, args, context, info);
let result = await resolve(obj, args, context, info);
// Insure all post hooks after we've resolved the field with the result
// passed in as the fifth argument.
+1 -1
View File
@@ -8,7 +8,7 @@ const Metrics = require('./metrics');
const Settings = require('./settings');
const Users = require('./users');
const plugins = require('../../plugins');
const plugins = require('../../services/plugins');
let loaders = [
+1 -1
View File
@@ -5,7 +5,7 @@ const Comment = require('./comment');
const Action = require('./action');
const User = require('./user');
const plugins = require('../../plugins');
const plugins = require('../../services/plugins');
let mutators = [
+7
View File
@@ -1,4 +1,11 @@
const Asset = {
lastComment({id}, _, {loaders: {Comments}}) {
return Comments.getByQuery({
asset_id: id,
limit: 1,
parent_id: null
}).then(data => data[0]);
},
recentComments({id}, _, {loaders: {Comments}}) {
return Comments.genRecentComments.load(id);
},
+3 -3
View File
@@ -20,7 +20,7 @@ const UserError = require('./user_error');
const User = require('./user');
const ValidationUserError = require('./validation_user_error');
const plugins = require('../../plugins');
const plugins = require('../../services/plugins');
// Provide the core resolvers.
let resolvers = {
@@ -49,10 +49,10 @@ let resolvers = {
* plugin based ones. This allows plugins to extend existing resolvers as well
* as provide new ones.
*/
resolvers = plugins.get('server', 'resolvers').reduce((resolvers, {plugin}) => {
resolvers = plugins.get('server', 'resolvers').reduce((acc, {plugin, resolvers}) => {
debug(`added plugin '${plugin.name}'`);
return _.merge(resolvers, plugin.resolvers);
return _.merge(acc, resolvers);
}, resolvers);
module.exports = resolvers;
+1 -28
View File
@@ -1,33 +1,6 @@
const {Error: {ValidationError}} = require('mongoose');
const errors = require('../../errors');
const wrapResponse = require('../helpers/response');
const CommentsService = require('../../services/comments');
/**
* Wraps up a promise to return an object with the resolution of the promise
* keyed at `key` or an error caught at `errors`.
*/
const wrapResponse = (key) => (promise) => {
return promise.then((value) => {
let res = {};
if (key) {
res[key] = value;
}
return res;
}).catch((err) => {
if (err instanceof errors.APIError) {
return {
errors: [err]
};
} else if (err instanceof ValidationError) {
// TODO: wrap this with one of our internal errors.
throw err;
}
throw err;
});
};
const RootMutation = {
createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.create({asset_id, parent_id, body}));
-5
View File
@@ -8,11 +8,6 @@ const RootQuery = {
},
asset(_, query, {loaders: {Assets}}) {
if (query.id) {
// TODO: we may not always have a comment stream here, therefore, when we
// load it, we may also need to create with the url. This may also have to
// move the logic over to the mutators function as an upsert operation
// possibly.
return Assets.getByID.load(query.id);
}
+1 -1
View File
@@ -2,7 +2,7 @@ const {makeExecutableSchema} = require('graphql-tools');
const {maskErrors} = require('graphql-errors');
const {decorateWithHooks} = require('./hooks');
const plugins = require('../plugins');
const plugins = require('../services/plugins');
const resolvers = require('./resolvers');
const typeDefs = require('./typeDefs');
+3
View File
@@ -413,6 +413,9 @@ type Asset {
# The URL that the asset is located on.
url: String
# Returns last comment
lastComment: Comment
# Returns recent comments
recentComments: [Comment]
+1 -1
View File
@@ -6,7 +6,7 @@ const fs = require('fs');
const path = require('path');
const {mergeStrings} = require('gql-merge');
const debug = require('debug')('talk:graph:typeDefs');
const plugins = require('../plugins');
const plugins = require('../services/plugins');
/**
* Plugin support requires us to merge the type definitions from the loaded
+6
View File
@@ -46,6 +46,12 @@ const AssetSchema = new Schema({
type: Schema.Types.Mixed,
default: null
},
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object
}
}, {
versionKey: false,
timestamps: {
+7 -1
View File
@@ -75,7 +75,13 @@ const CommentSchema = new Schema({
default: 'NONE'
},
tags: [TagSchema],
parent_id: String
parent_id: String,
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object
}
}, {
timestamps: {
createdAt: 'created_at',
+17 -7
View File
@@ -1,12 +1,13 @@
{
"name": "talk",
"version": "1.4.0",
"description": "A commenting platform from The Coral Project. https://coralproject.net",
"version": "1.5.0",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"scripts": {
"postinstall": "./bin/cli plugins reconcile --skip-remote",
"start": "./bin/cli serve --jobs",
"dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"",
"build": "NODE_ENV=production webpack --progress -p --config webpack.config.js --bail",
"build": "NODE_ENV=production webpack -p --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint bin/* . --fix",
@@ -16,7 +17,7 @@
"e2e": "NODE_ENV=test nightwatch",
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
"heroku-postbuild": "yarn build"
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build"
},
"config": {
"pre-git": {
@@ -49,11 +50,14 @@
},
"homepage": "https://github.com/coralproject/talk#readme",
"dependencies": {
"app-module-path": "^2.2.0",
"bcrypt": "^1.0.2",
"body-parser": "^1.17.1",
"cli-table": "^0.3.1",
"colors": "^1.1.2",
"commander": "^2.9.0",
"connect-redis": "^3.1.0",
"cross-spawn": "^5.1.0",
"csurf": "^1.9.0",
"dataloader": "^1.3.0",
"debug": "^2.6.3",
@@ -70,6 +74,7 @@
"graphql-tools": "^0.9.0",
"helmet": "^3.5.0",
"inquirer": "^3.0.6",
"joi": "^10.4.1",
"jsonwebtoken": "^7.3.0",
"kue": "^0.11.5",
"linkify-it": "^2.0.3",
@@ -80,21 +85,24 @@
"mongoose": "^4.9.1",
"morgan": "^1.8.1",
"natural": "^0.4.0",
"node-emoji": "^1.5.1",
"node-fetch": "^1.6.3",
"nodemailer": "^2.6.4",
"parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"passport-local": "^1.0.0",
"react-apollo": "^0.10.0",
"react-apollo": "^1.0.0",
"react-recaptcha": "^2.2.6",
"redis": "^2.7.1",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"simplemde": "^1.11.2",
"uuid": "^2.0.3"
},
"devDependencies": {
"apollo-client": "^0.8.3",
"apollo-client": "^1.0.0",
"autoprefixer": "^6.5.2",
"babel-cli": "^6.24.0",
"babel-core": "^6.24.0",
"babel-eslint": "^7.2.1",
"babel-jest": "^19.0.0",
@@ -108,10 +116,12 @@
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.0",
"babel-preset-react": "^6.23.0",
"babel-preset-stage-0": "^6.16.0",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"chai-http": "^3.0.0",
"common-tags": "^1.4.0",
"copy-webpack-plugin": "^4.0.0",
"css-loader": "^0.27.3",
"dialog-polyfill": "^0.4.4",
+9
View File
@@ -0,0 +1,9 @@
{
"server": [
"coral-plugin-respect",
"coral-plugin-facebook-auth"
],
"client": [
"coral-plugin-respect"
]
}
+1
View File
@@ -0,0 +1 @@
module.exports = JSON.parse(process.env.TALK_PLUGINS_JSON);
+161 -11
View File
@@ -1,5 +1,12 @@
const fs = require('fs');
const path = require('path');
const resolve = require('resolve');
const debug = require('debug')('talk:plugins');
const Joi = require('joi');
const amp = require('app-module-path');
// Add the current path to the module root.
amp.addPath(__dirname);
let plugins = {};
@@ -7,26 +14,151 @@ let plugins = {};
// file isn't loaded, but continuing. Else, like a parsing error, throw it and
// crash the program.
try {
plugins = JSON.parse(fs.readFileSync(path.join(__dirname, 'plugins.json'), 'utf8'));
let envPlugins = path.join(__dirname, 'plugins.env.js');
let customPlugins = path.join(__dirname, 'plugins.json');
let defaultPlugins = path.join(__dirname, 'plugins.default.json');
if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) {
debug('Now using TALK_PLUGINS_JSON environment variable for plugins');
plugins = require(envPlugins);
} else if (fs.existsSync(customPlugins)) {
debug(`Now using ${customPlugins} for plugins`);
plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8'));
} else {
debug(`Now using ${defaultPlugins} for plugins`);
plugins = JSON.parse(fs.readFileSync(defaultPlugins, 'utf8'));
}
} catch (err) {
if (err.code === 'ENOENT') {
console.error('plugins.json not found, plugins will not be active');
console.error('plugins.json and plugins.default.json not found, plugins will not be active');
} else {
throw err;
}
}
/**
* All the hooks from plugins must match the schema defined here.
*/
const hookSchemas = {
passport: Joi.func().arity(1),
router: Joi.func().arity(1),
context: Joi.object().pattern(/\w/, Joi.func().maxArity(1)),
hooks: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.object({
pre: Joi.func(),
post: Joi.func()
}))),
loaders: Joi.func().maxArity(1),
mutators: Joi.func().maxArity(1),
resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.func())),
typeDefs: Joi.string()
};
/**
* isInternal checks to see if a given plugin is internal, and returns true
* if it is.
*
* @param {String} name
* @returns {Boolean}
*/
function isInternal(name) {
const internalPluginPath = path.join(__dirname, 'plugins', name);
// Check to see if this plugin exists internally, because if it doesn't, it is
// external.
return fs.existsSync(internalPluginPath);
}
/**
* Returns the plugin path for the given plugin name.
*
* @param {any} name
* @returns
*/
function pluginPath(name) {
if (isInternal(name)) {
return path.join(__dirname, 'plugins', name);
}
try {
return resolve.sync(name, {basedir: process.cwd()});
} catch (e) {
return undefined;
}
}
/**
* Itterates over the plugins and gets the plugin path's, version, and name.
*
* @param {Array<Object|String>} plugins
* @returns {Array<Object>}
*/
function itteratePlugins(plugins) {
return plugins.map((p) => {
let plugin = {};
// This checks to see if the structure for this entry is an object:
//
// {"people": "^1.2.0"}
//
// otherwise it's checked whether it matches the local version:
//
// "people"
//
if (typeof p === 'object') {
plugin.name = Object.keys(p).find((name) => name !== null);
plugin.version = p[plugin.name];
} else if (typeof p === 'string') {
plugin.name = p;
plugin.version = `file:./plugins/${plugin.name}`;
} else {
throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof p}`);
}
// Get the path for the plugin.
plugin.path = pluginPath(plugin.name);
return plugin;
});
}
// Add each plugin folder to the allowed import path so that they can import our
// internal dependancies.
Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((plugin) => {
// The plugin may be remote, and therefore not installed. We check here if the
// plugin path is available before trying to monkeypatch it's require path.
if (plugin.path) {
amp.enableForDir(path.dirname(plugin.path));
}
}));
/**
* Stores a reference to a section for a section of Plugins.
*/
class PluginSection {
constructor(plugin_names) {
this.plugins = plugin_names.map((plugin_name) => {
let plugin = require(`./plugins/${plugin_name}`);
constructor(plugins) {
this.plugins = itteratePlugins(plugins).map((plugin) => {
if (typeof plugin.path === 'undefined') {
throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`);
}
// Ensure we have a default plugin name, but allow the name to be
// overrided by the plugin.
plugin.name = plugin.name || plugin_name;
try {
plugin.module = require(plugin.path);
} catch (e) {
if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(plugin.name)) {
console.error(new Error(`plugin '${plugin.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`));
throw e;
}
console.error(new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`));
throw e;
}
if (isInternal(plugin.name)) {
debug(`loading internal plugin '${plugin.name}' from '${plugin.path}'`);
} else {
debug(`loading external plugin '${plugin.name}' from '${plugin.path}'`);
}
return plugin;
});
@@ -38,8 +170,20 @@ class PluginSection {
*/
hook(hook) {
return this.plugins
.filter((plugin) => hook in plugin)
.map((plugin) => ({plugin, [hook]: plugin[hook]}));
.filter(({module}) => hook in module)
.filter((plugin) => {
// Validate the hook.
if (hook in hookSchemas) {
Joi.assert(plugin.module[hook], hookSchemas[hook], `Plugin '${plugin.name}' failed validation for the '${hook}' hook`);
}
return true;
})
.map((plugin) => ({
plugin,
[hook]: plugin.module[hook]
}));
}
}
@@ -78,4 +222,10 @@ class PluginManager {
}
}
module.exports = new PluginManager(plugins);
module.exports = {
plugins,
PluginManager,
isInternal,
pluginPath,
itteratePlugins
};
@@ -0,0 +1,24 @@
# coral-plugin-facebook-auth
This plugin provides facebook authentication support for Talk.
## Configuration
This plugin looks for the following configuration from the environment:
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
Login enabled app.
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
Facebook Login enabled app.
### License
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
@@ -0,0 +1,7 @@
const passport = require('./server/passport');
const router = require('./server/router');
module.exports = {
passport,
router
};
@@ -0,0 +1,9 @@
{
"name": "coral-plugin-facebook-auth",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"passport-facebook": "^2.1.1"
}
}
@@ -0,0 +1,27 @@
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin} = require('services/passport');
module.exports = (passport) => {
if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && process.env.TALK_ROOT_URL) {
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);
}));
} else if (process.env.NODE_ENV !== 'test') {
throw new Error('Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL');
}
};
@@ -0,0 +1,20 @@
module.exports = (router) => {
const {passport, HandleAuthPopupCallback} = require('services/passport');
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/api/v1/auth/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('/api/v1/auth/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);
});
};
@@ -0,0 +1,34 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
oauth@0.9.x:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
passport-facebook@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/passport-facebook/-/passport-facebook-2.1.1.tgz#c39d0b52ae4d59163245a4e21a7b9b6321303311"
dependencies:
passport-oauth2 "1.x.x"
passport-oauth2@1.x.x:
version "1.4.0"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad"
dependencies:
oauth "0.9.x"
passport-strategy "1.x.x"
uid2 "0.0.x"
utils-merge "1.x.x"
passport-strategy@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
uid2@0.0.x:
version "0.0.3"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
utils-merge@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -0,0 +1,6 @@
import React from 'react';
import cn from 'classnames';
export default ({className}) => (
<i className={cn('fa', 'fa-handshake-o', className)} aria-hidden="true"/>
);
@@ -0,0 +1,67 @@
import React, {Component} from 'react';
import styles from './style.css';
import Icon from './Icon';
import {I18n} from 'coral-framework';
import cn from 'classnames';
import translations from '../translations.json';
const lang = new I18n(translations);
class RespectButton extends Component {
handleClick = () => {
const {postRespect, showSignInDialog, deleteAction, commentId} = this.props;
const {me, comment} = this.props.data;
const respect = comment.action_summaries[0];
const respected = (respect && respect.current_user);
// If the current user does not exist, trigger sign in dialog.
if (!me) {
const offset = document.getElementById(`c_${commentId}`).getBoundingClientRect().top - 75;
showSignInDialog(offset);
return;
}
// If the current user is banned, do nothing.
if (me.status === 'BANNED') {
return;
}
if (!respected) {
postRespect({
item_id: commentId,
item_type: 'COMMENTS'
});
} else {
deleteAction(respect.current_user.id);
}
}
render() {
const {comment} = this.props.data;
const respect = comment && comment.action_summaries && comment.action_summaries[0];
const respected = respect && respect.current_user;
let count = respect ? respect.count : 0;
return (
<div className={styles.respect}>
<button
className={cn(styles.button, {[styles.respected]: respected})}
onClick={this.handleClick} >
<span>{lang.t(respected ? 'respected' : 'respect')}</span>
<Icon className={cn(styles.icon, {[styles.respected]: respected})} />
{count > 0 && count}
</button>
</div>
);
}
}
RespectButton.propTypes = {
data: React.PropTypes.object.isRequired
};
export default RespectButton;
@@ -0,0 +1,30 @@
.respect {
display: inline-block;
}
.button {
color: #2a2a2a;
margin: 5px 10px 5px 0px;
background: none;
padding: 0px;
border: none;
font-size: inherit;
&:hover {
color: #767676;
cursor: pointer;
}
&.respected {
color: #c98211;
&:hover {
color: #e59614;
cursor: pointer;
}
}
}
.icon {
padding: 0 5px;
}
@@ -0,0 +1,141 @@
import {compose, gql, graphql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import get from 'lodash/get';
import {showSignInDialog} from 'coral-framework/actions/auth';
import RespectButton from '../components/RespectButton';
// TODO: use `update` instead of `updateQueries` for optimistic mutations.
// See https://dev-blog.apollodata.com/apollo-clients-new-imperative-store-api-6cb69318a1e3
// and https://github.com/apollographql/apollo-client/issues/1224
export const RESPECT_QUERY = gql`
query RespectQuery($commentId: ID!) {
comment(id: $commentId) {
id
action_summaries {
... on RespectActionSummary {
count
current_user {
id
}
}
}
}
me {
status
}
}
`;
const withQuery = graphql(RESPECT_QUERY);
const withDeleteAction = graphql(gql`
mutation deleteAction($id: ID!) {
deleteAction(id:$id) {
errors {
translation_key
}
}
}
`, {
props: ({mutate}) => ({
deleteAction: (id) => {
return mutate({
variables: {id},
optimisticResponse: {
deleteAction: {
__typename: 'DeleteActionResponse',
errors: null,
}
},
updateQueries: {
RespectQuery: (prev) => {
if (get(prev, 'comment.action_summaries.0.current_user.id') !== id) {
return prev;
}
const next = {
...prev,
comment: {
...prev.comment,
action_summaries: [{
__typename: 'RespectActionSummary',
count: prev.comment.action_summaries[0].count - 1,
current_user: null,
}],
}
};
return next;
},
},
});
},
}),
});
const withPostRespect = graphql(gql`
mutation createRespect($respect: CreateRespectInput!) {
createRespect(respect: $respect) {
respect {
id
}
errors {
translation_key
}
}
}
`, {
props: ({mutate}) => ({
postRespect: (respect) => {
return mutate({
variables: {respect},
optimisticResponse: {
createRespect: {
__typename: 'CreateRespectResponse',
errors: null,
respect: {
__typename: 'RespectAction',
id: 'pending',
},
}
},
updateQueries: {
RespectQuery: (prev, {mutationResult, queryVariables}) => {
if (queryVariables.commentId !== respect.item_id ||
get(prev, 'comment.action_summaries.0.current_user')) {
return prev;
}
const respectAction = mutationResult.data.createRespect.respect;
const count = prev.comment.action_summaries[0] ? prev.comment.action_summaries[0].count : 0;
const next = {
...prev,
comment: {
...prev.comment,
action_summaries: [{
__typename: 'RespectActionSummary',
count: count + 1,
current_user: respectAction,
}],
}
};
return next;
},
},
});
},
}),
});
const mapDispatchToProps = dispatch =>
bindActionCreators({showSignInDialog}, dispatch);
const enhance = compose(
connect(null, mapDispatchToProps),
withDeleteAction,
withPostRespect,
withQuery,
);
export default enhance(RespectButton);
@@ -0,0 +1,6 @@
import RespectButton from './containers/RespectButton';
export default {
slots: {
commentDetail: [RespectButton],
}
};
@@ -0,0 +1,10 @@
{
"en": {
"respect": "Respect",
"respected": "Respected"
},
"es": {
"respect": "Respeto",
"respected": "Respetado"
}
}
+36
View File
@@ -0,0 +1,36 @@
const {readFileSync} = require('fs');
const path = require('path');
const wrapResponse = require('../../graph/helpers/response');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
resolvers: {
RootMutation: {
createRespect(_, {respect: {item_id, item_type}}, {mutators: {Action}}) {
return wrapResponse('respect')(Action.create({item_id, item_type, action_type: 'RESPECT'}));
}
}
},
hooks: {
Action: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'RESPECT':
return 'RespectAction';
}
}
}
},
ActionSummary: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'RESPECT':
return 'RespectActionSummary';
}
}
}
}
}
};
@@ -0,0 +1,54 @@
enum ACTION_TYPE {
# Represents a Respect.
RESPECT
}
input CreateRespectInput {
# The item's id for which we are to create a respect.
item_id: ID!
# The type of the item for which we are to create the respect.
item_type: ACTION_ITEM_TYPE!
}
# RespectAction is used by users who "respect" a specific entity.
type RespectAction implements Action {
# The ID of the action.
id: ID!
# The author of the action.
user: User
# The time when the Action was updated.
updated_at: Date
# The time when the Action was created.
created_at: Date
}
type RespectActionSummary implements ActionSummary {
# The count of actions with this group.
count: Int
# The current user's action.
current_user: RespectAction
}
type CreateRespectResponse implements Response {
# The respect that was created.
respect: RespectAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
}
type RootMutation {
# Creates a respect on an entity.
createRespect(respect: CreateRespectInput!): CreateRespectResponse
}
+1 -65
View File
@@ -1,7 +1,6 @@
const express = require('express');
const passport = require('../../../services/passport');
const {passport, HandleAuthCallback} = require('../../../services/passport');
const authorization = require('../../../middleware/authorization');
const errors = require('../../../errors');
const router = express.Router();
@@ -34,53 +33,6 @@ router.delete('/', authorization.needed(), (req, res) => {
// PASSPORT ROUTES
//==============================================================================
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return next(errors.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user});
});
};
/**
* Returns the response to the login attempt via a popup callback with some JS.
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
};
/**
* Local auth endpoint, will recieve a email and password
*/
@@ -90,20 +42,4 @@ router.post('/local', (req, res, next) => {
passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next);
});
/**
* 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);
});
module.exports = router;
+2
View File
@@ -14,6 +14,7 @@ router.get('/id/:asset_id', (req, res, next) => {
}
res.render('article', {
title: asset.title,
asset_id: asset.id,
asset_url: asset.url,
body: '',
basePath: '/client/embed/stream'
@@ -26,6 +27,7 @@ router.get('/title/:asset_title', (req, res) => {
return res.render('article', {
title: req.params.asset_title.split('-').join(' '),
asset_url: '',
asset_id: null,
body: body,
basePath: '/client/embed/stream'
});
+11
View File
@@ -1,5 +1,7 @@
const express = require('express');
const path = require('path');
const plugins = require('../services/plugins');
const debug = require('debug')('talk:routes');
const router = express.Router();
@@ -16,10 +18,19 @@ if (process.env.NODE_ENV !== 'production') {
return res.render('article', {
title: 'Coral Talk',
asset_url: '',
asset_id: '',
body: '',
basePath: '/client/embed/stream'
});
});
}
// Inject server route plugins.
plugins.get('server', 'router').forEach((plugin) => {
debug(`added plugin '${plugin.plugin.name}'`);
// Pass the root router to the plugin to mount it's routes.
plugin.router(router);
});
module.exports = router;
+24 -11
View File
@@ -2,9 +2,7 @@
set -e
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
# Sourced from https://segment.com/blog/ci-at-segment/
# Inspired by https://segment.com/blog/ci-at-segment/
deploy_tag() {
# Find our individual versions from the tags
@@ -28,6 +26,7 @@ deploy_tag() {
do
echo "==> tagging $version"
docker tag coralproject/talk:latest coralproject/talk:$version
docker tag coralproject/talk:latest-onbuild coralproject/talk:$version-onbuild
done
# Push each of the tags to docker hub, including latest
@@ -35,21 +34,35 @@ deploy_tag() {
do
echo "==> pushing $version"
docker push coralproject/talk:$version
docker push coralproject/talk:$version-onbuild
done
}
deploy_latest() {
echo "==> pushing latest"
docker push coralproject/talk:latest
docker push coralproject/talk:latest-onbuild
}
# build the repo
docker build -t coralproject/talk .
# build the repo, including the onbuild tagged versions.
docker build -t coralproject/talk:latest -f Dockerfile .
docker build -t coralproject/talk:latest-onbuild -f Dockerfile.onbuild .
# deploy based on the env
if [ -n "$CIRCLE_TAG" ]
if [ "$1" = "deploy" ]
then
deploy_tag
else
deploy_latest
fi
if [[ -n "$DOCKER_EMAIL" && -n "$DOCKER_USER" && -n "$DOCKER_PASS" ]]
then
# Log the Docker Daemon in
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
fi
# deploy based on the env
if [ -n "$CIRCLE_TAG" ]
then
deploy_tag
else
deploy_latest
fi
fi
+1
View File
@@ -0,0 +1 @@
<%= body %>
+80
View File
@@ -0,0 +1,80 @@
/**
* The key must be composed of alpha characters with periods seperating them.
*/
const KEY_REGEX = /^(?:[A-Za-z][A-Za-z\.]*[A-Za-z])?(?:[A-Za-z]*)$/;
/**
* Allows metadata properties to be set/unset from specific models. It is the
* expecatation of this API that the metadata field is either accessed later
* directly, or accessed as a result of another database load rather than
* this service providing an interface to do so.
*
* @class MetadataService
*/
class MetadataService {
/**
* Parses a key by ensuring that if it is either a string, or an array with
* only characters defined in the `KEY_REGEX`
*
* @static
* @param {String|Array} key
* @returns {String} string form of the key
*
* @memberOf Metadata
*/
static parseKey(key) {
if (Array.isArray(key)) {
key = key.join('.');
}
if ((typeof key !== 'string') || !KEY_REGEX.test(key) || key.length === 0) {
throw new Error(`${key} is not valid, only a-zA-Z. allowed`);
}
return ['metadata', key].join('.');
}
/**
* Sets an object on the metadata field of an object.
*
* @static
* @param {mongoose.Model} model the mongoose model for the object
* @param {String} id the value for the field `id` of the model
* @param {String|Array} key key for the metadata field
* @param {any} value javascript object to set the value of the metadata to
* @returns {Promise} resolves when the update is complete
*
* @memberOf Metadata
*/
static async set(model, id, key, value) {
key = MetadataService.parseKey(key);
return model.update({id}, {
$set: {
[key]: value
}
});
}
/**
* Removes the value for the metadata field as the specific key.
*
* @static
* @param {mongoose.Model} model the mongoose model for the object
* @param {String} id the value for the field `id` of the model
* @param {String|Array} key key for the metadata field
* @returns
*
* @memberOf Metadata
*/
static async unset(model, id, key) {
key = MetadataService.parseKey(key);
return model.update({id}, {
$unset: {[key]: ''}
});
}
}
module.exports = MetadataService;
+53 -24
View File
@@ -4,7 +4,6 @@ const SettingsService = require('./settings');
const fetch = require('node-fetch');
const FormData = require('form-data');
const LocalStrategy = require('passport-local').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
const errors = require('../errors');
const debug = require('debug')('talk:passport');
@@ -27,6 +26,52 @@ passport.deserializeUser((id, done) => {
});
});
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return next(errors.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user});
});
};
/**
* Returns the response to the login attempt via a popup callback with some JS.
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
};
/**
* Validates that a user is allowed to login.
* @param {User} user the user to be validated
@@ -307,26 +352,10 @@ passport.use(new LocalStrategy({
return ValidateUserLogin(loginProfile, user, done);
}));
if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && process.env.TALK_ROOT_URL) {
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);
}));
} else {
console.error('Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL');
}
module.exports = passport;
module.exports = {
passport,
ValidateUserLogin,
HandleFailedAttempt,
HandleAuthCallback,
HandleAuthPopupCallback
};
+3
View File
@@ -0,0 +1,3 @@
const {plugins, PluginManager} = require('../plugins');
module.exports = new PluginManager(plugins);
+1 -1
View File
@@ -268,7 +268,7 @@ module.exports = class UsersService {
if (checkAgainstWordlist) {
// check for profanity
return Wordlist.usernameCheck(username);
console.log('Username profanity check disabled: ', Wordlist.usernameCheck(username));
}
// No errors found!
+1 -1
View File
@@ -1,3 +1,3 @@
{
"extends": "../../client/.babelrc"
"extends": "../../.babelrc"
}
-1
View File
@@ -6,7 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coral - (Beta)</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<style>
/**
+3 -1
View File
@@ -1,5 +1,7 @@
test/helpers/*.js
test
test/e2e
test/server
--compilers js:babel-core/register
--require ignore-styles
--recursive
@@ -1,8 +1,8 @@
const expect = require('chai').expect;
const User = require('../../models/user');
const Context = require('../../graph/context');
const errors = require('../../errors');
const User = require('../../../models/user');
const Context = require('../../../graph/context');
const errors = require('../../../errors');
describe('graph.Context', () => {
@@ -1,13 +1,13 @@
const {expect} = require('chai');
const {graphql} = require('graphql');
const schema = require('../../../graph/schema');
const Context = require('../../../graph/context');
const UserModel = require('../../../models/user');
const AssetModel = require('../../../models/asset');
const SettingsService = require('../../../services/settings');
const ActionModel = require('../../../models/action');
const CommentModel = require('../../../models/comment');
const schema = require('../../../../graph/schema');
const Context = require('../../../../graph/context');
const UserModel = require('../../../../models/user');
const AssetModel = require('../../../../models/asset');
const SettingsService = require('../../../../services/settings');
const ActionModel = require('../../../../models/action');
const CommentModel = require('../../../../models/comment');
describe('graph.loaders.Metrics', () => {
beforeEach(() => SettingsService.init());
@@ -1,11 +1,11 @@
const expect = require('chai').expect;
const {graphql} = require('graphql');
const schema = require('../../../graph/schema');
const Context = require('../../../graph/context');
const UserModel = require('../../../models/user');
const SettingsService = require('../../../services/settings');
const CommentsService = require('../../../services/comments');
const schema = require('../../../../graph/schema');
const Context = require('../../../../graph/context');
const UserModel = require('../../../../models/user');
const SettingsService = require('../../../../services/settings');
const CommentsService = require('../../../../services/comments');
describe('graph.mutations.addCommentTag', () => {
let comment;
@@ -1,12 +1,12 @@
const expect = require('chai').expect;
const {graphql} = require('graphql');
const schema = require('../../../graph/schema');
const Context = require('../../../graph/context');
const UserModel = require('../../../models/user');
const AssetModel = require('../../../models/asset');
const SettingsService = require('../../../services/settings');
const ActionModel = require('../../../models/action');
const schema = require('../../../../graph/schema');
const Context = require('../../../../graph/context');
const UserModel = require('../../../../models/user');
const AssetModel = require('../../../../models/asset');
const SettingsService = require('../../../../services/settings');
const ActionModel = require('../../../../models/action');
describe('graph.mutations.createComment', () => {
beforeEach(() => SettingsService.init());
@@ -1,11 +1,11 @@
const expect = require('chai').expect;
const {graphql} = require('graphql');
const schema = require('../../../graph/schema');
const Context = require('../../../graph/context');
const UserModel = require('../../../models/user');
const SettingsService = require('../../../services/settings');
const CommentsService = require('../../../services/comments');
const schema = require('../../../../graph/schema');
const Context = require('../../../../graph/context');
const UserModel = require('../../../../models/user');
const SettingsService = require('../../../../services/settings');
const CommentsService = require('../../../../services/comments');
describe('graph.mutations.removeCommentTag', () => {
let comment;
+1 -1
View File
@@ -1,4 +1,4 @@
const kue = require('../services/kue');
const kue = require('../../services/kue');
beforeEach(() => {
+1 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('./helpers/mongoose');
const mongoose = require('../helpers/mongoose');
before(function(done) {
this.timeout(30000);
+1 -1
View File
@@ -1,4 +1,4 @@
const authorization = require('../middleware/authorization');
const authorization = require('../../middleware/authorization');
// Add the passport middleware here before it's setup.
authorization.middleware.push((req, res, next) => {
@@ -1,17 +1,17 @@
const passport = require('../../../passport');
const app = require('../../../../app');
const app = require('../../../../../app');
const chai = require('chai');
const expect = chai.expect;
const SettingsService = require('../../../../services/settings');
const SettingsService = require('../../../../../services/settings');
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
// Setup chai.
chai.should();
chai.use(require('chai-http'));
const UsersService = require('../../../../services/users');
const UsersService = require('../../../../../services/users');
describe('/api/v1/account/username', () => {
let mockUser;
@@ -1,6 +1,6 @@
const passport = require('../../../passport');
const app = require('../../../../app');
const app = require('../../../../../app');
const chai = require('chai');
const expect = chai.expect;
@@ -8,9 +8,9 @@ const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
const AssetModel = require('../../../../models/asset');
const AssetsService = require('../../../../services/assets');
const SettingsService = require('../../../../services/settings');
const AssetModel = require('../../../../../models/asset');
const AssetsService = require('../../../../../services/assets');
const SettingsService = require('../../../../../services/settings');
describe('/api/v1/assets', () => {

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