Compare commits

...
324 Commits
Author SHA1 Message Date
Kim Gardner db183124c9 Merge pull request #787 from coralproject/long-title
Fix for long titles in the All Streams Queue
2017-07-21 21:18:44 -04:00
Kim Gardner ee220ca936 Merge branch 'master' into long-title 2017-07-21 21:13:24 -04:00
Kim Gardner 907e9bf226 Merge pull request #786 from coralproject/featured-comments-info
Featured Comments Tooltips
2017-07-21 15:18:15 -04:00
Belen Curcio 25bccde40f fix 2017-07-21 16:08:18 -03:00
Belen Curcio 6343a0d621 Adding tooltip to badges too 2017-07-21 16:01:43 -03:00
Kim Gardner 6e6caaf5a9 Merge pull request #782 from coralproject/post-comment-no-alias
Remove remaining username alias in post comment mutation
2017-07-21 09:35:58 -04:00
Kim Gardner 8261c40c8d Merge branch 'master' into post-comment-no-alias 2017-07-21 09:31:01 -04:00
Kim Gardner 316b0a8808 Merge pull request #781 from coralproject/featured-tag-on-replies
Show featured tag on replies too
2017-07-21 09:30:45 -04:00
Kim Gardner ebce930b8c Merge branch 'master' into featured-tag-on-replies 2017-07-21 09:19:21 -04:00
Kim Gardner 8c8a2fcee0 Merge pull request #780 from coralproject/incosistent-state-load-more
Quick fix inconsistent state
2017-07-21 09:18:46 -04:00
Kim Gardner 1733ab8690 Merge branch 'master' into post-comment-no-alias 2017-07-21 09:16:54 -04:00
Kim Gardner 9624a12e39 Merge branch 'master' into featured-tag-on-replies 2017-07-21 09:15:31 -04:00
Kim Gardner 143050cbd5 Merge branch 'master' into incosistent-state-load-more 2017-07-21 09:14:19 -04:00
Kim Gardner 0fd7d9f020 Merge pull request #784 from coralproject/featured-comments-info
Featured Comments Info Tooltip
2017-07-21 09:14:00 -04:00
Belén Curcio 7ac3a325f7 Merge branch 'master' into featured-comments-info 2017-07-21 05:43:05 -03:00
Belen Curcio 149389057d Translations and style 2017-07-21 05:34:28 -03:00
Belen Curcio 3b01218338 Adding translations 2017-07-21 05:34:04 -03:00
Belen Curcio 2d8e36e511 Removing innecesary connects, and classes 2017-07-21 05:27:34 -03:00
Belen Curcio 9c9f088ac3 Containers, reducer and constants 2017-07-21 05:24:45 -03:00
Belen Curcio 392d82e30a Info Icon, and Tooltip 2017-07-21 05:24:27 -03:00
Belen Curcio f86d210bbb Adding actions 2017-07-21 05:23:54 -03:00
Belen Curcio 11ea45ef69 typo with white space 2017-07-21 05:21:13 -03:00
Belén Curcio b0cbddd313 Merge pull request #783 from coralproject/disable-fragment-warnings
Disable false-positive warnings
2017-07-20 19:32:57 -03:00
Chi Vinh Le 1f8f293594 Disable false-positive warnings 2017-07-21 04:17:47 +07:00
Chi Vinh Le 7ca7166c7a Add clarification 2017-07-21 04:08:43 +07:00
Chi Vinh Le 65500eda64 Remove remaining username alias in post comment mutation 2017-07-21 03:57:32 +07:00
Chi Vinh Le 0e37f36e02 Show featured tag on replies too 2017-07-21 02:36:04 +07:00
Chi Vinh Le abed842c23 Quick fix inconsistent state 2017-07-21 02:27:59 +07:00
Kim Gardner e1d1549c93 Merge pull request #736 from coralproject/featured-comments
Featured Comments
2017-07-20 10:45:56 -04:00
Belen Curcio 8f820ace5b Merge branch 'master' of github.com:coralproject/talk 2017-07-20 11:45:54 -03:00
Belen Curcio 1069498323 Merge branch 'master' of github.com:coralproject/talk 2017-07-20 11:31:25 -03:00
Kim Gardner c34daacd8c Merge branch 'master' into featured-comments 2017-07-19 20:07:28 -04:00
Kim Gardner f7f5b42b71 Merge pull request #778 from coralproject/make-permalink-readonly
Make permalink readonly
2017-07-19 20:07:12 -04:00
Chi Vinh Le f8a3b84e94 Make permalink readonly 2017-07-20 04:04:25 +07:00
Chi Vinh Le 44e89bc96c Remove unused style 2017-07-20 04:01:39 +07:00
Chi Vinh Le 2342934f48 Reset star button css 2017-07-20 03:58:22 +07:00
Chi Vinh Le 5328374b73 Make goTo link accessible 2017-07-20 03:51:34 +07:00
Chi Vinh Le 6f64eac5de Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-20 03:45:01 +07:00
Chi Vinh Le 0b884a7664 Harmonize classnames 2017-07-20 03:44:25 +07:00
Kim Gardner 64602f1731 Merge branch 'master' into featured-comments 2017-07-19 16:42:37 -04:00
Kim Gardner 3bc26cad80 Merge pull request #776 from coralproject/bump-version-2-5-0
Bump version 2.5.0
2017-07-19 16:41:26 -04:00
Kim Gardner def49d42aa Merge branch 'master' into bump-version-2-5-0 2017-07-19 16:34:43 -04:00
Chi Vinh Le 3d4c296837 Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-20 03:31:44 +07:00
Chi Vinh Le 64571d3848 Subtract ignored comments correctly 2017-07-20 03:30:37 +07:00
Kim Gardner ad669b8399 Merge branch 'master' into featured-comments 2017-07-19 16:28:20 -04:00
Kim Gardner 1c8e009366 Merge pull request #777 from coralproject/perma-link-plugin-commentId
Replace deprecated commentId in talk-plugin-permalink
2017-07-19 16:27:59 -04:00
Chi Vinh Le 5d8528fdb5 Replace deprecated commentId 2017-07-20 03:16:05 +07:00
Kim Gardner 6cab2634bf Add space before Go to conversation text 2017-07-19 16:05:28 -04:00
Chi Vinh Le d6adff288c Fix typo 2017-07-20 02:51:09 +07:00
Chi Vinh Le ad4b03bfcd Merge branch 'master' into featured-comments 2017-07-20 02:39:29 +07:00
Kim Gardner 99b8b4e90b Bump version 2.5.0 2017-07-19 15:34:04 -04:00
Chi Vinh Le 3fa475009a Use getDefinition name from plugin-api 2017-07-20 02:32:17 +07:00
Kim Gardner 139802381f Merge pull request #775 from coralproject/recursively-resolve-fragments
Recursively resolve fragments
2017-07-19 15:29:06 -04:00
Chi Vinh Le fd0143da5d Use connect from framework 2017-07-20 02:17:31 +07:00
Chi Vinh Le baa4808364 Allow setting default stream tab 2017-07-20 02:16:19 +07:00
Chi Vinh Le 673f0cd232 Hide featured comments tab when empty 2017-07-20 02:07:40 +07:00
Kim Gardner fa02d44270 Merge branch 'master' into recursively-resolve-fragments 2017-07-19 14:32:02 -04:00
Chi Vinh Le 84edd8c6c2 Merge branch 'master' into featured-comments
Conflicts:
	client/coral-embed-stream/src/components/Stream.js
2017-07-20 01:30:22 +07:00
Chi Vinh Le da2bc69d19 Translation 2017-07-20 01:27:06 +07:00
Chi Vinh Le b769e95898 Implement pagination 2017-07-20 01:19:45 +07:00
Kim Gardner 2b7f46efc7 Merge pull request #769 from coralproject/excludeIf-api
Conditionally exclude a Plugin Component from a Slot
2017-07-19 13:03:49 -04:00
Chi Vinh Le 7005175fc0 Simplify file namings 2017-07-19 23:55:35 +07:00
Chi Vinh Le 8f64229dee Implement FeaturedComment container 2017-07-19 23:50:39 +07:00
Chi Vinh Le 47a5b84a74 Pass correct slot props 2017-07-19 23:37:35 +07:00
Chi Vinh Le cc1026233f Merge branch 'recursively-resolve-fragments' into featured-comments 2017-07-19 23:32:04 +07:00
Chi Vinh Le 6f1b0b55ec Merge branch 'master' into featured-comments 2017-07-19 23:31:54 +07:00
Chi Vinh Le dcdfbb691a Use lazy loading for slot fragments 2017-07-19 23:30:51 +07:00
Chi Vinh Le 55ff0bcf8c Recursively resolve fragments 2017-07-19 22:58:27 +07:00
Belén Curcio d61374993a Merge branch 'master' into excludeIf-api 2017-07-19 11:47:51 -03:00
Kim Gardner 980dc2515e Merge pull request #774 from coralproject/resolve-slot-fragments-lazy
Support lazily resolving slot fragments
2017-07-19 10:45:19 -04:00
Chi Vinh Le 4511b9ca57 Merge branch 'resolve-slot-fragments-lazy' into featured-comments
Conflicts:
	client/coral-embed-stream/src/containers/Stream.js
	client/coral-framework/utils/index.js
	plugin-api/beta/client/utils/index.js
2017-07-19 21:15:35 +07:00
Chi Vinh Le 52cd7bcce4 Expose getSlotFragmentSpread in plugin-api 2017-07-19 20:59:03 +07:00
Chi Vinh Le 5fee26da0e Document getSlotFragmentSpreads 2017-07-19 20:50:00 +07:00
Chi Vinh Le d99f6d8923 Support lazily resolving slot fragments 2017-07-19 20:44:22 +07:00
Chi Vinh Le f051ee1d58 Don't create callback in render 2017-07-19 00:48:33 +07:00
Chi Vinh Le cd1735e1d6 Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-19 00:34:12 +07:00
Chi Vinh Le 3b10aa3945 Implement 'Go to conversation' 2017-07-19 00:33:01 +07:00
Kim Gardner 3b0b6bd73c Merge branch 'master' into featured-comments 2017-07-18 12:03:12 -04:00
Chi Vinh Le 760d078705 Embed scroll to correct element 2017-07-18 23:00:12 +07:00
Chi Vinh Le 46473142ba Use generic version of addTag, removeTag in framework 2017-07-18 21:23:39 +07:00
Chi Vinh Le 8a619a219f Refactor isFeatured / isTagged 2017-07-18 21:10:59 +07:00
Kim Gardner 73d15f34d7 Merge branch 'master' into excludeIf-api 2017-07-18 10:10:20 -04:00
Chi Vinh Le cf56ca8263 Merge branch 'featured-comments' of ssh://github.com/coralproject/talk into featured-comments 2017-07-18 21:07:09 +07:00
Kim Gardner 1183550e14 Merge pull request #768 from coralproject/docs-microservices
Write microservice docs
2017-07-18 10:05:37 -04:00
Chi Vinh Le 6652368221 Don't export internals of embed stream in plugin-api (for now) 2017-07-18 21:04:30 +07:00
Kim Gardner d488e168c4 Merge branch 'master' into featured-comments 2017-07-18 09:59:49 -04:00
Kim Gardner 9990142740 Merge branch 'master' into docs-microservices 2017-07-18 09:58:54 -04:00
Kim Gardner 33bd2f576a Merge branch 'master' into excludeIf-api 2017-07-18 09:58:44 -04:00
Chi Vinh Le 19929c97d8 Fix layout issues in comment 2017-07-18 20:17:01 +07:00
Chi Vinh Le 99fc67d0d9 Rename insertSorted to insertCommentSorted 2017-07-18 19:38:52 +07:00
Chi Vinh Le 636d1f4788 Use withFragments from plugin-api 2017-07-18 19:37:30 +07:00
Chi Vinh Le d45038ccc5 Adapt package.json 2017-07-18 19:37:24 +07:00
Chi Vinh Le 9746422b16 Remove unused actions 2017-07-18 18:31:35 +07:00
Chi Vinh Le 4ee727c86a Fix inconsistent query state 2017-07-18 18:27:58 +07:00
Chi Vinh Le ad09ce03a1 Prevent double execution of mutation 2017-07-18 17:09:48 +07:00
Chi Vinh Le afeeffc262 Show notification upon failure 2017-07-18 17:08:14 +07:00
Chi Vinh Le 043d5fdcec Handle ignore user 2017-07-18 16:39:06 +07:00
Chi Vinh Le 8c7c564a30 Allow comment slots to extend the asset fragment + harmonize slot props 2017-07-18 15:23:02 +07:00
Wyatt Johnson 13cac731b9 Merge pull request #773 from icco/patch-1
Fix email address env var in docs
2017-07-17 15:29:40 -06:00
Nat Welch ab87500303 Fix email address env var in docs 2017-07-17 17:02:46 -04:00
Andrew Losowsky 244d421eda Merge pull request #770 from coralproject/autoflag-suspect
Added new TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS option
2017-07-17 13:28:04 -07:00
Wyatt Johnson d5dfd89d94 review of docs 2017-07-17 10:54:54 -06:00
Wyatt Johnson d82735b2b5 Adjusted docs 2017-07-17 10:50:40 -06:00
Wyatt Johnson b091cf5221 Added new TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS option 2017-07-17 10:43:28 -06:00
Chi Vinh Le d90f87428c Remove changes in featured 2017-07-17 19:37:01 +07:00
Chi Vinh Le 41f5297d78 Deprecate isSlotEmpty in the plugins api 2017-07-17 19:32:48 +07:00
Chi Vinh Le 0e9d9067f9 Use plugin-api and port plugins to it 2017-07-17 19:28:02 +07:00
Chi Vinh Le d2e6a98d46 connect aware "excludeIf" using our own hoc 2017-07-17 18:06:51 +07:00
Chi Vinh Le 4dd03508ee Merge branch 'master' into excludeIf-api 2017-07-17 17:15:16 +07:00
Chi Vinh Le fb2662572a Implement excludeIf for the plugins API 2017-07-17 17:06:12 +07:00
David Erwin 0069f31244 Make it accurate 2017-07-14 16:07:47 -04:00
David Erwin b235b8334e Give some context 2017-07-14 16:06:47 -04:00
David Erwin acb7028471 Write microservice docs 2017-07-14 16:01:57 -04:00
Kim Gardner 9daaa6ffee Merge branch 'master' into featured-comments 2017-07-14 14:27:41 -04:00
David Erwin af3489db9c Merge pull request #767 from coralproject/update-node
Update Node version to 7.10.1
2017-07-14 14:27:08 -04:00
Kim Gardner 012fa3e4aa Merge branch 'master' into featured-comments 2017-07-14 14:13:53 -04:00
Belen Curcio a8467db7aa Handling cache and mutations 2017-07-14 14:57:34 -03:00
Kim Gardner 05e2302794 Update CircleCI node version 2017-07-14 13:31:35 -04:00
Kim Gardner c610c319de Downgrade to 7.10.1 due to dependency issues 2017-07-14 13:16:38 -04:00
Kim Gardner 04d330cf5b Update node version in CircleCI config 2017-07-14 12:41:15 -04:00
Kim Gardner 8cbb56af19 Update default node version for Talk 2017-07-14 12:40:39 -04:00
Kim Gardner d4cb07e448 Update node version in Dockerfile 2017-07-14 12:39:48 -04:00
Belen Curcio 71b4413f92 Using same slot 2017-07-14 13:39:35 -03:00
Kim Gardner 9455eea041 Merge pull request #766 from coralproject/add-code-of-conduct-1
Created Code of Conduct
2017-07-14 11:23:56 -04:00
Andrew Losowsky e8bbd7e4d7 Created Code of Conduct
Merged ours with the Contributor Covenant
2017-07-14 10:34:58 -04:00
Belen Curcio 94032b69fc Missing asset 2017-07-14 11:34:20 -03:00
Belen Curcio 1f199ad3dd Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-14 11:31:58 -03:00
Belen Curcio eb038b9d49 Adding a slot to push like into featured comments 😎 2017-07-13 17:59:09 -03:00
Belen Curcio b8d00ec9ce Adding go to conversation 2017-07-13 17:29:54 -03:00
Belen Curcio b5ce16a33b Styling 2017-07-13 16:56:40 -03:00
Belen Curcio f8a6987476 Styles 2017-07-13 16:36:41 -03:00
Kim Gardner 28e589de21 Merge pull request #765 from coralproject/remove-like-plugin-as-default
Remove "like" from default plugins
2017-07-13 14:32:19 -04:00
Belen Curcio 1b97811cb0 Querying by featured comments 2017-07-13 15:30:06 -03:00
Kim Gardner 31cfddf93c Remove "like" from default plugins 2017-07-13 14:20:26 -04:00
Kim Gardner 562f2350c1 Merge pull request #764 from coralproject/update-readme-recipes
Update README with info about Recipes
2017-07-13 13:10:42 -04:00
Kim Gardner ea6330bf94 Update README with info about Recipes 2017-07-13 13:01:47 -04:00
Kim Gardner 0be3dc2ceb Merge pull request #751 from coralproject/talk-plugin-avatar
Talk Plugin Avatar
2017-07-13 12:54:04 -04:00
Kim Gardner eb98b32d21 Merge branch 'master' into talk-plugin-avatar 2017-07-13 12:13:58 -04:00
Kim Gardner 127ee25d7f Merge pull request #763 from coralproject/docs-trouble
Add install troubleshooting doc
2017-07-13 12:13:40 -04:00
David Erwin ed4a3c3250 Merge branch 'master' into docs-trouble 2017-07-13 12:05:09 -04:00
David Erwin 7b53048a91 Add install troubleshooting doc 2017-07-13 12:04:18 -04:00
Chi Vinh Le 28e94d30b0 Only show seperator on top-level comments 2017-07-13 23:00:52 +07:00
Chi Vinh Le 1c2fec29a4 Merge branch 'master' into talk-plugin-avatar
Conflicts:
	client/coral-embed-stream/src/components/Comment.css
2017-07-13 22:58:35 +07:00
Kim Gardner 56cc4989fd Merge pull request #761 from coralproject/docs-metadata
Docs metadata
2017-07-13 11:53:58 -04:00
Belen Curcio d72df73d6f Implementing first draft of FC 2017-07-13 12:53:01 -03:00
Kim Gardner b32aebd6f1 Merge branch 'master' into docs-metadata 2017-07-13 11:47:40 -04:00
Kim Gardner 244a276e1a Merge pull request #760 from coralproject/styles-fixes-2
Remove some global css, fix some styling issues
2017-07-13 11:46:28 -04:00
Kim Gardner 245561bd52 Merge branch 'master' into talk-plugin-avatar 2017-07-13 11:42:59 -04:00
Kim Gardner d05a63bc2e Merge branch 'master' into docs-metadata 2017-07-13 11:42:53 -04:00
Kim Gardner 5d118c26d1 Merge branch 'master' into styles-fixes-2 2017-07-13 11:42:04 -04:00
Belén Curcio 5ab5b7ca70 Merge pull request #762 from coralproject/reply-error-fix
BUG: Console error when you reply to a comment
2017-07-13 12:41:30 -03:00
Belen Curcio 7ba11cdee9 Merge branch 'master' of github.com:coralproject/talk 2017-07-13 12:36:35 -03:00
Chi Vinh Le b79c990ee5 Only setState once 2017-07-13 22:34:53 +07:00
Belen Curcio 1541a618b5 Reordering divs 2017-07-13 12:29:06 -03:00
Belen Curcio 8d4c0c6721 WIP 2017-07-13 12:15:13 -03:00
Belen Curcio 195592b9ab Merge branch 'master' into featured-comments 2017-07-13 12:08:02 -03:00
Belen Curcio da46d27e5b Merge branch 'featured-comments' of github.com:coralproject/talk into featured-comments 2017-07-13 12:07:23 -03:00
Belen Curcio bfbe8d44aa Merg 2017-07-13 12:05:53 -03:00
Belen Curcio da6965122f Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-13 12:04:04 -03:00
David Erwin 30b200d734 Fix typos 2017-07-13 10:58:31 -04:00
David Erwin dc76d89c47 Fewer, better words 2017-07-13 10:49:38 -04:00
Belen Curcio 3d80df1db6 Support for empty slot 2017-07-13 11:37:54 -03:00
Chi Vinh Le 83832abd72 More fixes 2017-07-13 21:37:08 +07:00
David Erwin 47fd522f68 Add first draft 2017-07-13 10:25:36 -04:00
Chi Vinh Le 1280b16bcd Remove some global css, fix some styling issues 2017-07-13 20:47:30 +07:00
David Erwin 3d666111b0 Add metadata docs 2017-07-13 09:26:37 -04:00
David Erwin 919bdff40a Use less words, say same thing 2017-07-13 09:26:09 -04:00
Belen Curcio 518b7b5358 merge 2017-07-12 15:04:24 -03:00
Belen Curcio 8b9463f501 Merge branch 'master' into talk-plugin-avatar 2017-07-12 15:01:14 -03:00
Belen Curcio f5313c0aed Merge branch 'master' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 15:00:03 -03:00
Kim Gardner c59c09e1f4 Merge pull request #759 from coralproject/styles-quick-fix
Style glitches quick fix
2017-07-12 13:57:43 -04:00
Belen Curcio e98dcb6647 Merge branch 'master' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 14:53:14 -03:00
Belen Curcio 39db11569f Merge branch 'talk-plugin-avatar' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 14:52:18 -03:00
Belen Curcio c3ad47f6f6 Linting 2017-07-12 14:49:46 -03:00
Kiwi b9cb1a783f Merge branch 'master' into styles-quick-fix 2017-07-12 19:42:04 +02:00
Chi Vinh Le ea51fdf6cc Styles glitches quick fix 2017-07-13 00:39:55 +07:00
Kim Gardner 324aa6db32 Merge pull request #757 from coralproject/query-by-tags
Allow query comments and comment counts by tags
2017-07-12 13:35:00 -04:00
Belen Curcio 9065a64d13 Merge branch 'master' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-12 14:33:00 -03:00
Kim Gardner 9b36fcebe9 Merge branch 'master' into query-by-tags 2017-07-12 12:21:20 -04:00
Kim Gardner 5d9d01d87f Merge pull request #749 from coralproject/extendable-tab-menu
Extendable Stream Tab Panel
2017-07-12 12:20:22 -04:00
Chi Vinh Le 7f85129044 Merge branch 'master' into extendable-tab-menu
Conflicts:
	client/coral-embed-stream/src/components/Embed.js
	client/coral-embed-stream/style/default.css
	client/coral-framework/helpers/plugins.js
	client/coral-plugin-replies/ReplyButton.css
	client/coral-plugin-replies/ReplyButton.js
	plugins/coral-plugin-like/client/LikeButton.js
	plugins/coral-plugin-like/client/styles.css
	plugins/coral-plugin-love/client/LoveButton.js
	plugins/coral-plugin-love/client/styles.css
2017-07-12 22:38:05 +07:00
Chi Vinh Le fc31a2882d Add a comment 2017-07-12 21:05:29 +07:00
Chi Vinh Le 23cc3548d4 Query by tags 2017-07-12 20:55:54 +07:00
Belén Curcio 574f5b7cea Merge branch 'master' into talk-plugin-avatar 2017-07-12 09:12:41 -03:00
Belen Curcio b19e050399 Merge branch 'master' of github.com:coralproject/talk 2017-07-12 09:10:48 -03:00
David Erwin ef3c82fee1 Merge pull request #758 from coralproject/docs-architecture-cli
Create cli docs
2017-07-11 16:57:54 -04:00
Kim Gardner 32211c4e37 Merge branch 'master' into featured-comments 2017-07-11 16:33:34 -04:00
Kim Gardner 202e7a2b10 Merge branch 'master' into talk-plugin-avatar 2017-07-11 16:33:21 -04:00
Kim Gardner 2588f3946d Merge branch 'master' into docs-architecture-cli 2017-07-11 16:32:52 -04:00
Kim Gardner 1c2e850a07 Merge pull request #756 from coralproject/bump-version-2-4-0
Update version to 2.4.0
2017-07-11 16:23:16 -04:00
Kim Gardner 0e8ecb37f3 Merge branch 'master' into bump-version-2-4-0 2017-07-11 16:11:33 -04:00
David Erwin d1b96f5830 Merge branch 'docs-architecture-cli' of https://github.com/coralproject/talk into docs-architecture-cli 2017-07-11 15:06:07 -04:00
David Erwin a9c6014634 Add more info 2017-07-11 15:05:52 -04:00
Kim Gardner 125168c7c8 Merge branch 'master' into docs-architecture-cli 2017-07-11 14:58:41 -04:00
David Erwin eafcadd6a1 Warn users more 2017-07-11 14:43:59 -04:00
David Erwin 211f24476a Create cli docs 2017-07-11 14:39:26 -04:00
David Erwin f55e4ef895 Merge pull request #753 from coralproject/adapt-embed-classnames
Adapt Class Names
2017-07-11 14:20:53 -04:00
Kim Gardner bbf1da88e6 Merge branch 'master' into adapt-embed-classnames 2017-07-11 13:51:18 -04:00
Kim Gardner d688f70c19 Merge pull request #755 from coralproject/docs-qf-register-link
Fix 404
2017-07-11 13:47:11 -04:00
Kim Gardner da7aa8f6d0 Merge branch 'master' into talk-plugin-avatar 2017-07-11 13:46:41 -04:00
Kim Gardner b04a477f6a Merge branch 'master' into adapt-embed-classnames 2017-07-11 13:44:51 -04:00
Kim Gardner 0546f741a5 Bump version to 2.4.0 2017-07-11 13:43:12 -04:00
David Erwin a535baeb31 Merge branch 'master' into docs-qf-register-link 2017-07-11 13:41:56 -04:00
David Erwin 8b125eab48 Fix 404 2017-07-11 13:36:45 -04:00
Kim Gardner 053b687959 Merge pull request #746 from coralproject/create-plugin
Create Plugins - Create plugins through the CLI
2017-07-11 13:33:57 -04:00
Kim Gardner b620ac703b Merge branch 'master' into adapt-embed-classnames 2017-07-11 12:11:33 -04:00
Kim Gardner 3cb0bb052c Merge branch 'master' into create-plugin 2017-07-11 12:06:19 -04:00
Kim Gardner c4985e0007 Merge branch 'master' into talk-plugin-avatar 2017-07-11 12:02:17 -04:00
David Erwin 066d9ce0ec Merge pull request #745 from coralproject/docs-features
Docs Architecture
2017-07-11 11:55:41 -04:00
David Erwin dc6fa74d22 Add more clarity 2017-07-11 11:51:02 -04:00
David Erwin e9e79b4605 Merge branch 'docs-features' of https://github.com/coralproject/talk into docs-features 2017-07-11 11:06:25 -04:00
David Erwin 1d9b627d73 Fix typos and clarify 2017-07-11 11:05:58 -04:00
Chi Vinh Le 40f4419c9d Add flagged unflagged CSS classnames 2017-07-11 21:54:35 +07:00
Chi Vinh Le 8b70dc688d Adapt CSS classnames for Embed 2017-07-11 21:03:27 +07:00
Chi Vinh Le b05a6c00ce Correct aria-controls 2017-07-11 20:37:19 +07:00
Kim Gardner f0bdee415b Merge branch 'master' into featured-comments 2017-07-11 08:18:31 -04:00
Kim Gardner 9063b8adf1 Merge branch 'master' into docs-features 2017-07-11 08:18:25 -04:00
Kim Gardner 01fd667eb4 Merge branch 'master' into talk-plugin-avatar 2017-07-11 08:17:49 -04:00
Kim Gardner 73ac8d16ef Merge branch 'master' into create-plugin 2017-07-11 08:13:59 -04:00
Kim Gardner ced449a148 Merge pull request #747 from coralproject/fix-permalink-live-update
Fix perma view live updates
2017-07-11 08:12:59 -04:00
Kim Gardner ffad851066 Merge branch 'master' into fix-permalink-live-update 2017-07-11 07:49:16 -04:00
Kim Gardner 4281fd590c Merge pull request #750 from coralproject/no-label-on-mobile
Remove labels for comment actions on mobile.
2017-07-11 07:46:11 -04:00
Belen Curcio 0c167fdfc4 Removing talk-plugin-avatar 2017-07-10 17:43:51 -03:00
Belen Curcio 5dfb372c42 Merge branch 'master' of github.com:coralproject/talk 2017-07-10 17:11:06 -03:00
Belen Curcio 932103c204 updated yarn lock 2017-07-10 17:08:49 -03:00
Belen Curcio 666225614b Merge branch 'talk-plugin-avatar' of github.com:coralproject/talk into talk-plugin-avatar 2017-07-10 17:06:57 -03:00
Belen Curcio 636f357186 Reverting changes from old branch 2017-07-10 17:06:40 -03:00
Belén Curcio be6337f17c Merge branch 'master' into talk-plugin-avatar 2017-07-10 17:05:56 -03:00
Belen Curcio 2c7f5ca089 linting 2017-07-10 17:00:01 -03:00
Belen Curcio f09550a4ac Avatar plugin support extending fragments 2017-07-10 16:56:46 -03:00
Belen Curcio 3c3a0523de Avatar support 2017-07-10 16:27:15 -03:00
Chi Vinh Le a2dabcac0e Hide action labels on mobile 2017-07-11 01:26:25 +07:00
Wyatt Johnson a686bc75a7 Some changes to server side stuff + tempalte location 2017-07-10 10:30:20 -06:00
Chi Vinh Le 8960f67a4d Add todo comments 2017-07-10 22:49:06 +07:00
Chi Vinh Le e10c1f1a1f Docs 2017-07-10 22:45:28 +07:00
Chi Vinh Le 55e70dfbec Fix perma view live updates 2017-07-10 22:13:04 +07:00
Chi Vinh Le af46e3a7b1 Merge branch 'master' into extendable-tab-menu
Conflicts:
	client/coral-embed-stream/src/components/Comment.js
2017-07-10 22:03:32 +07:00
Chi Vinh Le 883dfedfab Fix new count styles 2017-07-10 21:57:25 +07:00
Chi Vinh Le ae52b27009 Don't use div inside p 2017-07-10 21:53:55 +07:00
Chi Vinh Le 34a4a005d1 Add margin to highlighted comment 2017-07-10 21:12:52 +07:00
Chi Vinh Le ab72995a2c Implement streamTabs and streamTabPanes slot 2017-07-10 20:46:28 +07:00
Chi Vinh Le aac47c4686 Add margin 2017-07-10 17:57:45 +07:00
Chi Vinh Le 362c072181 If only one tab left make it not clickable 2017-07-10 17:03:07 +07:00
Chi Vinh Le ef172ae5f8 Change streamBox to streamFilter 2017-07-10 17:03:07 +07:00
Chi Vinh Le 1e4e1cb413 Implement Stream TabBar 2017-07-10 17:03:06 +07:00
Belén Curcio e819b96473 Merge branch 'master' into create-plugin 2017-07-09 12:21:49 -03:00
Belen Curcio 1e3c1feadf cta 2017-07-09 12:09:54 -03:00
Belen Curcio 83d7e599c7 pushing client template 2017-07-09 12:09:07 -03:00
David Erwin 52d7ba8965 Add Tags docs 2017-07-07 16:59:18 -04:00
Belen Curcio c3ec772ea2 new cta for questions 2017-07-07 14:43:46 -03:00
David Erwin d2b4c2bcac Add architecture docs 2017-07-07 13:43:05 -04:00
Kim Gardner f738da4dd5 Merge pull request #744 from coralproject/redis-flush
Flush Redis before each test
2017-07-07 16:19:52 +01:00
Kim Gardner b796bf2607 Merge branch 'master' into redis-flush 2017-07-07 16:06:22 +01:00
Kim Gardner 3924ced659 Merge pull request #743 from coralproject/change-org-name
Added new cli-settings
2017-07-07 16:01:55 +01:00
Wyatt Johnson 23573501bd Configured redis to flush before each test and changed default test
redis database
2017-07-07 08:57:56 -06:00
Wyatt Johnson 787c00325d Added new cli-settings 2017-07-07 08:40:18 -06:00
Kim Gardner db371d1c82 Merge pull request #742 from coralproject/bump-version-2-3-0
Update version number
2017-07-07 15:25:36 +01:00
David Erwin b744a98a3d Merge branch 'master' into bump-version-2-3-0 2017-07-07 10:20:44 -04:00
Kim Gardner 37439a0896 Merge pull request #741 from coralproject/rm-commonjs-framework
Remove CommonJS style exports from framework
2017-07-07 15:19:57 +01:00
Kim Gardner af3cd8aa68 Update version number 2017-07-07 15:09:25 +01:00
Kiwi 7baa551a5c Merge branch 'master' into rm-commonjs-framework 2017-07-07 12:39:00 +02:00
Chi Vinh Le b7c4dbd224 Remove CommonJS style exports in framework 2017-07-07 17:27:05 +07:00
Belen Curcio aad2455654 Linting 2017-07-06 15:19:26 -03:00
Belen Curcio a8e5ad451d Plugins CLI :D 2017-07-06 15:08:39 -03:00
Belen Curcio 99425e89ca More questions 2017-07-06 13:09:43 -03:00
Belen Curcio 60608af8b2 Filter out the ones that have slots 2017-07-06 11:40:35 -03:00
Belen Curcio 7c753ee518 create-plugin 2017-07-05 16:41:52 -03:00
Belén Curcio 70d1d5e9de Merge branch 'master' into featured-comments 2017-07-05 12:42:07 -03:00
Belén Curcio b28902312d Merge pull request #740 from coralproject/threading-cleanup
Cleanup
2017-07-05 12:41:28 -03:00
Belen Curcio 28010488be Linting 2017-07-05 12:31:35 -03:00
Belen Curcio 2a9535f88a Featured Comment Styles 2017-07-05 12:28:09 -03:00
Belen Curcio f9bece468f Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-05 12:19:09 -03:00
Kiwi f32c03f002 Merge branch 'master' into threading-cleanup 2017-07-05 17:13:43 +02:00
Chi Vinh Le 693879647e Cleanup 2017-07-05 22:11:55 +07:00
Kim Gardner c01d1f95bc Merge pull request #735 from coralproject/french-translation
French translations
2017-07-05 15:59:36 +01:00
Belen Curcio 38e8bed68a Merge branch 'featured-comments' of github.com:coralproject/talk into featured-comments 2017-07-05 11:36:26 -03:00
Kim Gardner 65d6d3221b Merge branch 'master' into french-translation 2017-07-05 15:36:14 +01:00
Belen Curcio 3ca62cab65 wip 2017-07-05 11:36:03 -03:00
Kim Gardner 15af61de67 Merge pull request #738 from coralproject/threading
Deep Threading Support
2017-07-05 15:35:43 +01:00
Kim Gardner 1adca208dd Merge branch 'master' into french-translation 2017-07-05 15:26:39 +01:00
Kim Gardner 935982cb08 Merge branch 'master' into threading 2017-07-05 15:23:45 +01:00
Belen Curcio da6bd80de0 Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-05 11:07:42 -03:00
Kim Gardner 774e0cc12e Merge branch 'master' into featured-comments 2017-07-05 14:29:17 +01:00
Kim Gardner 4b9f3390d3 Merge pull request #739 from coralproject/bump-version-2-2-1
Update version 2.2.1
2017-07-05 11:57:27 +01:00
Kim Gardner f94c36e9c9 Update version 2.2.1 2017-07-05 11:43:18 +01:00
Kim Gardner c976ba0ea4 Merge branch 'master' into french-translation 2017-07-05 09:17:36 +01:00
Chi Vinh Le 6d71bcbee9 Fix styling issues 2017-07-04 22:56:58 +07:00
Chi Vinh Le 9be2a520db Merge branch 'master' into threading 2017-07-04 22:42:49 +07:00
Chi Vinh Le d155615102 Add pending comment 2017-07-04 22:41:38 +07:00
Kim Gardner 4d71a5a448 Merge pull request #737 from coralproject/fix-plugin-slot-logic
Fix isSlotEmpty logic and handle empty plugin_config
2017-07-04 16:35:05 +01:00
Chi Vinh Le cfa7a74742 Fix isSlotEmpty logic and handle empty plugin_config 2017-07-04 22:22:52 +07:00
Chi Vinh Le 524be1b869 Allow customizing thread levels via TALK_THREADING_LEVEL env 2017-07-04 22:10:40 +07:00
Chi Vinh Le 31ab54d13c First round styling 2017-07-04 21:32:33 +07:00
Belén Curcio dcfbcd287b Merge branch 'master' into featured-comments 2017-07-04 10:29:25 -03:00
David Erwin c5203e3460 Merge pull request #734 from coralproject/dont-show-popup-when-empty
Don't show popup when empty
2017-07-03 14:40:31 -04:00
David Erwin 79c1bd9bdd Merge branch 'master' into dont-show-popup-when-empty 2017-07-03 14:35:44 -04:00
Belen Curcio 4af9c3bee2 Merge branch 'featured-comments' of github.com:coralproject/talk into featured-comments 2017-07-03 15:32:48 -03:00
Belen Curcio ec559f0f23 Merge branch 'master' of github.com:coralproject/talk into featured-comments 2017-07-03 15:32:40 -03:00
Belén Curcio b902695cb7 Merge branch 'master' into featured-comments 2017-07-03 15:32:36 -03:00
David Erwin e2575cb346 Merge pull request #733 from coralproject/disable-plugin-components-via-config
Disable plugin components via plugin_config
2017-07-03 14:28:54 -04:00
Belen Curcio 34dbbb0984 Removing Best from the core 2017-07-03 15:27:08 -03:00
Chi Vinh Le 3a2a4379b8 Change to 4 levels 2017-07-04 01:09:27 +07:00
Chi Vinh Le f1899f2f4a Threading support 2017-07-04 01:09:12 +07:00
Belén Curcio 4d821afa6a Merge branch 'master' into disable-plugin-components-via-config 2017-07-03 15:08:36 -03:00
Belen Curcio 002dc377a6 Featured Comments container, and FeaturedComment component 2017-07-03 13:31:13 -03:00
Belen Curcio fbdb044551 Moving withTags to the plugin api, adding perms to Feature comments 2017-07-03 11:55:09 -03:00
Belen Curcio 23ebe9f8e3 Translations and functionality 2017-07-03 11:46:34 -03:00
David Erwin c16278221c Merge pull request #719 from coralproject/docs-contribution-guide
Rewrite contributor's guide
2017-07-03 10:24:28 -04:00
David Erwin a50b39091f ABC Always Be Clarifying 2017-07-03 10:13:09 -04:00
David Erwin 4c7506e4f5 Merge branch 'docs-contribution-guide' of https://github.com/coralproject/talk into docs-contribution-guide 2017-07-03 10:06:56 -04:00
David Erwin 2f06281439 Move contributor's guide 2017-07-03 10:03:51 -04:00
Belen Curcio e3aecd645e Ádding withTags hoc, and creating withTags buttons 2017-07-03 10:16:35 -03:00
Belen Curcio b8020d5347 Adding talk-plugins-featured-comments to the default json 2017-07-03 10:15:57 -03:00
Chi Vinh Le fd1f8b24c1 Don't show popup when empty 2017-07-03 19:40:26 +07:00
Belen Curcio 95e820e9de Creating Tag Featured 2017-07-03 09:25:18 -03:00
Chi Vinh Le 270c3b7634 Disable plugin components via plugin_config 2017-07-03 19:01:17 +07:00
Kim Gardner e4647cab98 Merge branch 'master' into docs-contribution-guide 2017-07-03 12:12:56 +01:00
Kim Gardner 4319bb9da3 Merge branch 'master' into docs-contribution-guide 2017-06-29 17:31:33 +01:00
Belén Curcio d55fc6b601 Merge branch 'master' into docs-contribution-guide 2017-06-28 12:08:40 -03:00
David Erwin 3695ce5cb7 Update links 2017-06-27 15:08:21 -04:00
David Erwin d68169757d Rewrite contributor's guide 2017-06-27 15:04:00 -04:00
gaba d22f5215ed Adding french translation to the auth plugin. 2017-06-09 16:09:13 -07:00
gaba 19b6dfaa0a Adding a new language is not so straight forward. It needs to change at some point. 2017-06-08 11:39:51 -07:00
gaba b009d7a67a Adding french translations. 2017-06-08 10:47:17 -07:00
168 changed files with 4750 additions and 1398 deletions
+2
View File
@@ -13,4 +13,6 @@ plugins/*
!plugins/coral-plugin-viewing-options
!plugins/coral-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
node_modules
+1
View File
@@ -26,5 +26,6 @@ plugins/*
!plugins/coral-plugin-viewing-options
!plugins/coral-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
**/node_modules/*
+77
View File
@@ -0,0 +1,77 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We expect everyone contributing to The Coral Project to follow this code of conduct. That means the team, contractors we employ, contributors, as well as anyone posting to our public or internal-facing channels.
We created it not because we anticipate any unacceptable behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team, and because having a code provides us with clear avenues to correct our culture should it ever stray from that course.
We commit to enforce and evolve this code over the duration of the project.
## Expected behavior
* Be supportive of each other.
* Be collaborative. Involve others in brainstorms, sketching sessions, code reviews, planning documents, and the like. Its not only okay to ask for help or feedback often, its unacceptable not to do so.
* Be generous and kind in both giving and accepting critique. Critique is a natural and important part of our culture. Good critiques are kind, respectful, clear, and constructive, focused on goals and requirements rather than personal preferences. You are expected to give and receive criticism with grace.
* Be humane. Be polite and friendly in all forms of communication, especially remote communication, where opportunities for misunderstanding are greater. Use sarcasm carefully. Tone is hard to decipher online; make judicious use of emoji to aid in communication.
* Be considerate.
* Be tolerant.
* Respect peoples boundaries.
* Do not make it personal.
* Use welcoming and inclusive language.
* Offer to help if you see someone struggling or otherwise in need of assistance (taking care not to be patronizing or disrespectful).
* If someone approaches you looking for help, be generous with your time; if youre under a deadline, direct them to someone else who may be of assistance.
* Go out of your way to include people in jokes or memes, recognizing that we want to build an environment free of cliques.
* Show empathy towards other community members
## Unacceptable behavior
We are committed to providing a welcoming and safe environment for people of all races, gender identities, gender expressions, sexual orientations, physical abilities, physical appearances, socioeconomic backgrounds, nationalities, ages, religions, and beliefs.
We expect that you will refrain from demeaning, discriminatory, or harassing behavior and speech.
Harassment includes, but is not limited to: deliberate intimidation; stalking; unwanted photography or recording; sustained or willful disruption of talks or other events; inappropriate physical contact; use of sexual or discriminatory imagery, comments, or jokes; and unwelcome sexual attention.
Furthermore, any behavior or language which is unwelcoming—whether or not it rises to the level of harassment—is also strongly discouraged. Much exclusionary behavior takes the form of microaggressions—subtle put-downs which may be unconsciously delivered. Regardless of intent, microaggressions can have a significant negative impact on victims and have no place on our team.
Other inappropriate behavior:
* Threats
* Slurs
* Pornography
* Spam
* Bullying
* Copyright infringement
* Impersonation of someone else
* Violating someones privacy
If you feel that someone has harassed you or otherwise treated you or someone else inappropriately, please alert the project lead at andrewl@mozillafoundation.org.
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
These guidelines are ambitious, and were not always going to succeed in meeting them. When something goes wrong—whether its a microaggression or an instance of harassment — there are a number of things you can do to address the situation. Depending on your comfort level and the severity of the situation, here are some suggestions:
* Address it directly. If youre comfortable bringing up the incident with the person who instigated it, pull them aside to discuss how it affected you. Be sure to approach these conversations in a forgiving spirit: an angry or tense conversation will not do either of you any good. If youre unsure how to go about that, try discussing with your manager or with the people and culture team first—they might have some advice about how to make this conversation happen.
If youre too frustrated to have a direct conversation, there are a number of alternate routes you can take.
* Talk to a peer or mentor. Your colleagues are likely to have personal and professional experience on which to draw that could be of use to you. If you have someone youre comfortable approaching, reach out and discuss the situation with them. They may be able to advise on how they would handle it, or direct you to someone who can. The flip side of this, of course, is that you should also be available when your colleagues reach out to you.
* Contact the project lead, Andrew Losowsky, andrewl@mozillafoundation.org, or the technical lead. We will work with you to help you figure out how to ensure that any conflict doesnt interfere with your work, in confidence if you would prefer.
* Talk to Chris Lawrence. Chris oversees the project. He can be contacted at clawrence@mozillafoundation.org.
If you feel you have been unfairly accused of violating this code of conduct, you should contact Chris with a concise description of your grievance.
## Conclusion
We welcome your feedback on this and every other aspect of what we do as The Coral Project, and we thank you for working with us to make it a safe, enjoyable, and friendly experience for everyone involved in the project and what we do.
Above text is licensed CC BY-SA 4.0, adapted from the SRCCON code of conduct, FreeBSDs code of conduct, Vox Medias product team code of conduct, Mediums code of conduct, as well as adapted from the Contributor Covenant.
+51 -42
View File
@@ -6,16 +6,46 @@ This document is a companion to help you approach contributing. If it does not d
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
## Product Roadmap
## What should I Contribute?
You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625.
There are at least three ways to contribute to Talk:
If you'd like to see our longer term roadmap, [get in touch.](https://coralproject.net/contact.html)
* Writing Code
* Writing Docs
* Providing Translations
## Writing Code
## Contribute to the documentation
Conversation surrounding contributions begins in [issues](https://github.com/coralproject/talk/issues).
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
### When should I Create an Issue?
File an issue as soon as you have an idea of something you'd like to contribute. We would love to hear what you're thinking and help refine the idea to make it into the Talk ecosystem.
Please file issues if:
* you would like to contribute to Talk Core.
* you are building a Plugin that the current Plugin API doesn't support.
* you are building a Plugin and want advice.
### What should I include?
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repo should start with a story or this form:
`As a [type of person] I'd like to be able to [do something] so that I can [get some result].`
This exercise does two things:
* allows us to ground our technical choices in a clear, simple product need.
* expresses that product need in a way that doesn't imply a specific technical solution allowing for debate as to the best way to solve the problem.
Please feel free to provide as much detail as possible when filing the issue but please do keep the initial issue specific to one need and try to avoid including technical or design solutions.
If you have a specific technical or design solution in mind, please submit it as the first comment on the thread.
## Contributing Documentation
Clear docs are a prerequisite for a successful open source project. Contributing to the documentation is often more important than contributing to the code!
We are looking for _documentarians_ to:
@@ -23,55 +53,34 @@ We are looking for _documentarians_ to:
* create new / missing sections, and
* take the lead in making sections, or the over all structure better.
### But how?
Information about how to update docs can be found in our [FAQ](faq.html#how-do-i-contribute-to-these-docs).
* Our public docs site can be updated [here](https://github.com/coralproject/docs).
* [Let us know](https://github.com/coralproject/talk/wiki/Contact-Us) if you'd like permission to update our wiki.
* Update any of our .md docs files by following [this guide](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging).
* Our API docs need to be kept honest. [Update them here](https://github.com/coralproject/talk/blob/master/docs/swagger.yaml).
If you'd like to discuss a contribution, please [file an issue](https://github.com/coralproject/talk/issues) describing the changes you would like to see.
## Integrate into your environment
## Contributing Translations
First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes!
Talk's tranlations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
Talk is designed to integrate into existing environments in a variety of ways:
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact.html) the translations to us. We can import it into the repo.
* [Auth integrations](https://github.com/coralproject/talk/wiki/Security#authentication-strategies)
* [Push assets into Talk](https://github.com/coralproject/talk/blob/master/routes/api/assets/index.js)
* Monitoring Hooks (coming in 2017)
## I want to contribute but I'm not sure what to do!
If you're considering deploying Talk, [please let us know](https://github.com/coralproject/talk/wiki/Contact-Us)! We are quite literally doing this for you and want to help you succeed any way we can.
If you want to contribute but don't have a clear idea of exactly what that may be, here are some resources that may help:
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
### Product Roadmap
## Write some code
Please visit our product roadmap here: https://www.pivotaltracker.com/n/projects/1863625. If you'd like to take on any of our scheduled tasks we'd be forever grateful!
First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes!
### Discussion Forum
### Build a New Feature / Plugin
If you'd like to discuss what we're up to, please visit or [community](https://community.coralproject.net/) or [contact us](https://coralproject.net/contact.html).
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
### Integrations
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
Have a favorite analytics engine? Data science service? CMS? Auth platform? Deployment platform or pipeline? Pet project? Consider building a plugin to integrate them!
To get an idea of where the Coral Team is going, see:
### Favorite Features?
* 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).
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
Examples:
* [Add An Emoji Button to Comments](https://github.com/coralproject/talk/wiki/Add-An-Emoji-Button-to-Comments)
### Work on the Core
There is always more work to be done to make an application more stable, scaleable and secure.
If you see issues in the code or have ideas on how we may improve Talk, please consider:
* [contributing a fix](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging),
* [filing an issue](https://github.com/coralproject/talk/issues), or
* or otherwise [letting us know](https://github.com/coralproject/talk/wiki/Contact-Us).
## Thanks!
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:7.8
FROM node:7.10.1
# Create app directory
RUN mkdir -p /usr/src/app
+6 -1
View File
@@ -19,6 +19,10 @@ See our [Talk Documentation & Guides](https://coralproject.github.io/talk/index.
See our guide to using and building [Talk Plugins](https://github.com/coralproject/talk/blob/master/PLUGINS.md).
### Recipes
Recipes are plugin templates provided by the Coral Core team. Developers can use these recipes to build their own plugins. You can find all the Talk recipes here: https://github.com/coralproject/talk-recipes/
## Usage
### Installation
@@ -46,11 +50,12 @@ sign and verify tokens via a `HS256` algorithm.
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - When `TRUE`, disables the dynamic setup endpoint. (Default `FALSE`)
- `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
- `TALK_KEEP_ALIVE` (_optional_) - The keepalive timeout that should be used to send keep alive messages through the websocket to keep the socket alive. (Default `30s`)
- `TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS` (_optional_) When `TRUE`, disables flagging of comments that match the suspect word filter. (Default `FALSE`)
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
+9 -2
View File
@@ -9,13 +9,17 @@ const program = require('./commander');
program
.command('serve', 'serve the application')
.command('settings', 'interact with the application settings')
.command('assets', 'interact with assets')
.command('setup', 'setup the application')
.command('jobs', 'work with the job queues')
.command('token', 'work with the access tokens')
.command('users', 'work with the application auth')
.command('migration', 'provides utilities for migrating the database')
.command('plugins', 'provides utilities for interacting with the plugin system')
.command(
'plugins',
'provides utilities for interacting with the plugin system'
)
.parse(process.argv);
/**
@@ -25,7 +29,10 @@ program
* labled with the PID written out by the parent process.
*/
process.once('exit', () => {
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
if (
program.runningCommand.killed === false &&
program.runningCommand.exitCode === null
) {
program.runningCommand.kill('SIGINT');
}
});
+120 -1
View File
@@ -8,13 +8,14 @@
// https://yarnpkg.com/
const program = require('./commander');
const inquirer = require('inquirer');
// Make things colorful!
require('colors');
const emoji = require('node-emoji');
const dir = process.cwd();
const fs = require('fs');
const fs = require('fs-extra');
const path = require('path');
const spawn = require('cross-spawn');
const semver = require('semver');
@@ -272,10 +273,128 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote
console.log(`✨ Done in ${totalTime}s.`);
}
async function createSeedPlugin() {
const pluginsDir = path.join(__dirname, 'plugins');
function pluginNameExists(pluginName) {
const pluginNames = fs.readdirSync(pluginsDir);
return !!pluginNames
.filter((pn) => pn === pluginName).length;
}
let answers = await inquirer.prompt([
{
type: 'input',
name: 'pluginName',
message: 'Plugin Name:',
validate: (input) => {
if (pluginNameExists(input)) {
return 'Please, choose another name. That name already exists';
}
if (input && input.length > 0) {
return true;
}
return 'Plugin Name is required.';
}
},
{
type: 'confirm',
name: 'server',
message: 'Is this plugin extending the server capabilities?'
},
{
type: 'confirm',
name: 'client',
message: 'Is this plugin extending the client capabilities?'
},
{
type: 'confirm',
name: 'addPluginsJson',
message: 'Should we add it to the plugins.json?'
}
]);
//==============================================================================
// Creating plugin seed
//==============================================================================
const seedPlugin = path.join(__dirname, 'bin/templates/plugin');
const newPluginPath = path.join(pluginsDir, answers.pluginName);
if (fs.existsSync(seedPlugin)) {
if (answers.server && answers.client) {
// This is a server-side and client-side plugin!, let's copy the template
fs.copySync(seedPlugin, newPluginPath);
} else {
fs.copySync(seedPlugin, newPluginPath, {filter: (p) => {
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/igm;
if (rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())) {
return true;
}
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
}});
}
// Let's add this to the plugins.json
if (answers.addPluginsJson) {
const pluginsJson = path.join(dir, 'plugins.json');
fs.readJson(pluginsJson)
.then((j) => {
// This is a client-side plugin, let's push this.
if (answers.client) {
j.client.push(answers.pluginName);
const output = JSON.stringify(j, null, 2);
fs.writeFileSync(pluginsJson, output);
}
// This is a server-side plugin, let's push this.
if (answers.server) {
j.server.push(answers.pluginName);
const output = JSON.stringify(j, null, 2);
fs.writeFileSync(pluginsJson, output);
}
})
.catch((err) => {
console.error(err);
});
}
console.log(`✨ Yay! Plugin created! Find your plugin: ${answers.pluginName} in the ./plugins folder`);
}
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('create')
.description('creates a seed plugin')
.action(createSeedPlugin);
program
.command('list')
.description('')
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env node
const program = require('./commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingsService = require('../services/settings');
const util = require('./util');
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
/**
* Change the organization name
*/
async function changeOrgName() {
try {
let settings = await SettingsService.retrieve();
let {organizationName} = await inquirer.prompt([
{
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName
}
]);
if (settings.organizationName !== organizationName) {
settings.organizationName = organizationName;
await SettingsService.update(settings);
console.log('Settings were updated.');
} else {
console.log('No update needed, no change was made.');
}
} catch (err) {
console.error(err);
util.shutdown(1);
}
util.shutdown();
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('change-org-name')
.description('change the organization name')
.action(changeOrgName);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
util.shutdown();
}
+14
View File
@@ -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,27 @@
.myPluginContainer {
padding: 10px;
background: #f0f0f0;
border: 1px solid #d6d6d6;
margin: 10px 0;
text-align: center;
border-radius: 3px;
}
.logo {
position: block;
animation: spin 2s infinite ease;
animation-delay: 1s;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.description {
color: #444444;
}
@@ -0,0 +1,25 @@
import React from 'react';
import {CoralLogo} from 'plugin-api/beta/client/components/ui';
import styles from './MyPluginComponent.css';
class MyPluginComponent extends React.Component {
render() {
return (
<div className={styles.myPluginContainer}>
<CoralLogo className={styles.logo}/>
<div className={styles.description}>
<h3>Plugin created by Talk CLI</h3>
<small>
To read more about plugins check{' '}
<a href="https://coralproject.github.io/talk/plugins-client.html">
our docs and guides!
</a>
</small>
</div>
</div>
);
}
}
export default MyPluginComponent;
+26
View File
@@ -0,0 +1,26 @@
/**
This is a client index example file and it could look like this:
```
import LoveButton from './components/LoveButton';
export default {
slots: {
commentReactions: [LoveButton]
},
reducer,
translations
};
```
To read more info on how to build client plugins. Please, go to: https://coralproject.github.io/talk/plugins-client.html
*/
import MyPluginComponent from './components/MyPluginComponent';
export default {
slots: {
stream: [MyPluginComponent]
}
};
@@ -0,0 +1,15 @@
#
# This file is for translations and they should look like this:
#
#
# ```
# en:
# coral-plugin-respect:
# respect: Respect
# respected: Respected
# es:
# coral-plugin-respect:
# respect: Respetar
# respected: Respetado
# ```
#
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+1 -1
View File
@@ -1,6 +1,6 @@
machine:
node:
version: 7.9
version: 7.10.1
services:
- docker
- redis
@@ -74,7 +74,7 @@
cursor: pointer;
display: block;
text-decoration: none;
height: 50px;
min-height: 50px;
box-sizing: border-box;
transition: background-color 400ms;
@@ -1,22 +1,21 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
const pluginFragments = getSlotsFragments([
const slots = [
'adminCommentInfoBar',
'adminCommentContent',
'adminSideActions',
'adminCommentDetailArea',
]);
];
export default withFragments({
root: gql`
fragment CoralAdmin_ModerationComment_root on RootQuery {
__typename
${pluginFragments.spreads('root')}
${getSlotFragmentSpreads(slots, 'root')}
}
${pluginFragments.definitions('root')}
`,
comment: gql`
fragment CoralAdmin_ModerationComment_comment on Comment {
@@ -52,8 +51,7 @@ export default withFragments({
editing {
edited
}
${pluginFragments.spreads('comment')}
${getSlotFragmentSpreads(slots, 'comment')}
}
${pluginFragments.definitions('comment')}
`
})(Comment);
@@ -4,8 +4,7 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import {getDefinitionName} from 'coral-framework/utils';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
changeUserDetailStatuses,
clearUserDetailSelections,
@@ -26,9 +25,9 @@ const commentConnectionFragment = gql`
${Comment.fragments.comment}
`;
const pluginFragments = getSlotsFragments([
const slots = [
'userProfile',
]);
];
class UserDetailContainer extends React.Component {
static propTypes = {
@@ -80,7 +79,7 @@ export const withUserDetailQuery = withQuery(gql`
id
provider
}
${pluginFragments.spreads('user')}
${getSlotFragmentSpreads(slots, 'user')}
}
totalComments: commentCount(query: {author_id: $author_id})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
@@ -90,11 +89,8 @@ export const withUserDetailQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
${pluginFragments.spreads('root')}
${getSlotFragmentSpreads(slots, 'root')}
}
${Comment.fragments.comment}
${pluginFragments.definitions('user')}
${pluginFragments.definitions('root')}
${commentConnectionFragment}
`, {
options: ({id, moderation: {userDetailStatuses: statuses}}) => {
+36 -21
View File
@@ -1,35 +1,24 @@
import {pym} from 'coral-framework';
import pym from 'coral-framework/services/pym';
import * as actions from '../constants/stream';
import {buildUrl} from 'coral-framework/utils';
import queryString from 'query-string';
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
function removeParam(key, sourceURL) {
let rtn = sourceURL.split('?')[0];
let param;
let params_arr = [];
let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : '';
if (queryString !== '') {
params_arr = queryString.split('&');
for (let i = params_arr.length - 1; i >= 0; i -= 1) {
param = params_arr[i].split('=')[0];
if (param === key) {
params_arr.splice(i, 1);
}
}
rtn = `${rtn}?${params_arr.join('&')}`;
}
return rtn;
}
export const viewAllComments = () => {
const search = queryString.stringify({
...queryString.parse(location.search),
comment_id: undefined,
});
// remove the comment_id url param
const modifiedUrl = removeParam('comment_id', location.href);
const url = buildUrl({...location, search});
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, modifiedUrl);
window.history.replaceState({}, document.title, url);
// also change the parent url
pym.sendMessage('coral-view-all-comments');
@@ -38,6 +27,28 @@ export const viewAllComments = () => {
return {type: actions.VIEW_ALL_COMMENTS};
};
export const viewComment = (id) => {
const search = queryString.stringify({
...queryString.parse(location.search),
comment_id: id,
});
// remove the comment_id url param
const url = buildUrl({...location, search});
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, url);
// also change the parent url
pym.sendMessage('coral-view-comment', id);
} catch (e) { /* not sure if we're worried about old browsers */ }
return {type: actions.VIEW_COMMENT, id};
};
export const addCommentClassName = (className) => ({
type: actions.ADD_COMMENT_CLASSNAME,
className
@@ -47,3 +58,7 @@ export const removeCommentClassName = (idx) => ({
type: actions.REMOVE_COMMENT_CLASSNAME,
idx
});
export const setActiveTab = (tab) => (dispatch) => {
dispatch({type: actions.SET_ACTIVE_TAB, tab});
};
@@ -0,0 +1,198 @@
import React from 'react';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import {TransitionGroup} from 'react-transition-group';
import {forEachError} from 'coral-framework/utils';
import Comment from '../components/Comment';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
const comments = props.root.asset.comments;
if (comments && comments.nodes.length) {
const idCursors = [comments.nodes[0].id];
if (comments.nodes[1]) {
idCursors.push(comments.nodes[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const comments = props.root.asset.comments;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
const nextInLine = comments.nodes[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
class AllCommentsPane extends React.Component {
constructor(props) {
super(props);
this.state = {
...resetCursors(this.state, props),
loadingState: '',
};
}
componentWillReceiveProps(next) {
const {comments: prevComments} = this.props;
const {comments: nextComments} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
}
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
}
}
}
loadMore = () => {
this.setState({loadingState: 'loading'});
this.props.loadMore()
.then(() => {
this.setState({loadingState: 'success'});
})
.catch((error) => {
this.setState({loadingState: 'error'});
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
});
}
viewNewComments = () => {
this.setState(resetCursors);
};
// getVisibileComments returns a list containing comments
// which were authored by current user or comes after the `idCursor`.
getVisibleComments() {
const {comments, currentUser: user} = this.props;
const idCursor = this.state.idCursors[0];
const userId = user ? user.id : null;
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.nodes.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor || comment.user.id === userId) {
view.push(comment);
}
});
return view;
}
render() {
const {
data,
root,
comments,
commentClassNames,
ignoreUser,
setActiveReplyBox,
activeReplyBox,
addNotification,
disableReply,
postComment,
asset,
currentUser,
postFlag,
postDontAgree,
loadNewReplies,
deleteAction,
showSignInDialog,
commentIsIgnored,
charCountEnable,
maxCharCount,
editComment,
} = this.props;
const {loadingState} = this.state;
const view = this.getVisibleComments();
return (
<div className="talk-stream-comments-container">
<NewCount
count={comments.nodes.length - view.length}
loadMore={this.viewNewComments}
/>
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
commentClassNames={commentClassNames}
data={data}
root={root}
disableReply={disableReply}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
depth={0}
postComment={postComment}
asset={asset}
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
comment={comment}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
editComment={editComment}
/>;
})}
</TransitionGroup>
<LoadMore
topLevel={true}
moreComments={asset.comments.hasNextPage}
loadMore={this.loadMore}
loadingState={loadingState}
/>
</div>
);
}
}
export default AllCommentsPane;
@@ -1,11 +1,48 @@
.Reply {
.root {
margin-left: 20px;
margin-bottom: 16px;
position: relative;
margin-bottom: 15px;
padding-top: 12px;
}
.Comment {
margin-bottom: 15px;
.rootLevel0:first-child {
padding-top: 0;
border: 0;
}
.rootLevel0 {
margin-left: 0px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.comment {
position: relative;
padding-left: 15px;
}
.commentLevel0 {
padding-left: 0px;
}
.commentLevel1 {
border-left: 3px #212121 solid;
}
.commentLevel2 {
border-left: 2px #6a6a6a solid;
}
.commentLevel3 {
border-left: 2px #9e9e9e solid;
}
.commentLevel4 {
border-left: 2px #c1c1c1 solid;
}
.highlightedComment {
padding-left: 15px;
border-left: 3px solid rgb(35,118,216);
}
.pendingComment {
@@ -22,15 +59,11 @@
font-style: italic;
}
/* element in the top right of the Comment */
.topRight {
float: right;
margin-top: 10px;
text-align: right;
}
.topRight > * {
text-align: initial;
.hr {
border: 0;
height: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.topRight .popover {
@@ -44,11 +77,6 @@
border-bottom: 2px solid currentColor;
}
.topRightMenu {
cursor: pointer;
margin-top: 5px;
}
.editCommentForm {
margin-bottom: 10px;
}
@@ -99,7 +127,7 @@
}
.commentInfoBar {
float: right;
margin-left: auto;
}
@keyframes enter {
@@ -111,3 +139,35 @@
.enter {
animation: enter 1000ms;
}
.commentContainer {
}
.commentAvatar {
max-width: 60px;
}
.commentAvatar:empty {
display: none;
}
.timerIcon {
vertical-align: middle;
font-size: 14px;
}
.commentFooter {
padding-top: 8px;
}
.header {
display: flex;
align-items: center;
}
.content {
}
.footer {
}
@@ -1,4 +1,5 @@
import React, {PropTypes} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import TagLabel from 'coral-plugin-tag-label/TagLabel';
@@ -9,14 +10,10 @@ import {can} from 'coral-framework/services/perms';
import {TransitionGroup} from 'react-transition-group';
import cn from 'classnames';
import styles from './Comment.css';
import {THREADING_LEVEL} from '../constants/stream';
import merge from 'lodash/merge';
import mapValues from 'lodash/mapValues';
import {
BestButton,
IfUserCanModifyBest,
BEST_TAG,
commentIsBest,
BestIndicator
} from 'coral-plugin-best/BestButton';
import LoadMore from './LoadMore';
import {getEditableUntilDate} from './util';
import {TopRightMenu} from './TopRightMenu';
@@ -131,7 +128,6 @@ export default class Comment extends React.Component {
}
static propTypes = {
reactKey: PropTypes.string.isRequired,
// id of currently opened ReplyBox. tracked in Stream.js
activeReplyBox: PropTypes.string.isRequired,
@@ -145,12 +141,8 @@ export default class Comment extends React.Component {
addNotification: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
liveUpdates: PropTypes.bool.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
url: PropTypes.string
}).isRequired,
liveUpdates: PropTypes.bool,
asset: PropTypes.object.isRequired,
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
@@ -180,19 +172,13 @@ export default class Comment extends React.Component {
}).isRequired,
// given a comment, return whether it should be rendered as ignored
commentIsIgnored: React.PropTypes.func,
// dispatch action to add a tag to a comment
addTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeTag: React.PropTypes.func,
commentIsIgnored: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
ignoreUser: PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: React.PropTypes.func,
editComment: PropTypes.func,
}
editComment = (...args) => {
@@ -305,6 +291,8 @@ export default class Comment extends React.Component {
render () {
const {
asset,
data,
root,
depth,
comment,
postFlag,
@@ -316,14 +304,13 @@ export default class Comment extends React.Component {
postDontAgree,
setActiveReplyBox,
activeReplyBox,
loadMore,
deleteAction,
disableReply,
maxCharCount,
addNotification,
charCountEnable,
showSignInDialog,
addTag,
removeTag,
liveUpdates,
commentIsIgnored,
commentClassNames = []
@@ -331,9 +318,11 @@ export default class Comment extends React.Component {
const view = this.getVisibileReplies();
const {loadingState} = this.state;
const isPending = comment.id.indexOf('pending') >= 0;
const isHighlighted = highlighted === comment.id;
const hasMoreComments = comment.replies && (comment.replies.hasNextPage || comment.replies.nodes.length > view.length);
const replyCount = this.hasIgnoredReplies() ? '' : comment.replyCount;
const moreRepliesCount = this.hasIgnoredReplies() ? -1 : comment.replyCount - view.length;
const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary(
'DontAgreeActionSummary',
@@ -346,47 +335,8 @@ export default class Comment extends React.Component {
myFlag = dontAgreeSummary.find((s) => s.current_user);
}
let commentClass = parentId
? `reply ${styles.Reply}`
: `comment ${styles.Comment}`;
commentClass += comment.id.indexOf('pending') >= 0 ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) =>
async function(...args) {
if (typeof errorToMessage !== 'function') {
errorToMessage = (error) => error.message;
}
try {
return await fn(...args);
} catch (error) {
addNotification('error', errorToMessage(error));
throw error;
}
};
const addBestTag = notifyOnError(
() =>
addTag({
id: comment.id,
name: BEST_TAG,
assetId: asset.id
}),
() => 'Failed to tag comment as best'
);
const removeBestTag = notifyOnError(
() =>
removeTag({
id: comment.id,
name: BEST_TAG,
assetId: asset.id
}),
() => 'Failed to remove best comment tag'
);
/**
* classNamesToAdd
* conditionClassNames
* adds classNames based on condition
* classnames is an array of objects with key as classnames and value as conditions
* i.e:
@@ -396,155 +346,169 @@ export default class Comment extends React.Component {
*
* This will add myClassName to comments tagged with STAFF TAG.
* **/
const classNamesToAdd = commentClassNames.reduce((acc, className) => {
let res = [];
// Adding classNames based on tags
Object.keys(className).forEach((cn) => {
const condition = className[cn];
condition.tags.forEach((tag) => {
if (hasTag(comment.tags, tag)) {
res = [...res, cn];
}
});
const conditionalClassNames =
mapValues(merge({}, ...commentClassNames), (condition) => {
if (condition.tags) {
return condition.tags.some((tag) => hasTag(comment.tags, tag));
}
return false;
});
// TODO: Compare rest of the comment obj to the condition if needed
const rootClassName = cn(
'talk-stream-comment-wrapper',
`talk-stream-comment-wrapper-level-${depth}`,
styles.root,
styles[`rootLevel${depth}`],
{
...conditionalClassNames,
[styles.enter]: this.state.animateEnter,
},
);
return [...acc, ...res];
}, []);
const commentClassName = cn(
'talk-stream-comment',
`talk-stream-comment-level-${depth}`,
styles.comment,
styles[`commentLevel${depth}`],
{
[styles.pendingComment]: isPending,
[styles.highlightedComment]: isHighlighted,
'talk-stream-pending-comment': isPending,
'talk-stream-highlighted-comment': isHighlighted,
}
);
// props that are passed down the slots.
const slotProps = {
data,
root,
asset,
comment,
depth,
};
return (
<div
className={cn(commentClass, 'talk-stream-comment-wrapper', classNamesToAdd, {[styles.enter]: this.state.animateEnter})}
className={rootClassName}
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}
>
<hr aria-hidden={true} />
<div
className={highlighted === comment.id ? 'highlighted-comment' : ''}
>
<AuthorName author={comment.user} className={'talk-stream-comment-user-name'} />
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
{commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel>
: null }
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
</span>
<div className={commentClassName}>
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
depth={depth}
comment={comment}
commentId={comment.id}
data={this.props.data}
root={this.props.root}
className={styles.commentAvatar}
fill="commentAvatar"
{...slotProps}
inline
/>
{ (currentUser && (comment.user.id === currentUser.id)) &&
<div className={styles.commentContainer}>
/* User can edit/delete their own comment for a short window after posting */
<span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
}
{ (currentUser && (comment.user.id !== currentUser.id)) &&
<div className={styles.header}>
<AuthorName author={comment.user} className={'talk-stream-comment-user-name'} />
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
/* TopRightMenu allows currentUser to ignore other users' comments */
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
</span>
}
{
this.state.isEditing
? <EditableCommentContent
editComment={this.editComment}
addNotification={addNotification}
asset={asset}
comment={comment}
currentUser={currentUser}
maxCharCount={maxCharCount}
parentId={parentId}
stopEditing={this.stopEditing}
/>
: <div>
<Slot fill="commentContent" comment={comment} defaultComponent={CommentContent} />
</div>
}
<div className="commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
data={this.props.data}
root={this.props.root}
comment={comment}
commentId={comment.id}
inline
/>
<ActionButton>
<IfUserCanModifyBest user={currentUser}>
<BestButton
isBest={commentIsBest(comment)}
addBest={addBestTag}
removeBest={removeBestTag}
/>
</IfUserCanModifyBest>
</ActionButton>
{!disableReply &&
<ActionButton>
<ReplyButton
onClick={this.showReplyBox}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
/>
</ActionButton>}
</div>
<div className="commentActionsRight comment__action-container">
<Slot
fill="commentActions"
wrapperComponent={ActionButton}
data={this.props.data}
root={this.props.root}
asset={asset}
comment={comment}
commentId={comment.id}
inline
/>
<ActionButton>
<FlagComment
flaggedByCurrentUser={!!myFlag}
flag={myFlag}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
addNotification={addNotification}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser}
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
{...slotProps}
/>
</ActionButton>
{ (currentUser && (comment.user.id === currentUser.id)) &&
/* User can edit/delete their own comment for a short window after posting */
<span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
}
{ (currentUser && (comment.user.id !== currentUser.id)) &&
/* TopRightMenu allows currentUser to ignore other users' comments */
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
</span>
}
</div>
<div className={styles.content}>
{
this.state.isEditing
? <EditableCommentContent
editComment={this.editComment}
addNotification={addNotification}
comment={comment}
currentUser={currentUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
parentId={parentId}
stopEditing={this.stopEditing}
/>
: <div>
<Slot
fill="commentContent"
defaultComponent={CommentContent}
{...slotProps}
/>
</div>
}
</div>
<div className={styles.footer}>
<div className="commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
{...slotProps}
inline
/>
{!disableReply &&
<ActionButton>
<ReplyButton
onClick={this.showReplyBox}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
/>
</ActionButton>}
</div>
<div className="commentActionsRight comment__action-container">
<Slot
fill="commentActions"
wrapperComponent={ActionButton}
{...slotProps}
inline
/>
<ActionButton>
<FlagComment
flaggedByCurrentUser={!!myFlag}
flag={myFlag}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
addNotification={addNotification}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser}
/>
</ActionButton>
</div>
</div>
</div>
</div>
{activeReplyBox === comment.id
? <ReplyBox
commentPostedHandler={() => {
@@ -553,7 +517,7 @@ export default class Comment extends React.Component {
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
setActiveReplyBox={setActiveReplyBox}
parentId={parentId || comment.id}
parentId={(depth < THREADING_LEVEL) ? comment.id : parentId}
addNotification={addNotification}
postComment={postComment}
currentUser={currentUser}
@@ -581,8 +545,7 @@ export default class Comment extends React.Component {
currentUser={currentUser}
postFlag={postFlag}
deleteAction={deleteAction}
addTag={addTag}
removeTag={removeTag}
loadMore={loadMore}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
@@ -598,7 +561,7 @@ export default class Comment extends React.Component {
<div className="talk-load-more-replies">
<LoadMore
topLevel={false}
replyCount={replyCount}
replyCount={moreRepliesCount}
moreComments={hasMoreComments}
loadMore={this.loadNewReplies}
loadingState={loadingState}
@@ -18,11 +18,6 @@ export class EditableCommentContent extends React.Component {
// show notification to the user (e.g. for errors)
addNotification: PropTypes.func.isRequired,
asset: PropTypes.shape({
settings: PropTypes.shape({
charCountEnable: PropTypes.bool,
}),
}).isRequired,
// comment that is being edited
comment: PropTypes.shape({
@@ -39,6 +34,7 @@ export class EditableCommentContent extends React.Component {
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
// edit a comment, passed {{ body }}
@@ -121,7 +117,7 @@ export class EditableCommentContent extends React.Component {
<div className={styles.editCommentForm}>
<CommentForm
defaultValue={this.props.comment.body}
charCountEnable={this.props.asset.settings.charCountEnable}
charCountEnable={this.props.charCountEnable}
maxCharCount={this.props.maxCharCount}
submitEnabled={this.isSubmitEnabled}
body={this.state.body}
@@ -149,7 +145,7 @@ export class EditableCommentContent extends React.Component {
}
</span>
: <span>
<Icon name="timer"/> {t('edit_comment.edit_window_timer_prefix')}
<Icon name="timer" className={styles.timerIcon}/> {t('edit_comment.edit_window_timer_prefix')}
<CountdownSeconds
until={this.getEditableUntil()}
classNameForMsRemaining={(remainingMs) => (remainingMs <= 10 * 1000) ? styles.editWindowAlmostOver : '' }
@@ -4,67 +4,82 @@ import Slot from 'coral-framework/components/Slot';
import {can} from 'coral-framework/services/perms';
import t from 'coral-framework/services/i18n';
import {TabBar, Tab, TabContent, Button} from 'coral-ui';
import Count from 'coral-plugin-comment-count/CommentCount';
import {TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import Popup from 'coral-framework/components/Popup';
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
import ConfigureStreamContainer
from 'coral-configure/containers/ConfigureStreamContainer';
import cn from 'classnames';
export default class Embed extends React.Component {
changeTab = (tab) => {
switch (tab) {
case 0:
this.props.setActiveTab('stream');
break;
case 1:
this.props.setActiveTab('profile');
// TODO: move data fetching to profile container.
// TODO: move data fetching to appropiate containers.
switch (tab) {
case 'profile':
this.props.data.refetch();
break;
case 2:
this.props.setActiveTab('config');
// TODO: move data fetching to config container.
case 'config':
this.props.data.refetch();
break;
}
this.props.setActiveTab(tab);
};
handleShowProfile = () => this.props.setActiveTab('profile');
render() {
const {activeTab, viewAllComments, commentId} = this.props;
const {asset: {totalCommentCount}} = this.props.root;
const {activeTab, commentId, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props;
const {user} = this.props.auth;
const hasHighlightedComment = !!commentId;
return (
<div>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab} className='talk-stream-tabbar'>
<Tab className={'talk-stream-comment-count-tab'} id='stream'><Count count={totalCommentCount}/></Tab>
<Tab className={'talk-stream-profile-tab'} id='profile'>{t('framework.my_profile')}</Tab>
<Tab className={'talk-stream-configuration-tab'} id='config' restricted={!can(user, 'UPDATE_CONFIG')}>{t('framework.configure_stream')}</Tab>
</TabBar>
{commentId &&
<Button
cStyle="darkGrey"
style={{float: 'right'}}
onClick={viewAllComments}
>
{t('framework.show_all_comments')}
</Button>}
<Slot fill="embed" />
<TabContent show={activeTab === 'stream'}>
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
<IfSlotIsNotEmpty slot="login">
<Popup
href='/embed/stream/login'
title='Login'
features='menubar=0,resizable=0,width=500,height=550,top=200,left=500'
open={showSignInDialog}
focus={signInDialogFocus}
onFocus={focusSignInDialog}
onBlur={blurSignInDialog}
onClose={hideSignInDialog}
/>
</IfSlotIsNotEmpty>
<TabBar
onTabClick={this.changeTab}
activeTab={activeTab}
className='talk-embed-stream-tab-bar'
aria-controls='talk-embed-stream-tab-content'
>
<Tab tabId={'stream'} className={'talk-embed-stream-comments-tab'}>
{t('embed_comments_tab')}
</Tab>
<Tab tabId={'profile'} className={'talk-embed-stream-profile-tab'}>
{t('framework.my_profile')}
</Tab>
{can(user, 'UPDATE_CONFIG') &&
<Tab tabId={'config'} className={'talk-embed-stream-configuration-tab'}>
{t('framework.configure_stream')}
</Tab>
}
</TabBar>
<Slot fill="embed" />
<TabContent
activeTab={activeTab}
id='talk-embed-stream-tab-content'
>
<TabPane tabId={'stream'}>
<Stream data={this.props.data} root={this.props.root} />
</TabContent>
<TabContent show={activeTab === 'profile'}>
</TabPane>
<TabPane tabId={'profile'}>
<ProfileContainer />
</TabContent>
<TabContent show={activeTab === 'config'}>
</TabPane>
<TabPane tabId={'config'}>
<ConfigureStreamContainer />
</TabContent>
</div>
</TabPane>
</TabContent>
</div>
);
}
@@ -5,27 +5,15 @@ import t from 'coral-framework/services/i18n';
import cn from 'classnames';
class LoadMore extends React.Component {
initialState = true;
replyCountFormat = (count) => {
if (!count) {
return t('framework.view_all_replies_unknown_number');
return t('framework.show_all_replies');
}
if (count === 1) {
return t('framework.view_reply');
return t('framework.show_1_more_reply');
}
if (this.initialState) {
return t('framework.view_all_replies_initial', count);
} else {
return t('framework.view_all_replies', count);
}
}
componentWillReceiveProps(nextProps) {
if (['success', 'error'].indexOf(nextProps.loadingState) >= 0) {
this.initialState = false;
}
return t('framework.show_x_more_replies', count);
}
render () {
@@ -1,16 +1,17 @@
import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const NewCount = ({count, loadMore}) => {
return <div className='coral-new-comments coral-load-more'>
return <div className='talk-new-comments talk-load-more'>
{
count ?
<button onClick={loadMore}>
<Button onClick={loadMore}>
{count === 1
? t('framework.new_count', count, t('framework.comment'))
: t('framework.new_count', count, t('framework.comments'))}
</button>
</Button>
: null
}
</div>;
@@ -0,0 +1,29 @@
.root {
margin-top: 6px;
}
.viewAllButton {
position: absolute;
right: 0px;
}
.tabPanel {
margin-top: 8px;
}
.filterWrapper {
position: absolute;
right: 0;
margin-top: 6px;
}
.highlightedContainer {
margin-top: 28px;
}
.tabContainer {
position: relative;
margin-top: 28px;
padding-bottom: 50px;
min-height: 600px;
}
+150 -200
View File
@@ -1,5 +1,5 @@
import React, {PropTypes} from 'react';
import LoadMore from './LoadMore';
import React from 'react';
import PropTypes from 'prop-types';
import {StreamError} from './StreamError';
import Comment from '../components/Comment';
import SuspendedAccount from './SuspendedAccount';
@@ -10,168 +10,100 @@ import {ModerationLink} from 'coral-plugin-moderation';
import RestrictedMessageBox
from 'coral-framework/components/RestrictedMessageBox';
import t, {timeago} from 'coral-framework/services/i18n';
import {getSlotComponents} from 'coral-framework/helpers/plugins';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import {TransitionGroup} from 'react-transition-group';
import {forEachError} from 'coral-framework/utils';
import {Button, TabBar, Tab, TabCount, TabContent, TabPane} from 'coral-ui';
import cn from 'classnames';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
import {getTopLevelParent} from '../graphql/utils';
import AllCommentsPane from './AllCommentsPane';
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
const comments = props.root.asset.comments;
if (comments && comments.nodes.length) {
const idCursors = [comments.nodes[0].id];
if (comments.nodes[1]) {
idCursors.push(comments.nodes[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const comments = props.root.asset.comments;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
const nextInLine = comments.nodes[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
import styles from './Stream.css';
class Stream extends React.Component {
constructor(props) {
super(props);
this.state = {
...resetCursors(this.state, props),
keepCommentBox: false,
loadingState: '',
};
}
componentWillReceiveProps(next) {
const {root: {asset: {comments: prevComments}}} = this.props;
const {root: {asset: {comments: nextComments}}} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
}
// Keep comment box when user was live suspended, banned, ...
if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) {
this.setState({keepCommentBox: true});
}
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
) {
this.fallbackAllTab(next);
}
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
componentDidMount() {
this.fallbackAllTab();
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
fallbackAllTab(props = this.props) {
if (props.activeStreamTab !== 'all') {
const slotPlugins = this.getSlotComponents('streamTabs', props).map((c) => c.talkPluginName);
if (slotPlugins.indexOf(props.activeStreamTab) === -1) {
props.setActiveStreamTab('all');
}
}
}
viewNewComments = () => {
this.setState(resetCursors);
};
getSlotProps({data, root, root: {asset}} = this.props) {
return {data, root, asset};
}
setActiveReplyBox = (reactKey) => {
getSlotComponents(slot, props = this.props) {
return getSlotComponents(slot, props.reduxState, this.getSlotProps(props));
}
setActiveReplyBox = (id) => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
} else {
this.props.setActiveReplyBox(reactKey);
this.props.setActiveReplyBox(id);
}
};
loadMoreComments = () => {
this.setState({loadingState: 'loading'});
this.props.loadMoreComments()
.then(() => {
this.setState({loadingState: 'success'});
})
.catch((error) => {
this.setState({loadingState: 'error'});
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
});
}
// getVisibileComments returns a list containing comments
// which were authored by current user or comes after the `idCursor`.
getVisibleComments() {
const {root: {asset: {comments}}, auth: {user}} = this.props;
const idCursor = this.state.idCursors[0];
const userId = user ? user.id : null;
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.nodes.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor || comment.user.id === userId) {
view.push(comment);
}
});
return view;
}
userIsDegraged({auth: {user}} = this.props) {
return !can(user, 'INTERACT_WITH_COMMUNITY');
}
render() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
appendItemArray,
commentClassNames,
root: {asset, asset: {comments}, comment, me},
root: {asset, asset: {comments, totalCommentCount}, comment, me},
postComment,
addNotification,
editComment,
postFlag,
postDontAgree,
deleteAction,
showSignInDialog,
addTag,
updateItem,
ignoreUser,
activeStreamTab,
setActiveStreamTab,
loadNewReplies,
loadMoreComments,
viewAllComments,
auth: {loggedIn, user},
removeTag,
pluginProps,
editName
} = this.props;
const {keepCommentBox, loadingState} = this.state;
const view = this.getVisibleComments();
const {keepCommentBox} = this.state;
const open = asset.closedAt === null;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
const highlightedComment = comment && comment.parent
? comment.parent
: comment;
const highlightedComment = comment && getTopLevelParent(comment);
const banned = user && user.status === 'BANNED';
const temporarilySuspended =
@@ -188,6 +120,7 @@ class Stream extends React.Component {
};
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const slotProps = this.getSlotProps();
if (!comment && !comments) {
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
@@ -195,7 +128,15 @@ class Stream extends React.Component {
}
return (
<div id="stream">
<div id="stream" className={styles.root}>
{comment &&
<Button
cStyle="darkGrey"
className={cn('talk-stream-show-all-comments-button', styles.viewAllButton)}
onClick={viewAllComments}
>
{t('framework.show_all_comments')}
</Button>}
{open
? <div id="commentBox">
@@ -212,7 +153,7 @@ class Stream extends React.Component {
<RestrictedMessageBox>
{t(
'stream.temporarily_suspended',
this.props.root.settings.organizationName,
root.settings.organizationName,
timeago(user.suspension.until)
)}
</RestrictedMessageBox>}
@@ -224,10 +165,10 @@ class Stream extends React.Component {
/>}
{showCommentBox &&
<CommentBox
addNotification={this.props.addNotification}
postComment={this.props.postComment}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
addNotification={addNotification}
postComment={postComment}
appendItemArray={appendItemArray}
updateItem={updateItem}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
@@ -238,7 +179,10 @@ class Stream extends React.Component {
</div>
: <p>{asset.settings.closedMessage}</p>}
<Slot fill="stream" />
<Slot
fill="stream"
{...slotProps}
/>
{loggedIn && (
<ModerationLink
@@ -247,86 +191,98 @@ class Stream extends React.Component {
/>
)}
<div className="talk-stream-wrapper-box">
<Slot fill="streamBox" />
</div>
{/* the highlightedComment is isolated after the user followed a permalink */}
{highlightedComment
? <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
disableReply={!open}
postComment={this.props.postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadNewReplies}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
key={highlightedComment.id}
commentIsIgnored={commentIsIgnored}
reactKey={highlightedComment.id}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
liveUpdates={true}
/>
: <div className="talk-stream-comments-container">
<NewCount
count={comments.nodes.length - view.length}
loadMore={this.viewNewComments}
? (
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
<Comment
data={data}
root={root}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
depth={0}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={highlightedComment.id}
commentIsIgnored={commentIsIgnored}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
liveUpdates={true}
/>
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
commentClassNames={commentClassNames}
data={this.props.data}
root={this.props.root}
disableReply={!open}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
addTag={addTag}
removeTag={removeTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={this.props.loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
pluginProps={pluginProps}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
liveUpdates={false}
/>;
})}
</TransitionGroup>
<LoadMore
topLevel={true}
moreComments={asset.comments.hasNextPage}
loadMore={this.loadMoreComments}
loadingState={loadingState}
/>
</div>}
</div>
)
: <div className={cn('talk-stream-tab-container', styles.tabContainer)}>
<div
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
>
<Slot
fill="streamFilter"
{...slotProps}
/>
</div>
<TabBar activeTab={activeStreamTab} onTabClick={setActiveStreamTab} sub>
{this.getSlotComponents('streamTabs').map((PluginComponent) => (
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...slotProps}
active={activeStreamTab === PluginComponent.talkPluginName}
/>
</Tab>
))}
<Tab tabId={'all'}>
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
</Tab>
</TabBar>
<TabContent activeTab={activeStreamTab} sub>
{this.getSlotComponents('streamTabPanes').map((PluginComponent) => (
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...slotProps}
/>
</TabPane>
))}
<TabPane tabId={'all'}>
<AllCommentsPane
data={data}
root={root}
comments={comments}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadMoreComments}
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
commentIsIgnored={commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
/>
</TabPane>
</TabContent>
</div>
}
</div>
);
}
@@ -336,12 +292,6 @@ Stream.propTypes = {
addNotification: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
// dispatch action to add a tag to a comment
addTag: PropTypes.func,
// dispatch action to remove a tag from a comment
removeTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
@@ -2,21 +2,25 @@
outline: none;
}
/**
.toggler {
composes: buttonReset from "coral-framework/styles/reset.css";
}
/**
* Up/Down Chevrons for the top right menu
*/
.chevron {
}
.chevron:before {
content: '⌃';
display: inline-block;
display: inline-block;
position: relative;
top: 0.25em;
top: 0.25em;
}
/* Down Arrow */
/* Down Arrow */
.chevron.down:before {
display: inline-block;
display: inline-block;
position: relative;
transform: rotate(180deg);
top: 0;
@@ -27,9 +27,8 @@ export default class Toggleable extends React.Component {
const {isOpen} = this.state;
return (
<ClickOutside onClickOutside={this.close}>
<span className={styles.Toggleable} tabIndex="0" >
<span className={styles.toggler}
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
<span className={styles.Toggleable}>
<button className={styles.toggler} onClick={this.toggle}>{isOpen ? upArrow : downArrow}</button>
{isOpen ? children : null}
</span>
</ClickOutside>
@@ -1,5 +1,8 @@
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const VIEW_COMMENT = 'VIEW_COMMENT';
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
@@ -1,25 +1,31 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import {withFragments} from 'coral-framework/hocs';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
const pluginFragments = getSlotsFragments([
const slots = [
'streamQuestionArea',
'commentInputArea',
'commentInputDetailArea',
'commentInfoBar',
'commentActions',
'commentContent',
'commentReactions'
]);
'commentReactions',
'commentAvatar'
];
export default withFragments({
root: gql`
fragment CoralEmbedStream_Comment_root on RootQuery {
__typename
${pluginFragments.spreads('root')}
${getSlotFragmentSpreads(slots, 'root')}
}
`,
asset: gql`
fragment CoralEmbedStream_Comment_asset on Asset {
__typename
${getSlotFragmentSpreads(slots, 'asset')}
}
${pluginFragments.definitions('root')}
`,
comment: gql`
fragment CoralEmbedStream_Comment_comment on Comment {
@@ -27,6 +33,7 @@ export default withFragments({
body
created_at
status
replyCount
tags {
tag {
name
@@ -47,8 +54,7 @@ export default withFragments({
edited
editableUntil
}
${pluginFragments.spreads('comment')}
${getSlotFragmentSpreads(slots, 'comment')}
}
${pluginFragments.definitions('comment')}
`
})(Comment);
@@ -7,7 +7,9 @@ import branch from 'recompose/branch';
import renderComponent from 'recompose/renderComponent';
import {Spinner} from 'coral-ui';
import {authActions, assetActions, pym} from 'coral-framework';
import * as authActions from 'coral-framework/actions/auth';
import * as assetActions from 'coral-framework/actions/asset';
import pym from 'coral-framework/services/pym';
import {getDefinitionName} from 'coral-framework/utils';
import {withQuery} from 'coral-framework/hocs';
import Embed from '../components/Embed';
@@ -16,9 +18,8 @@ import {addNotification} from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
import {setActiveTab} from '../actions/embed';
import {viewAllComments} from '../actions/stream';
const {logout, checkLogin} = authActions;
const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
@@ -93,7 +94,7 @@ class EmbedContainer extends React.Component {
if (!prevProps.root.comment && this.props.root.comment) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
setTimeout(() => pym.scrollParentToChildEl('talk-embed-stream-container'), 0);
}
}
@@ -146,9 +147,6 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
const EMBED_QUERY = gql`
query CoralEmbedStream_Embed($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) {
asset(id: $assetId, url: $assetUrl) {
totalCommentCount(excludeIgnored: $excludeIgnored)
}
me {
id
status
@@ -185,9 +183,11 @@ const mapDispatchToProps = (dispatch) =>
logout,
checkLogin,
setActiveTab,
viewAllComments,
fetchAssetSuccess,
addNotification,
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
},
dispatch
);
@@ -2,26 +2,29 @@ import React from 'react';
import {gql, compose} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
withAddTag, withRemoveTag, withIgnoreUser, withEditComment,
withPostComment, withPostFlag, withPostDontAgree,
withDeleteAction, withIgnoreUser, withEditComment
} from 'coral-framework/graphql/mutations';
import {notificationActions, authActions} from 'coral-framework';
import * as authActions from 'coral-framework/actions/auth';
import * as notificationActions from 'coral-framework/actions/notification';
import {editName} from 'coral-framework/actions/user';
import {setActiveReplyBox} from '../actions/stream';
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import {withFragments} from 'coral-framework/hocs';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {Spinner} from 'coral-ui';
import {getDefinitionName} from 'coral-framework/utils';
import {
findCommentInEmbedQuery,
insertCommentIntoEmbedQuery,
removeCommentFromEmbedQuery,
insertFetchedCommentsIntoEmbedQuery,
nest,
} from '../graphql/utils';
import omit from 'lodash/omit';
const {showSignInDialog} = authActions;
const {addNotification} = notificationActions;
@@ -91,9 +94,7 @@ class StreamContainer extends React.Component {
}
loadNewReplies = (parent_id) => {
const comment = this.props.root.comment
? this.props.root.comment.parent || this.props.root.comment // highlighted comment.
: this.props.root.asset.comments.nodes.filter((comment) => comment.id === parent_id)[0];
const comment = findCommentInEmbedQuery(this.props.root, parent_id);
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
@@ -157,16 +158,18 @@ const commentFragment = gql`
fragment CoralEmbedStream_Stream_comment on Comment {
id
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies(excludeIgnored: $excludeIgnored) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
${nest(`
replies(excludeIgnored: $excludeIgnored) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
...nest
}
hasNextPage
startCursor
endCursor
}
hasNextPage
startCursor
endCursor
}
`, THREADING_LEVEL)}
}
${Comment.fragments.comment}
`;
@@ -205,16 +208,18 @@ const LOAD_MORE_QUERY = gql`
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3, excludeIgnored: $excludeIgnored) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
${nest(`
replies(limit: 3, excludeIgnored: $excludeIgnored) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
...nest
}
hasNextPage
startCursor
endCursor
}
hasNextPage
startCursor
endCursor
}
`, THREADING_LEVEL)}
}
hasNextPage
startCursor
@@ -224,14 +229,22 @@ const LOAD_MORE_QUERY = gql`
${Comment.fragments.comment}
`;
const slots = [
'streamTabs',
'streamTabPanes',
];
const fragments = {
root: gql`
fragment CoralEmbedStream_Stream_root on RootQuery {
comment(id: $commentId) @include(if: $hasComment) {
...CoralEmbedStream_Stream_comment
parent {
...CoralEmbedStream_Stream_comment
}
${nest(`
parent {
...CoralEmbedStream_Stream_comment
...nest
}
`, THREADING_LEVEL)}
}
asset(id: $assetId, url: $assetUrl) {
id
@@ -262,6 +275,8 @@ const fragments = {
startCursor
endCursor
}
${getSlotFragmentSpreads(slots, 'asset')}
...${getDefinitionName(Comment.fragments.asset)}
}
me {
status
@@ -272,8 +287,10 @@ const fragments = {
settings {
organizationName
}
${getSlotFragmentSpreads(slots, 'root')}
...${getDefinitionName(Comment.fragments.root)}
}
${Comment.fragments.asset}
${Comment.fragments.root}
${commentFragment}
`,
@@ -289,7 +306,11 @@ const mapStateToProps = (state) => ({
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
previousTab: state.embed.previousTab,
commentClassNames: state.stream.commentClassNames
activeStreamTab: state.stream.activeTab,
previousStreamTab: state.stream.previousTab,
commentClassNames: state.stream.commentClassNames,
pluginConfig: state.config.plugin_config,
reduxState: omit(state, 'apollo'),
});
const mapDispatchToProps = (dispatch) =>
@@ -298,6 +319,8 @@ const mapDispatchToProps = (dispatch) =>
addNotification,
setActiveReplyBox,
editName,
viewAllComments,
setActiveStreamTab: setActiveTab,
}, dispatch);
export default compose(
@@ -306,8 +329,6 @@ export default compose(
withPostComment,
withPostFlag,
withPostDontAgree,
withAddTag,
withRemoveTag,
withIgnoreUser,
withDeleteAction,
withEditComment,
@@ -84,7 +84,7 @@ const extension = {
}
user {
id
name: username
username
}
action_summaries {
count
@@ -143,7 +143,7 @@ const extension = {
user: {
__typename: 'User',
id: auth.toJS().user.id,
name: auth.toJS().user.username
username: auth.toJS().user.username
},
created_at: new Date().toISOString(),
body,
+63 -31
View File
@@ -1,21 +1,31 @@
import update from 'immutability-helper';
import {insertCommentsSorted} from 'coral-framework/utils';
function determineCommentDepth(comment) {
let depth = 0;
let cur = comment;
while (cur.parent) {
cur = cur.parent;
depth++;
}
return depth;
}
function applyToCommentsOrigin(root, callback) {
if (root.comment) {
if (root.comment.parent) {
return update(root, {
comment: {
parent: {
$apply: (node) => callback(node),
},
},
});
let comment = root.comment;
for (let depth = 0; depth <= determineCommentDepth(comment); depth++) {
let changes = {$apply: (node) => node ? callback(node) : node};
for (let i = 0; i < depth; i++) {
changes = {parent: changes};
}
comment = update(comment, changes);
}
return update(root, {
comment: {
$apply: (node) => callback(node),
},
});
return {
...root,
comment,
};
}
return update(root, {
asset: {$apply: (asset) => callback(asset)},
@@ -97,6 +107,13 @@ export function removeCommentFromEmbedQuery(root, id) {
return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id));
}
export function getTopLevelParent(comment) {
if (comment.parent) {
return getTopLevelParent(comment.parent);
}
return comment;
}
function findComment(nodes, callback) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
@@ -119,12 +136,7 @@ export function findCommentInEmbedQuery(root, callbackOrId) {
callback = (node) => node.id === callbackOrId;
}
if (root.comment) {
if (callback(root.comment)) {
return root.comment;
}
if (root.comment.parent && callback(root.comment.parent)) {
return root.comment.parent;
}
return findComment([getTopLevelParent(root.comment)], callback);
}
if (!root.asset.comments) {
return false;
@@ -132,14 +144,6 @@ export function findCommentInEmbedQuery(root, callbackOrId) {
return findComment(root.asset.comments.nodes, callback);
}
const ascending = (a, b) => {
const dateA = new Date(a.created_at);
const dateB = new Date(b.created_at);
if (dateA < dateB) { return -1; }
if (dateA > dateB) { return 1; }
return 0;
};
function findAndInsertFetchedComments(parent, comments, parent_id) {
const isAsset = parent.__typename === 'Asset';
const connectionField = isAsset ? 'comments' : 'replies';
@@ -152,11 +156,9 @@ function findAndInsertFetchedComments(parent, comments, parent_id) {
if (isAsset) {
return nodes.concat(comments.nodes);
}
return nodes
.concat(comments.nodes.filter(
return insertCommentsSorted(nodes, comments.nodes.filter(
(comment) => !nodes.some((node) => node.id === comment.id)
))
.sort(ascending);
));
}},
},
});
@@ -179,3 +181,33 @@ function findAndInsertFetchedComments(parent, comments, parent_id) {
export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) {
return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id));
}
/**
* Nest a string in itself repeatly until `level` has been reached.
*
* Example:
* nest(`
* a
* ...nest
* b
* `, 2)
*
* Output:
* `
* a
* a
* b
* b
* `
*/
export function nest(document, level) {
let result = '';
for (let x = 0; x < level; x++) {
if (x === 0) {
result += document;
continue;
}
result = result.replace('...nest', document);
}
return result.replace('...nest', '');
}
+2 -2
View File
@@ -7,8 +7,8 @@ import './graphql';
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
import {getStore, injectReducers} from 'coral-framework/services/store';
import {getClient} from 'coral-framework/services/client';
import pym from 'coral-framework/services/pym';
import AppRouter from './AppRouter';
import {pym} from 'coral-framework';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
import reducers from './reducers';
@@ -48,5 +48,5 @@ render(
<ApolloProvider client={client} store={store}>
<AppRouter />
</ApolloProvider>
, document.querySelector('#coralStream')
, document.querySelector('#talk-embed-stream-container')
);
@@ -17,14 +17,22 @@ function getQueryVariable(variable) {
const initialState = {
activeReplyBox: '',
assetId: getQueryVariable('asset_id'),
assetUrl: getQueryVariable('asset_url'),
commentId: getQueryVariable('comment_id'),
commentClassNames: []
assetId: getQueryVariable('asset_id') || '',
assetUrl: getQueryVariable('asset_url') || '',
commentId: getQueryVariable('comment_id') || '',
commentClassNames: [],
activeTab: process.env.TALK_DEFAULT_STREAM_TAB,
previousTab: '',
};
export default function stream(state = initialState, action) {
switch (action.type) {
case actions.SET_ACTIVE_TAB:
return {
...state,
activeTab: action.tab,
previousTab: state.activeTab,
};
case authActions.LOGOUT:
return {
...state,
@@ -40,6 +48,11 @@ export default function stream(state = initialState, action) {
...state,
commentId: '',
};
case actions.VIEW_COMMENT:
return {
...state,
commentId: action.id,
};
case actions.ADD_COMMENT_CLASSNAME :
return {
...state,
+24 -62
View File
@@ -17,7 +17,7 @@ body {
height: auto !important;
}
#coralStream {
#talk-embed-stream-container {
padding: 4px;
}
@@ -25,7 +25,7 @@ body {
min-height: 600px;
}
button {
.coralButton {
margin: 5px 10px 5px 0px;
background: none;
padding: 0px;
@@ -33,16 +33,16 @@ button {
font-size: inherit;
}
button:hover {
.coralButton:hover {
border-radius: 2px;
color: #767676;
}
button i {
.coralButton i {
margin-right: 3px;
}
hr {
.coralHr {
border: 0;
height: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
@@ -112,13 +112,6 @@ hr {
color: #2a2a2a;
}
.commentStream .material-icons {
vertical-align: middle;
width: 1em;
font-size: 1em;
overflow: hidden;
}
/* Question Box Styles */
.coral-plugin-questionbox-info {
top: 0;
@@ -181,21 +174,10 @@ hr {
display: none;
}
.talk-stream-wrapper-box {
padding: 10px 0;
}
.talk-stream-comments-container {
position: relative;
}
.commentStream {
/* prevent absolutely positioned final permalink popover from being clipped */
padding-bottom: 50px;
min-height: 600px;
position: relative;
}
/* Comment Box Styles */
.talk-plugin-commentbox-container {
display: flex;
@@ -266,11 +248,6 @@ hr {
float: right;
}
.highlighted-comment {
padding-left: 10px;
border-left: 3px solid rgb(35,118,216);
}
/* Tag Labels */
.coral-plugin-tag-label {
@@ -282,31 +259,16 @@ hr {
border-radius: 2px;
}
/* Reply styles */
.comment .reply {
margin: 0px 0px 10px 20px;
}
/* Comment Action Styles */
.commentActionsRight, .replyActionsRight {
display: flex;
justify-content: flex-end;
width: 30%;
}
.commentActionsLeft, .replyActionsLeft {
display: flex;
justify-content: flex-start;
float: left;
width: 70%;
}
.comment__action-container .material-icons {
font-size: 12px;
margin-left: 3px;
}
button.comment__action-button,
@@ -422,9 +384,11 @@ button.comment__action-button[disabled],
}
.talk-load-more button {
width: 100%;
text-align: center;
color: #FFF;
background-color: #2376D8;
border-radius: 2px;
cursor: pointer;
padding: 10px;
border-radius: 2px;
@@ -437,30 +401,28 @@ button.comment__action-button[disabled],
background-color: #4399FF;
}
.talk-load-more-replies, .coral-new-comments {
.talk-new-comments {
width: 100%;
display: flex;
justify-content: center;
cursor: pointer;
text-align: center;
margin: 4px 0;
}
.coral-new-comments {
position: relative;
top: 1.8em;
z-index: 100;
.talk-load-more-replies {
width: 100%;
padding-left: 20px;
box-sizing: border-box;
}
.talk-load-more-replies button.talk-load-more, .coral-new-comments button.talk-load-more{
width: initial;
.talk-load-more-replies .talk-load-more-button {
background-color: transparent;
color: #979797;
border: #979797 solid 1px;
border-radius: 2px;
}
@media (min-device-width : 300px) and (max-device-width : 420px) {
.commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button {
visibility: collapse;
margin-left: -30px;
}
.commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button .coral-plugin-replies-icon {
visibility: visible;
}
.talk-load-more-replies .talk-load-more:hover button {
background-color: #979797;
color: white;
}
+41 -7
View File
@@ -1,7 +1,8 @@
import pym from 'pym.js';
import URLSearchParams from 'url-search-params';
import {stringify} from 'querystring';
import {buildUrl} from 'coral-framework/utils';
import queryString from 'query-string';
// TODO: Styles should live in a separate file
const snackbarStyles = {
@@ -39,7 +40,7 @@ function buildStreamIframeUrl(talkBaseUrl, query) {
'embed/stream?'
].join('');
url += stringify(query);
url += queryString.stringify(query);
return url;
}
@@ -98,10 +99,37 @@ function configurePymParent(pymParent, opts) {
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-all-comments', function() {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: undefined,
});
// remove the commentId url param
const url = buildUrl({...location, search});
window.history.replaceState(
{},
document.title,
location.origin + location.pathname + location.search
url,
);
});
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-comment', function(id) {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: id,
});
// remove the commentId url param
const url = buildUrl({...location, search});
window.history.replaceState(
{},
document.title,
url,
);
});
@@ -193,12 +221,18 @@ Talk.render = function(el, opts) {
let urlParams = new URLSearchParams(window.location.search);
query.comment_id = urlParams.get('commentId');
if (urlParams.get('commentId')) {
query.comment_id = urlParams.get('commentId');
}
query.asset_id = opts.asset_id;
if (opts.asset_id) {
query.asset_id = opts.asset_id;
}
query.asset_url = opts.asset_url;
if (!query.asset_url) {
if (opts.asset_url) {
query.asset_url = opts.asset_url;
}
else {
try {
query.asset_url = document.querySelector('link[rel="canonical"]').href;
} catch (e) {
+22 -30
View File
@@ -3,45 +3,37 @@ import bowser from 'bowser';
import * as actions from '../constants/auth';
import * as Storage from '../helpers/storage';
import coralApi, {base} from '../helpers/request';
import pym from '../services/PymConnection';
import pym from '../services/pym';
import {resetWebsocket} from 'coral-framework/services/client';
import t from 'coral-framework/services/i18n';
export const showSignInDialog = () => (dispatch, getState) => {
const signInPopUp = window.open(
'/embed/stream/login',
'Login',
'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
);
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
});
// Workaround odd behavior in older WebKit versions, where
// onunload is called twice. (Encountered in IOS 8.3)
let loaded = false;
signInPopUp.onload = () => {
loaded = true;
// Fire some actions inside the popups reducer, to initialize required state.
const required = getState().asset.toJS().settings.requireEmailConfirmation;
const redirectUri = getState().auth.toJS().redirectUri;
signInPopUp.coralStore.dispatch(setRequireEmailVerification(required));
signInPopUp.coralStore.dispatch(setRedirectUri(redirectUri));
};
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
signInPopUp.onunload = () => {
if (loaded) {
dispatch(checkLogin());
}
};
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
};
export const hideSignInDialog = () => (dispatch) => {
if (window.opener && window.opener !== window) {
// TODO: We need to address this when we refactor the
// login popup out of the embed.
// we are in a popup
window.close();
} else {
dispatch(checkLogin());
}
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
window.close();
};
export const focusSignInDialog = () => ({
type: actions.FOCUS_SIGNIN_DIALOG,
});
export const blurSignInDialog = () => ({
type: actions.BLUR_SIGNIN_DIALOG,
});
export const createUsernameRequest = () => ({
type: actions.CREATE_USERNAME_REQUEST
});
-9
View File
@@ -1,9 +0,0 @@
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 '../services/PymConnection';
import pym from '../services/pym';
import * as actions from '../constants/notification';
export const addNotification = (notifType, text) => {
@@ -1,7 +1,7 @@
import {Component, cloneElement, Children} from 'react';
import PropTypes from 'prop-types';
import {findDOMNode} from 'react-dom';
import {pym} from 'coral-framework';
import pym from 'coral-framework/services/pym';
export default class ClickOutside extends Component {
static propTypes = {
@@ -0,0 +1,25 @@
import React from 'react';
import {connect} from 'react-redux';
import {isSlotEmpty} from 'coral-framework/helpers/plugins';
import PropTypes from 'prop-types';
import omit from 'lodash/omit';
function IfSlotIsEmpty({slot, className, reduxState, component: Component = 'div', children, ...rest}) {
return (
<Component className={className}>
{isSlotEmpty(slot, reduxState, rest) ? children : null}
</Component>
);
}
IfSlotIsEmpty.propTypes = {
slot: PropTypes.string,
className: PropTypes.string,
};
const mapStateToProps = (state) => ({
reduxState: omit(state, 'apollo'),
});
export default connect(mapStateToProps, null)(IfSlotIsEmpty);
@@ -0,0 +1,25 @@
import React from 'react';
import {connect} from 'react-redux';
import {isSlotEmpty} from 'coral-framework/helpers/plugins';
import PropTypes from 'prop-types';
import omit from 'lodash/omit';
function IfSlotIsNotEmpty({slot, className, reduxState, component: Component = 'div', children, ...rest}) {
return (
<Component className={className}>
{!isSlotEmpty(slot, reduxState, rest) ? children : null}
</Component>
);
}
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.string,
className: PropTypes.string,
};
const mapStateToProps = (state) => ({
reduxState: omit(state, 'apollo'),
});
export default connect(mapStateToProps, null)(IfSlotIsNotEmpty);
+152
View File
@@ -0,0 +1,152 @@
import {Component} from 'react';
import PropTypes from 'prop-types';
export default class Popup extends Component {
ref = null;
detectCloseInterval = null;
constructor(props) {
super(props);
if (props.open) {
this.openWindow(props);
}
}
openWindow(props = this.props) {
this.ref = window.open(
props.href,
props.title,
props.features,
);
this.setCallbacks();
}
setCallbacks() {
this.ref.onload = () => {
clearInterval(this.detectCloseInterval);
this.onLoad();
};
this.ref.onfocus = () => {
this.onFocus();
};
this.ref.onblur = () => {
this.onBlur();
};
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
this.ref.onunload = () => {
this.onUnload();
const interval = setInterval(() => {
if (this.ref.onload === null) {
this.setCallbacks();
clearInterval(interval);
}
}, 50);
this.detectCloseInterval = setInterval(() => {
if (this.ref.closed) {
clearInterval(this.detectCloseInterval);
this.onClose();
}
}, 50);
};
}
closeWindow() {
if (this.ref) {
if (!this.ref.closed) {
this.ref.close();
}
this.ref = null;
}
}
focusWindow() {
if (this.ref && !this.ref.closed) {
this.ref.focus();
}
}
blurWindow() {
if (this.ref && !this.ref.closed) {
this.ref.blur();
}
}
onLoad = () => {
if (this.props.onLoad) {
this.props.onLoad();
}
}
onUnload = () => {
if (this.props.onUnload) {
this.props.onUnload();
}
}
onClose = () => {
if (this.props.onClose) {
this.props.onClose();
}
}
onFocus = () => {
if (this.props.onFocus) {
this.props.onFocus();
}
}
onBlur = () => {
if (this.props.onBlur) {
this.props.onBlur();
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.open && !this.ref) {
this.openWindow(nextProps);
}
if (this.props.open && !nextProps.open) {
this.closeWindow();
}
if (!this.props.focus && nextProps.focus) {
this.focusWindow();
}
if (this.props.focus && !nextProps.focus) {
this.blurWindow();
}
if (this.props.href !== nextProps.href) {
this.ref.location.href = nextProps.href;
}
}
componentWillUnmount() {
this.closeWindow();
}
render() {
return null;
}
}
Popup.propTypes = {
open: PropTypes.bool,
focus: PropTypes.bool,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onLoad: PropTypes.func,
onUnload: PropTypes.func,
onClose: PropTypes.func,
href: PropTypes.string.isRequired,
features: PropTypes.string,
};
+8 -4
View File
@@ -3,15 +3,17 @@ import cn from 'classnames';
import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
import omit from 'lodash/omit';
function Slot ({fill, inline = false, className, plugin_config: config, defaultComponent: DefaultComponent, ...rest}) {
let children = getSlotElements(fill, {...rest, config});
function Slot ({fill, inline = false, className, reduxState, defaultComponent: DefaultComponent, ...rest}) {
let children = getSlotElements(fill, reduxState, rest);
const pluginConfig = reduxState.config.pluginConfig || {};
if (children.length === 0 && DefaultComponent) {
children = <DefaultComponent {...rest} />;
}
return (
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug}, className)}>
<div className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
{children}
</div>
);
@@ -21,7 +23,9 @@ Slot.propTypes = {
fill: React.PropTypes.string
};
const mapStateToProps = ({config: {plugin_config = {}}}) => ({plugin_config});
const mapStateToProps = (state) => ({
reduxState: omit(state, 'apollo'),
});
export default connect(mapStateToProps, null)(Slot);
+2
View File
@@ -3,6 +3,8 @@ export const CLEAN_STATE = 'CLEAN_STATE';
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
+111 -92
View File
@@ -1,6 +1,117 @@
import {gql} from 'react-apollo';
import withMutation from '../hocs/withMutation';
function convertItemType(item_type) {
switch (item_type) {
case 'COMMENTS':
return 'Comment';
case 'USERS':
return 'User';
case 'ASSETS':
return 'Asset';
default:
throw new Error(`Unknown item_type ${item_type}`);
}
}
function getTagFragment(item_type) {
return gql`
fragment Coral_UpdateFragment on ${convertItemType(item_type)} {
tags {
tag {
name
}
}
}
`;
}
export const withAddTag = withMutation(
gql`
mutation AddTag($id: ID!, $asset_id: ID!, $name: String!, $item_type: TAGGABLE_ITEM_TYPE!) {
addTag(tag: {name: $name, id: $id, item_type: $item_type, asset_id: $asset_id}) {
...ModifyTagResponse
}
}
`, {
props: ({mutate}) => ({
addTag: ({id, name, assetId, itemType}) => {
return mutate({
variables: {
id,
name,
asset_id: assetId,
item_type: itemType,
},
optimisticResponse: {
addTag: {
__typename: 'ModifyTagResponse',
errors: null,
}
},
update: (proxy) => {
const fragmentId = `${convertItemType(itemType)}_${id}`;
const fragment = getTagFragment(itemType);
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment, id: fragmentId});
data.tags.push({
tag: {
__typename: 'Tag',
name
},
__typename: 'TagLink'
});
// Write our data back to the cache.
proxy.writeFragment({fragment, id: fragmentId, data});
},
});
}}),
});
export const withRemoveTag = withMutation(
gql`
mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!, $item_type: TAGGABLE_ITEM_TYPE!) {
removeTag(tag: {name: $name, id: $id, item_type: $item_type, asset_id: $asset_id}) {
...ModifyTagResponse
}
}
`, {
props: ({mutate}) => ({
removeTag: ({id, name, assetId, itemType}) => {
return mutate({
variables: {
id,
name,
asset_id: assetId,
item_type: itemType,
},
optimisticResponse: {
removeTag: {
__typename: 'ModifyTagResponse',
errors: null,
}
},
update: (proxy) => {
const fragmentId = `${convertItemType(itemType)}_${id}`;
const fragment = getTagFragment(itemType);
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment, id: fragmentId});
const idx = data.tags.findIndex((i) => i.tag.name === name);
data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)];
// Write our data back to the cache.
proxy.writeFragment({fragment, id: fragmentId, data});
}
});
}}),
});
export const withSetCommentStatus = withMutation(
gql`
mutation SetCommentStatus($commentId: ID!, $status: COMMENT_STATUS!){
@@ -173,98 +284,6 @@ export const withDeleteAction = withMutation(
}}),
});
const COMMENT_FRAGMENT = gql`
fragment CoralBest_UpdateFragment on Comment {
tags {
tag {
name
}
}
}
`;
export const withAddTag = withMutation(
gql`
mutation AddTag($id: ID!, $asset_id: ID!, $name: String!) {
addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
...ModifyTagResponse
}
}
`, {
props: ({mutate}) => ({
addTag: ({id, name, assetId}) => {
return mutate({
variables: {
id,
name,
asset_id: assetId
},
optimisticResponse: {
addTag: {
__typename: 'ModifyTagResponse',
errors: null,
}
},
update: (proxy) => {
const fragmentId = `Comment_${id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
data.tags.push({
tag: {
__typename: 'Tag',
name: 'BEST'
},
__typename: 'TagLink'
});
// Write our data back to the cache.
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
},
});
}}),
});
export const withRemoveTag = withMutation(
gql`
mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) {
removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
...ModifyTagResponse
}
}
`, {
props: ({mutate}) => ({
removeTag: ({id, name, assetId}) => {
return mutate({
variables: {
id,
name,
asset_id: assetId
},
optimisticResponse: {
removeTag: {
__typename: 'ModifyTagResponse',
errors: null,
}
},
update: (proxy) => {
const fragmentId = `Comment_${id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
const idx = data.tags.findIndex((i) => i.tag.name === 'BEST');
data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)];
// Write our data back to the cache.
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
}
});
}}),
});
export const withIgnoreUser = withMutation(
gql`
mutation IgnoreUser($id: ID!) {
+59 -62
View File
@@ -2,81 +2,63 @@ import React from 'react';
import uniq from 'lodash/uniq';
import pick from 'lodash/pick';
import merge from 'lodash/merge';
import plugins from 'pluginsConfig';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
import flatten from 'lodash/flatten';
import {loadTranslations} from 'coral-framework/services/i18n';
import {injectReducers} from 'coral-framework/services/store';
import camelize from './camelize';
import plugins from 'pluginsConfig';
export function getSlotComponents(slot, reduxState, props = {}) {
const pluginConfig = reduxState.config.pluginConfig || {};
return flatten(plugins
// Filter out components that have slots and have been disabled in `plugin_config`
.filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components))
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot])
)
.filter((component) => {
if(!component.isExcluded) {
return true;
}
let resolvedProps = {...props, config: pluginConfig};
if (component.mapStateToProps) {
resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)};
}
return !component.isExcluded(resolvedProps);
});
}
export function isSlotEmpty(slot, reduxState, props) {
return getSlotComponents(slot, reduxState, props).length === 0;
}
/**
* 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, {key: i, ...props}));
export function getSlotElements(slot, reduxState, props = {}) {
const pluginConfig = reduxState.config.pluginConfig || {};
return getSlotComponents(slot, reduxState, props)
.map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig}));
}
function getComponentFragments(components) {
const res = components
export function getSlotFragments(slot, part) {
const components = uniq(flattenDeep(plugins
.filter((o) => o.module.slots ? o.module.slots[slot] : false)
.map((o) => o.module.slots[slot])
));
const documents = components
.map((c) => c.fragments)
.filter((fragments) => fragments)
.filter((fragments) => fragments && fragments[part])
.reduce((res, fragments) => {
Object.keys(fragments).forEach((key) => {
if (!(key in res)) {
res[key] = {spreads: [], definitions: []};
}
res[key].spreads.push(getDefinitionName(fragments[key]));
res[key].definitions.push(fragments[key]);
});
res.push(fragments[part]);
return res;
}, {});
}, []);
Object.keys(res).forEach((key) => {
// Assemble arguments for `gql` to call it directly without using template literals.
res[key].spreads = `...${res[key].spreads.join('\n...')}\n`;
res[key].definitions = mergeDocuments(res[key].definitions);
});
return res;
}
/**
* Returns an object that can be used to compose fragments or queries.
*
* Example:
* const pluginFragments = getSlotsFragments(['commentInfoBar', 'commentActions']);
* const rootFragment = gql`
* fragment Comment_root on RootQuery {
+ ${pluginFragments.spreads('root')}
* }
* ${pluginFragments.definitions('root')}
* `;
*/
export function getSlotsFragments(slots) {
if (!Array.isArray(slots)) {
slots = [slots];
}
const components = uniq(flattenDeep(slots.map((slot) => {
return plugins
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot]);
})));
const fragments = getComponentFragments(components);
return {
spreads(key) {
return (fragments[key] && fragments[key].spreads) || '';
},
definitions(key) {
return (fragments[key] && fragments[key].definitions) || '';
},
};
return documents;
}
export function getGraphQLExtensions() {
@@ -99,7 +81,22 @@ export function injectPluginsReducers() {
const reducers = merge(
...plugins
.filter((o) => o.module.reducer)
.map((o) => ({[camelize(o.plugin)] : o.module.reducer}))
.map((o) => ({[camelize(o.name)] : o.module.reducer}))
);
injectReducers(reducers);
}
function addMetaDataToSlotComponents() {
// Add talkPluginName to Slot Components.
plugins.forEach((plugin) => {
const slots = plugin.module.slots;
slots && Object.keys(slots).forEach((slot) => {
slots[slot].forEach((component) => {
component.talkPluginName = plugin.name;
});
});
});
}
addMetaDataToSlotComponents();
+6
View File
@@ -0,0 +1,6 @@
import {connect} from 'react-redux';
export default (mapStateToProps, ...rest) => (BaseComponent) => {
BaseComponent.mapStateToProps = mapStateToProps;
return connect(mapStateToProps, ...rest)(BaseComponent);
};
+4
View File
@@ -0,0 +1,4 @@
export default (condition) => (BaseComponent) => {
BaseComponent.isExcluded = condition;
return BaseComponent;
};
+3 -12
View File
@@ -1,14 +1,5 @@
import React from 'react';
import {getDisplayName} from '../helpers/hoc';
// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38.
export default (fragments) => (WrappedComponent) => {
class WithFragments extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
WithFragments.fragments = fragments;
WithFragments.displayName = `WithFragments(${getDisplayName(WrappedComponent)})`;
return WithFragments;
export default (fragments) => (BaseComponent) => {
BaseComponent.fragments = fragments;
return BaseComponent;
};
+32 -4
View File
@@ -15,14 +15,42 @@ const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
* apply query options registered in the graphRegistry.
*/
export default (document, config = {}) => (WrappedComponent) => {
config = {
const wrappedConfig = {
...config,
options: config.options || {},
props: config.props || (({data}) => separateDataAndRoot(data)),
props: (args) => {
const wrappedArgs = {
...args,
data: {
...args.data,
subscribeToMore(stmArgs) {
// Resolve document fragments before passing it to `apollo-client`.
return args.data.subscribeToMore({
...stmArgs,
document: resolveFragments(stmArgs.document),
});
},
fetchMore(lmArgs) {
// Resolve document fragments before passing it to `apollo-client`.
return args.data.fetchMore({
...lmArgs,
query: resolveFragments(lmArgs.query),
});
},
},
};
return config.props
? config.props(wrappedArgs)
: separateDataAndRoot(wrappedArgs.data);
},
};
const wrappedOptions = (data) => {
const base = (typeof config.options === 'function') ? config.options(data) : config.options;
const base = (typeof wrappedConfig.options === 'function')
? wrappedConfig.options(data)
: wrappedConfig.options;
const name = getDefinitionName(document);
const configs = getQueryOptions(name);
const reducerCallbacks =
@@ -45,7 +73,7 @@ export default (document, config = {}) => (WrappedComponent) => {
let memoized = null;
const getWrapped = () => {
if (!memoized) {
memoized = graphql(resolveFragments(document), {...config, options: wrappedOptions})(WrappedComponent);
memoized = graphql(resolveFragments(document), {...wrappedConfig, options: wrappedOptions})(WrappedComponent);
}
return memoized;
};
-10
View File
@@ -1,10 +0,0 @@
import pym from './services/PymConnection';
import actions from './actions';
// TODO (bc): Deprecate old actions. Spreading actions is now needed.
export default {
pym,
actions,
...actions
};
@@ -22,7 +22,7 @@ module.exports = function(source) {
const config = this.exec(source, this.resourcePath);
const plugins = getPluginList(config).map((plugin) => `{
module: require('${plugin}/client'),
plugin: '${plugin}'
name: '${plugin}'
}`);
return stripIndent`
+11 -2
View File
@@ -1,12 +1,13 @@
import {Map, fromJS} from 'immutable';
import * as actions from '../constants/auth';
import {pym} from 'coral-framework';
import pym from 'coral-framework/services/pym';
const initialState = Map({
isLoading: false,
loggedIn: false,
user: null,
showSignInDialog: false,
signInDialogFocus: false,
showCreateUsernameDialog: false,
checkedInitialLogin: false,
view: 'SIGNIN',
@@ -29,13 +30,21 @@ const purge = (user) => {
export default function auth (state = initialState, action) {
switch (action.type) {
case actions.FOCUS_SIGNIN_DIALOG:
return state
.set('signInDialogFocus', true);
case actions.BLUR_SIGNIN_DIALOG:
return state
.set('signInDialogFocus', false);
case actions.SHOW_SIGNIN_DIALOG :
return state
.set('showSignInDialog', true);
.set('showSignInDialog', true)
.set('signInDialogFocus', true);
case actions.HIDE_SIGNIN_DIALOG :
return state.merge(Map({
isLoading: false,
showSignInDialog: false,
signInDialogFocus: false,
view: 'SIGNIN',
error: '',
passwordRequestFailure: null,
@@ -1,7 +1,18 @@
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
import {getGraphQLExtensions} from 'coral-framework/helpers/plugins';
import {getGraphQLExtensions, getSlotFragments} from 'coral-framework/helpers/plugins';
import globalFragments from 'coral-framework/graphql/fragments';
import uniq from 'lodash/uniq';
import {gql} from 'react-apollo';
/*
* Disable false-positive warning below, as it doesn't work well with how we currently
* assemble the queries.
*
* Warning: fragment with name {fragment name} already exists.
* graphql-tag enforces all fragment names across your application to be unique; read more about
* this in the docs: http://dev.apollodata.com/core/fragments.html#unique-names
*/
gql.disableFragmentWarnings();
const fragments = {};
const mutationOptions = {};
@@ -139,18 +150,47 @@ export function getQueryOptions(key) {
}
/**
* Get a document with a fragment named `key`, which contains
* all fragments added under this key.
* getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource.
* It parses the slot name and the resource and usees the plugin API to assemble
* the fragment document.
*/
export function getFragmentDocument(key) {
init();
function getSlotFragmentDocument(key) {
const match = key.match(/TalkSlot_(.*)_(.*)/);
if (!match) {
return '';
}
const slot = match[1][0].toLowerCase() + match[1].substr(1);
const resource = match[2];
const documents = getSlotFragments(slot, resource);
if (documents.length === 0) {
return '';
}
const names = documents.map((d) => getDefinitionName(d));
const typeName = getTypeName(documents[0]);
// Assemble arguments for `gql` to call it directly without using template literals.
const main = `
fragment ${key} on ${typeName} {
...${names.join('\n...')}\n
}
`;
return mergeDocuments([main, ...documents]);
}
/**
* getRegistryFragmentDocument assembles a fragment document using
* all registered fragment under given `key`.
*/
function getRegistryFragmentDocument(key) {
if (!(key in fragments)) {
return '';
}
let documents = fragments[key] ? fragments[key].documents : [];
let fields = fragments[key] ? `...${fragments[key].names.join('\n...')}\n` : ' __typename';
let documents = fragments[key].documents;
let fields = `...${fragments[key].names.join('\n...')}\n`;
// Assemble arguments for `gql` to call it directly without using template literals.
const main = `
@@ -161,6 +201,16 @@ export function getFragmentDocument(key) {
return mergeDocuments([main, ...documents]);
}
/**
* getFragmentDocument returns a fragment that assembles all registered
* fragments under given `key` or if `key` refers to Slot fragments it will
* return the slot fragments specified by this key.
*/
export function getFragmentDocument(key) {
init();
return getRegistryFragmentDocument(key) || getSlotFragmentDocument(key);
}
// The fragments and configs are lazily loaded to allow circular dependencies to work.
// TODO: We might want to change this to an explicit add after we have lazy Queries and Mutations.
let initialized = false;
@@ -178,19 +228,56 @@ function init() {
getGraphQLExtensions().forEach((ext) => add(ext));
}
/**
* resolveFragments finds fragment spread names and attachs
* the related fragment document to the given root document.
*/
export function resolveFragments(document) {
if (document.loc.source) {
// resolve fragments from registry
const matchedSubFragments = document.loc.source.body.match(/\.\.\.(.*)/g) || [];
const subFragments =
uniq(matchedSubFragments.map((f) => f.replace('...', '')))
.map((key) => getFragmentDocument(key))
.filter((i) => i);
// Remember keys that we have already resolved.
const resolvedKeys = [];
if (subFragments.length > 0) {
return mergeDocuments([document, ...subFragments]);
// Spreads from slots that are empty and need to be removed.
// (works around the issue that we don't know the resource type
// if we don't have a fragment)
const spreadsToBeRemoved = [];
// fragments to be attached.
const subFragments = [];
// body contains the final result.
let body = document.loc.source.body;
let done = false;
while (!done) {
done = true;
const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || [];
uniq(matchedSubFragments.map((f) => f.replace('...', '')))
.filter((key) => resolvedKeys.indexOf(key) === -1)
.forEach((key) => {
const doc = getFragmentDocument(key);
if (doc) {
subFragments.push(doc);
// We found a new fragment, so we are not done yet.
done = false;
} else if(key.startsWith('TalkSlot_')) {
spreadsToBeRemoved.push(key);
}
resolvedKeys.push(key);
});
body = mergeDocuments([body, ...subFragments]).loc.source.body;
}
spreadsToBeRemoved.forEach((key) => {
const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g');
body = body.replace(regex, '');
});
return gql`${body}`;
} else {
console.warn('Can only resolve fragments from documents definied using the gql tag.');
}
+4 -1
View File
@@ -4,13 +4,15 @@ import get from 'lodash/get';
import merge from 'lodash/merge';
import esTA from '../../../node_modules/timeago.js/locales/es';
import frTA from '../../../node_modules/timeago.js/locales/fr';
import en from '../../../locales/en.yml';
import es from '../../../locales/es.yml';
import fr from '../../../locales/fr.yml';
// Translations are happening at https://translate.lingohub.com/the-coral-project/dashboard
const defaultLanguage = 'en';
const translations = {...en, ...es};
const translations = {...en, ...es, ...fr};
let lang;
let timeagoInstance;
@@ -40,6 +42,7 @@ function init() {
}
ta.register('es', esTA);
ta.register('fr', frTA);
timeagoInstance = ta();
}
@@ -1,4 +1,4 @@
import Pym from '../../../node_modules/pym.js';
import Pym from 'pym.js';
const pym = new Pym.Child({polling: 100});
export default pym;
+28
View File
@@ -0,0 +1,28 @@
.buttonReset {
/* reset button */
user-select: none;
outline: invert none medium;
border: none;
touch-action: manipulation;
padding: 0;
overflow: hidden;
/* Unify anchor and button. */
cursor: pointer;
display: inline-block;
box-sizing: border-box;
text-align: center;
text-decoration: none;
align-items: flex-start;
vertical-align: middle;
white-space: nowrap;
background: transparent;
font-size: inherit;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
&::-moz-focus-inner: {
border: 0;
}
}
+43
View File
@@ -1,5 +1,6 @@
import {gql} from 'react-apollo';
import t from 'coral-framework/services/i18n';
import {capitalize} from 'coral-framework/helpers/strings';
export const getTotalActionCount = (type, comment) => {
return comment.action_summaries
@@ -144,3 +145,45 @@ export function forEachError(error, callback) {
callback({error: e, msg});
});
}
const ascending = (a, b) => {
const dateA = new Date(a.created_at);
const dateB = new Date(b.created_at);
if (dateA < dateB) { return -1; }
if (dateA > dateB) { return 1; }
return 0;
};
const descending = (a, b) => ascending(a, b) * -1;
export function insertCommentsSorted(nodes, comments, sortOrder = 'CHRONOLOGICAL') {
const added = nodes.concat(comments);
if (sortOrder === 'CHRONOLOGICAL') {
return added.sort(ascending);
}
if (sortOrder === 'REVERSE_CHRONOLOGICAL') {
return added.sort(descending);
}
throw new Error(`Unknown sort order ${sortOrder}`);
}
export const isTagged = (tags, which) => tags.some((t) => t.tag.name === which);
export function buildUrl({protocol, hostname, port, pathname, search, hash} = window.location) {
if (search && search[0] !== '?') {
search = `?${search}`;
} else if (search === '?') {
search = '';
}
return `${protocol}//${hostname}${port ? `:${port}` : ''}${pathname}${search}${hash}`;
}
/**
* getSlotFragmentSpreads will return a string in the
* expected format for slot fragments, given `slots` and `resource`.
* e.g. `getSlotFragmentSpreads(['slotName'], 'root')` returns
* `...TalkSlot_SlotName_root`.
*/
export function getSlotFragmentSpreads(slots, resource) {
return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${resource}`).join('\n...')}\n`;
}
-104
View File
@@ -1,104 +0,0 @@
import React, {Component, PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
import {Icon} from 'coral-ui';
import classnames from 'classnames';
// tag string for best comments
export const BEST_TAG = 'BEST';
export const commentIsBest = ({tags} = {}) => tags.some((t) => t.tag.name === BEST_TAG);
const name = 'coral-plugin-best';
// It would be best if the backend/api held this business logic
const canModifyBestTag = ({roles = []} = {}) => roles && ['ADMIN', 'MODERATOR'].some((role) => roles.includes(role));
// Put this on a comment to show that it is best
export const BestIndicator = ({children = <Icon name='star'/>}) => (
<span aria-label={t('comment_is_best')}>
{ children }
</span>
);
/**
* Component that only renders children if the provided user prop can modify best tags
*/
export const IfUserCanModifyBest = ({user, children}) => {
if (!(user && canModifyBestTag(user))) {return null;}
return children;
};
/**
* Button that lets a moderator tag a comment as "Best".
* Used to recognize really good comments.
*/
export class BestButton extends Component {
static propTypes = {
// whether the comment is already tagged as best
isBest: PropTypes.bool.isRequired,
// set that this comment is best
addBest: PropTypes.func.isRequired,
// remove the best status
removeBest: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.onClickAddBest = this.onClickAddBest.bind(this);
this.onClickRemoveBest = this.onClickRemoveBest.bind(this);
}
state = {
isSaving: false
}
async onClickAddBest(e) {
e.preventDefault();
const {addBest} = this.props;
if (!addBest) {
console.warn('BestButton#onClickAddBest called even though there is no addBest prop. doing nothing');
return;
}
this.setState({isSaving: true});
try {
await addBest();
} finally {
this.setState({isSaving: false});
}
}
async onClickRemoveBest(e) {
e.preventDefault();
const {removeBest} = this.props;
if (!removeBest) {
console.warn('BestButton#onClickAddBest called even though there is no removeBest prop. doing nothing');
return;
}
this.setState({isSaving: true});
try {
await removeBest();
} finally {
this.setState({isSaving: false});
}
}
render() {
const {isBest, addBest, removeBest} = this.props;
const {isSaving} = this.state;
const disabled = isSaving || !(isBest ? removeBest : addBest);
return (
<button onClick={isBest ? this.onClickRemoveBest : this.onClickAddBest}
disabled={disabled}
className={classnames(`${name}-button`, `e2e__${isBest ? 'unset' : 'set'}-best-comment`)}
aria-label={t(isBest ? 'unset_best' : 'set_best')}>
<Icon name={ isBest ? 'star' : 'star_border' } />
</button>
);
}
}
@@ -1,17 +0,0 @@
import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
const name = 'coral-plugin-comment-count';
const CommentCount = ({count}) => {
return <div className={`${name}-text`}>
{`${count} ${count === 1 ? t('comment_singular') : t('comment_plural')}`}
</div>;
};
CommentCount.propTypes = {
count: PropTypes.number.isRequired
};
export default CommentCount;
+1 -3
View File
@@ -67,7 +67,7 @@ class CommentBox extends React.Component {
postComment(comment, 'comments')
.then(({data}) => {
this.setState({loadingState: 'success'});
this.setState({loadingState: 'success', body: ''});
const postedComment = data.createComment.comment;
// Execute postSubmit Hooks
@@ -78,8 +78,6 @@ class CommentBox extends React.Component {
if (commentPostedHandler) {
commentPostedHandler();
}
this.setState({body: ''});
})
.catch((err) => {
this.setState({loadingState: 'error'});
@@ -5,6 +5,8 @@ import {can} from 'coral-framework/services/perms';
import {PopupMenu, Button} from 'coral-ui';
import ClickOutside from 'coral-framework/components/ClickOutside';
import cn from 'classnames';
import styles from './styles.css';
const name = 'coral-plugin-flags';
@@ -146,14 +148,17 @@ export default class FlagButton extends Component {
<button
ref={(ref) => this.flagButton = ref}
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
className={`${name}-button`}>
className={cn(`${name}-button`, {[`${name}-button-flagged`]: flagged}, styles.button)}>
{
flagged
? <span className={`${name}-button-text`}>{t('reported')}</span>
: <span className={`${name}-button-text`}>{t('report')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged ? styles.flaggedIcon : {}}
<i className={
cn(`${name}-icon`, 'material-icons', styles.icon, {
flaggedIcon: flagged,
[styles.flaggedIcon]: flagged,
})}
aria-hidden={true}>flag</i>
</button>
{
@@ -214,12 +219,3 @@ export default class FlagButton extends Component {
);
}
}
const styles = {
flaggedIcon: {
color: '#F00'
},
unflaggedIcon: {
color: 'inherit'
}
};
@@ -0,0 +1,14 @@
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
margin: 5px 0px 5px 10px;
}
.flaggedIcon {
color: #f00
}
.icon {
padding: 0 2px;
font-size: 12px;
vertical-align: middle;
}
+5
View File
@@ -75,3 +75,8 @@
margin: inherit;
color: inherit;
}
.iconView, .iconDate {
vertical-align: middle;
padding-bottom: 1px;
}
+2 -2
View File
@@ -30,11 +30,11 @@ const Comment = (props) => {
<ul>
<li>
<a onClick={props.link(`${props.asset.url}?commentId=${props.comment.id}`)}>
<Icon name="open_in_new" />{t('view_conversation')}
<Icon name="open_in_new" className={styles.iconView}/>{t('view_conversation')}
</a>
</li>
<li>
<Icon name="schedule" />
<Icon name="schedule" className={styles.iconDate}/>
<PubDate
className={styles.pubdate}
created_at={props.comment.created_at}
@@ -0,0 +1,16 @@
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
margin: 5px 10px 5px 0px;
}
@media (max-width: 425px) {
.label {
display: none;
}
}
.icon {
padding: 0 2px;
font-size: 12px;
vertical-align: middle;
}
+7 -4
View File
@@ -2,17 +2,20 @@ import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
import classnames from 'classnames';
import cn from 'classnames';
import styles from './ReplyButton.css';
const name = 'coral-plugin-replies';
const ReplyButton = ({onClick}) => {
return (
<button
className={classnames(`${name}-reply-button`)}
className={cn(`${name}-reply-button`, styles.button)}
onClick={onClick}>
{t('reply')}
<i className={`${name}-icon material-icons`}
<span className={cn(`${name}-label`, styles.label)}>
{t('reply')}
</span>
<i className={cn(`${name}-icon`, 'material-icons', styles.icon)}
aria-hidden={true}>reply</i>
</button>
);
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
import {withStopIgnoringUser} from 'coral-framework/graphql/mutations';
import {link} from 'coral-framework/services/PymConnection';
import {link} from 'coral-framework/services/pym';
import NotLoggedIn from '../components/NotLoggedIn';
import IgnoredUsers from '../components/IgnoredUsers';
import {Spinner} from 'coral-ui';
+3
View File
@@ -0,0 +1,3 @@
.root {
}
+3 -1
View File
@@ -1,8 +1,10 @@
import React, {PropTypes} from 'react';
import {Icon as IconMDL} from 'react-mdl';
import cn from 'classnames';
import styles from './Icon.css';
const Icon = ({className = '', name}) => (
<IconMDL className={className} name={name} />
<IconMDL className={cn(styles.root, className)} name={name} />
);
Icon.propTypes = {
+67 -6
View File
@@ -1,9 +1,70 @@
li.base--active {
background: white;
font-weight: bold;
.root {
display: inline-block;
margin-right: -1px;
margin-bottom: -1px;
}
li.material--active {
font-weight: bold;
border-bottom: solid 2px black;
.rootActive {
}
.rootSub {
display: inline-block;
margin-bottom: -2px;
}
.rootSubActive {
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
padding: 8px 10px;
color: #4E5259;
border: solid 1px #D8D8D8;
background: #F0F0F0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
font-size: 13px;
outline: none;
}
.button:hover, .button:focus {
background: #d5d5d5;
border-bottom: 1px solid #d5d5d5;
}
.buttonActive, .buttonActive:hover, .buttonActive:focus {
background: white;
font-weight: bold;
border-bottom: 1px solid white;
}
.buttonSub {
composes: buttonReset from "coral-framework/styles/reset.css";
color: black;
border: none;
background: transparent;
padding: 6px 12px;
margin-bottom: 3px;
outline: none;
}
.buttonSub:hover, .buttonSub:focus {
background: transparent;
border-bottom: solid 3px #d5d5d5;
margin-bottom: 0px;
}
.buttonSubActive, .buttonSubActive:hover, .buttonSubActive:focus {
font-weight: bold;
border-bottom: solid 3px #10589b;
margin-bottom: 0px;
padding: 6px 10px;
}
.root:only-child .button, .rootSub:only-child .buttonSub {
cursor: default;
pointer-events: none;
}
+106 -9
View File
@@ -1,12 +1,109 @@
import React from 'react';
import styles from './Tab.css';
import cn from 'classnames';
import PropTypes from 'prop-types';
export default ({children, tabId, active, onTabClick, cStyle = 'base', ...props}) => (
<li
key={tabId}
className={`${active ? `${styles[`${cStyle}--active`]} talk-tab-active` : ''} talk-tab ${props.className}`}
onClick={() => onTabClick(tabId)}
>
{children}
</li>
);
/**
* The `Tab` component is used inside the `TabBar` Component, to
* render tabs.
*/
class Tab extends React.Component {
handleTabClick = () => {
if (this.props.onTabClick) {
this.props.onTabClick(this.props.tabId);
}
}
getRootClassName({active, className, sub, classNames = {}} = this.props) {
return cn(
'talk-tab',
className,
{
[classNames.root || styles.root]: !sub,
[classNames.rootSub || styles.rootSub]: sub,
[classNames.rootActive || styles.rootActive]: active && !sub,
[classNames.rootSubActive || styles.rootSubActive]: active && sub,
'talk-tab-active': active,
}
);
}
getButtonClassName({sub, active, classNames = {}} = this.props) {
return cn(
'talk-tab-button',
{
[classNames.button || styles.button]: !sub,
[classNames.buttonSub || styles.buttonSub]: sub,
[classNames.buttonActive || styles.buttonActive]: active && !sub,
[classNames.buttonSubActive || styles.buttonSubActive]: active && sub,
}
);
}
render() {
const {
children,
classNames: _a,
active,
onTabClick: _c,
tabId: _d,
sub: _e,
'aria-controls': ariaControls,
...rest,
} = this.props;
return (
<li
{...rest}
role="presentation"
className={this.getRootClassName()}
>
<button
aria-controls={ariaControls}
role="tab"
aria-selected={active}
className={this.getButtonClassName()}
onClick={this.handleTabClick}
>
{children}
</button>
</li>
);
}
}
Tab.propTypes = {
// className to be added to the root element.
className: PropTypes.string,
// classNames allows full design customization of the component.
classNames: PropTypes.shape({
root: PropTypes.string,
rootActive: PropTypes.string,
rootSub: PropTypes.string,
rootSubActive: PropTypes.string,
button: PropTypes.string,
buttonActive: PropTypes.string,
buttonSub: PropTypes.string,
buttonSubActive: PropTypes.string,
}),
// active indicates that this tab is currently active.
// This is injected by the `TabBar` component.
active: PropTypes.bool,
// onTabClick is fired whenever the tab was clicked. The tabId is passed as
// the first argument.
onTabClick: PropTypes.func,
// Sub indicates that this is a tab of a sub-tab-panel.
// This is injected by the `TabBar` component.
sub: PropTypes.bool,
// `aria-controls` should be set to the `id` of the `TabContent` for accessibility.
// This is injected by the `TabBar` component.
'aria-controls': PropTypes.string,
};
export default Tab;
+5 -35
View File
@@ -1,44 +1,14 @@
.base {
.root {
list-style: none;
border-bottom: solid 1px #D8D8D8;
padding: 0;
margin: 0;
}
.base li {
color: #4E5259;
border: solid 1px #D8D8D8;
background: #F0F0F0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
display: inline-block;
border-bottom: none;
padding: 8px 10px;
margin-right: -1px;
user-select: none;
font-size: 13px;
}
.base li:hover {
background: #d5d5d5;
cursor: pointer;
}
.material {
.rootSub {
list-style: none;
border: none;
padding: 0;
}
.material li {
color: black;
border: none;
border-bottom: solid 2px white;
background: white;
padding: 8px 0;
margin-right: 40px;
}
.material li:hover {
background: white;
border-bottom: solid 2px grey;
margin: 0;
border-bottom: solid 2px #eee;
}
+72 -23
View File
@@ -1,37 +1,86 @@
import React from 'react';
import styles from './TabBar.css';
import cn from 'classnames';
import Tab from './Tab';
import PropTypes from 'prop-types';
/**
* The `TabBar` component accepts `Tab` components to create
* a tab bar.
*/
class TabBar extends React.Component {
constructor(props) {
super(props);
this.handleClickTab = this.handleClickTab.bind(this);
}
handleClickTab(tabId) {
if (this.props.onChange) {
this.props.onChange(tabId);
}
getRootClassName({className, classNames = {}, sub} = this.props) {
return cn(
'talk-tab-bar',
className,
{
[classNames.root || styles.root]: !sub,
[classNames.rootSub || styles.rootSub]: sub,
}
);
}
render() {
const {children, activeTab, cStyle = 'base'} = this.props;
const {
children,
activeTab,
tabClassNames,
classNames: _a,
onTabClick: _b,
'aria-controls': ariaControls,
sub,
...rest,
} = this.props;
return (
<div>
<ul className={`${styles.base} ${cStyle ? styles[cStyle] : ''} talk-tab-bar ${this.props.className}`}>
{React.Children.toArray(children)
.filter((child) => !child.props.restricted)
.map((child, tabId) =>
React.cloneElement(child, {
tabId,
active: child.props.id === activeTab,
onTabClick: this.handleClickTab,
cStyle
})
)}
</ul>
</div>
<ul
{...rest}
role="tablist"
className={this.getRootClassName()}
>
{React.Children.toArray(children)
.map((child, i) =>
React.cloneElement(child, {
tabId: (child.props.tabId !== undefined) ? child.props.tabId : i,
active: child.props.tabId === activeTab,
onTabClick: this.props.onTabClick,
classNames: tabClassNames,
'aria-controls': ariaControls,
sub,
})
)}
</ul>
);
}
}
TabBar.propTypes = {
// className to be added to the root element.
className: PropTypes.string,
// classNames allows full design customization of the component.
classNames: PropTypes.shape({
root: PropTypes.string,
rootSub: PropTypes.string,
}),
// classNames to be passed to the children.
tabClassNames: Tab.propTypes.classNames,
// activeTab should be set to the currently active tabId.
activeTab: PropTypes.string,
// onTabClick is fired whenever the tab was clicked. The tabId is passed as
// the first argument.
onTabClick: PropTypes.func,
// Sub indicates that this is a sub-tab-panel.
sub: PropTypes.bool,
// `aria-controls` should be set to the `id` of the `TabContent` for accessibility.
'aria-controls': PropTypes.string,
};
export default TabBar;
@@ -0,0 +1,3 @@
.root {
padding-top: 10px;
}
+39 -2
View File
@@ -1,6 +1,43 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './TabContent.css';
export default ({children, show = true}) => (
show ? <div>{children}</div> : null
function getRootClassName(className) {
return cn('talk-tab-content', className, styles.root);
}
/**
* The `TabContent` component accepts `TabPane` components to render
* the content of a `Tab`.
*/
const TabContent = ({children, className, activeTab, sub, ...rest}) => (
<div
{...rest}
className={getRootClassName(className)}
>
{
React.Children.toArray(children)
.filter((child) => child.props.tabId === activeTab)
.map((child, i) =>
React.cloneElement(child, {
tabId: (child.props.tabId !== undefined) ? child.props.tabId : i,
sub,
}))
}
</div>
);
TabContent.propTypes = {
// className to be added to the root element.
className: PropTypes.string,
// activeTab should be set to the currently active tabId.
activeTab: PropTypes.string,
// Sub indicates that this component belongs to a sub-tab-panel.
sub: PropTypes.bool,
};
export default TabContent;
+18
View File
@@ -0,0 +1,18 @@
.root, .rootSub {
display: inline-block;
position: relative;
top: -2px;
background: #616161;
color: white;
font-weight: normal;
font-size: 10px;
padding: 2px;
margin-left: 2px;
margin-top: -2px;
min-width: 20px;
}
.rootSubActive {
background: #10589b;
}
+47
View File
@@ -0,0 +1,47 @@
import React from 'react';
import cn from 'classnames';
import styles from './TabCount.css';
import PropTypes from 'prop-types';
function getNumber(no) {
let result = Number.parseInt(no);
if (no >= 1000) {
result = `${Math.round(result / 100) / 10}k`;
}
return result;
}
function getRootClassName({className, active, sub}) {
return cn(
'talk-tab-count',
className,
{
[styles.root]: !sub,
[styles.rootSub]: sub,
[styles.rootActive]: active && !sub,
[styles.rootSubActive]: active && sub,
'talk-tab-active': active,
}
);
}
/**
* The `TabCount` renders a count number next to a tab name.
*/
const TabCount = ({children, active, sub, className}) => (
<span className={getRootClassName({className, active, sub})}>{getNumber(children)}</span>
);
TabCount.propTypes = {
// className to be added to the root element.
className: PropTypes.string,
// active indicates that the related tab is currently active.
active: PropTypes.bool,
// Sub indicates that this component belongs to a sub-tab-panel.
sub: PropTypes.bool,
};
export default TabCount;
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
function getRootClassName(className) {
return cn('talk-pane', className);
}
/**
* The `TabPane` component is used inside the `TabContent` component to render
* the content of a `Tab`.
*/
const TabPane = ({children, className, tabId: _a, sub: _b, ...rest}) => (
<div
{...rest}
className={getRootClassName(className)}
>
{children}
</div>
);
TabPane.propTypes = {
// className to be added to the root element.
className: PropTypes.string,
tabId: PropTypes.string,
// Sub indicates that this component belongs to a sub-tab-panel.
// This is injected by the `TabContent` component.
sub: PropTypes.bool,
};
export default TabPane;
+2
View File
@@ -4,7 +4,9 @@ export {default as CoralLogo} from './components/CoralLogo';
export {default as FabButton} from './components/FabButton';
export {default as TabBar} from './components/TabBar';
export {default as Tab} from './components/Tab';
export {default as TabCount} from './components/TabCount';
export {default as TabContent} from './components/TabContent';
export {default as TabPane} from './components/TabPane';
export {default as Button} from './components/Button';
export {default as Spinner} from './components/Spinner';
export {default as Tooltip} from './components/Tooltip';
+29 -8
View File
@@ -29,7 +29,7 @@ const CONFIG = {
// JWT_ISSUER is the value for the issuer for the tokens that will be verified
// when decoding. If `JWT_ISSUER` is not in the environment, then it will try
// `TALK_ROOT_URL`, otherwise, it will be undefined.
JWT_ISSUER: process.env.TALK_JWT_ISSUER || process.env.TALK_ROOT_URL || undefined,
JWT_ISSUER: process.env.TALK_JWT_ISSUER || process.env.TALK_ROOT_URL,
// JWT_EXPIRY is the time for which a given token is valid for.
JWT_EXPIRY: process.env.TALK_JWT_EXPIRY || '1 day',
@@ -77,7 +77,15 @@ const CONFIG = {
SMTP_HOST: process.env.TALK_SMTP_HOST,
SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD,
SMTP_PORT: process.env.TALK_SMTP_PORT,
SMTP_USERNAME: process.env.TALK_SMTP_USERNAME
SMTP_USERNAME: process.env.TALK_SMTP_USERNAME,
//------------------------------------------------------------------------------
// Flagging Config
//------------------------------------------------------------------------------
// DISABLE_AUTOFLAG_SUSPECT_WORDS is true when the suspect words that are
// matched should not be flagged.
DISABLE_AUTOFLAG_SUSPECT_WORDS: process.env.TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS === 'TRUE'
};
//==============================================================================
@@ -91,7 +99,9 @@ const CONFIG = {
if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) {
CONFIG.JWT_SECRET = 'keyboard cat';
} else if (!CONFIG.JWT_SECRET) {
throw new Error('TALK_JWT_SECRET must be provided in the environment to sign/verify tokens');
throw new Error(
'TALK_JWT_SECRET must be provided in the environment to sign/verify tokens'
);
}
//------------------------------------------------------------------------------
@@ -108,7 +118,7 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.MONGO_URL) {
// Reset the redis url in the event it hasn't been overrided and we are in a
// testing environment.
if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) {
CONFIG.REDIS_URL = 'redis://localhost';
CONFIG.REDIS_URL = 'redis://localhost/1';
}
//------------------------------------------------------------------------------
@@ -119,10 +129,15 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) {
* This is true when the recaptcha secret is provided and the Recaptcha feature
* is to be enabled.
*/
CONFIG.RECAPTCHA_ENABLED = CONFIG.RECAPTCHA_SECRET && CONFIG.RECAPTCHA_SECRET.length > 0 &&
CONFIG.RECAPTCHA_PUBLIC && CONFIG.RECAPTCHA_PUBLIC.length > 0;
CONFIG.RECAPTCHA_ENABLED =
CONFIG.RECAPTCHA_SECRET &&
CONFIG.RECAPTCHA_SECRET.length > 0 &&
CONFIG.RECAPTCHA_PUBLIC &&
CONFIG.RECAPTCHA_PUBLIC.length > 0;
if (!CONFIG.RECAPTCHA_ENABLED) {
console.warn('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.');
console.warn(
'Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'
);
}
//------------------------------------------------------------------------------
@@ -138,7 +153,13 @@ if (!CONFIG.RECAPTCHA_ENABLED) {
];
if (requiredProps.some((prop) => !CONFIG[prop])) {
console.warn(`${requiredProps.map((v) => `TALK_${v}`).join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`);
console.warn(
`${requiredProps
.map((v) => `TALK_${v}`)
.join(
', '
)} should be defined in the environment if you would like to send password reset emails from Talk`
);
}
}
+25
View File
@@ -11,6 +11,9 @@ entries:
- title: About
url: /index.html
output: web
- title: Contribute
url: /contribute.html
output: web
- title: FAQ
url: /faq.html
output: web
@@ -33,6 +36,28 @@ entries:
- title: Setup
url: /install-setup.html
output: web
- title: Microservice Deployments
url: /install-microservices.html
output: web
- title: Troubleshooting
url: /install-troubleshooting.html
output: web
- title: Architecture
output: web
folderitems:
- title: Overview
url: /architecture.html
output: web
- title: Tags
url: /architecture-tags.html
output: web
- title: Metadata API
url: /architecture-metadata.html
output: web
- title: cli
url: /architecture-cli.html
output: web
- title: Plugins
output: web
+139
View File
@@ -0,0 +1,139 @@
---
title: The Talk cli
keywords: architecture
sidebar: talk_sidebar
permalink: architecture-cli.html
summary:
---
Talk ships with a cli tool that allows access to a wide variety of functionality.
It is designed to provide a convenient way for engineers to perform key tasks without the need to muck about in the UI. It also opens the door for scripts to perform operations programmatically.
Note: the cli tool requires [the Talk environment to be configured](configuration.html) either via env vars or by using a `.cli` file via `bin/cli -c .env [command] ....`
## Using the cli
In a terminal, try:
```
/path/to/talk/bin cli [options] [commands] [arguments]
```
Commonly, you'll be in the `talk/` folder, in which case you can:
```
bin/cli [options] [commands] [arguments]
```
If you're a heavy cli user, consider adding the `bin` folder to your PATH so you can run `cli` from anywhere!
If you are using [our Docker environment](install-docker.html), the bin folder will already be in the PATH.
## What can I do with the cli?
The Talk cli ships with 'unix style' help. To access the docs, simply run the cli with insufficient arguments.
Let's say I wanted to figure out how to change a user's password. I'd start be seeing what the cli has for me.
(Note: the following output may change, please reference at the `--help` output for your version as you use the cli.)
```
talk :) ]$ bin/cli --help
Usage: cli [options] [command]
Commands:
serve serve the application
settings interact with the application settings
assets interact with assets
setup setup the application
jobs work with the job queues
token work with the access tokens
users work with the application auth
migration provides utilities for migrating the database
plugins provides utilities for interacting with the plugin system
help [cmd] display help for [cmd]
Options:
-h, --help output usage information
-V, --version output the version number
-c, --config [path] Specify the configuration file to load
--pid [path] Specify a path to output the current PID to
```
Most commands contain sub-commands. Running with cli with such a command generates the docs for the sub-commands and options therein.
Change user password is likely to be in the `users` command group.
```
talk :) ]$ bin/cli users
Usage: cli-users [options] [command]
Commands:
create [options] create a new user
delete <userID> delete a user
passwd <userID> change a password for a user
update [options] <userID> update a user
list list all the users in the database
merge <dstUserID> <srcUserID> merge srcUser into the dstUser
addrole <userID> <role> adds a role to a given user
removerole <userID> <role> removes a role from a given user
ban <userID> ban a given user
uban <userID> unban a given user
disable <userID> disable a given user from logging in
enable <userID> enable a given user from logging in
Options:
-h, --help output usage information
-V, --version output the version number
-c, --config [path] Specify the configuration file to load
--pid [path] Specify a path to output the current PID to
```
I now see that I can change a password like so:
```
bin/cli users passwd [userID]
```
You can also read these help prompts by [exploring the source code](https://github.com/coralproject/talk/blob/master/bin/cli).
### Usage Notes
If you haven't used a cli enabled system before, think of it like this: generally, you'd make a rest call, rpc, etc... to perform an action. The cli's api is designed in the same way, just for the audience of command line wielding engineers and scripts.
The best way to understand what the cli does is to explore the help commands. Uses of cli are also scattered through this documentation in context of their topics.
For some real world uses of the cli, see the scripts in the [package.json file](https://github.com/coralproject/talk/blob/d688f70c19d8dee48371784009fd07322dae4eb5/package.json#L8).
## What's really going on when I run the cli?
The cli tool is a standalone application. Running it starts up the internals of a talk process, executes the given command, then shuts it down. No server functionality is enabled by cli commands unless specifically noted.
The cli tool _does not require a talk server to be running._ This means that you can execute commands, for example, during and installation process before starting the server. The also means that you can execute commands using varying configurations (via the `-c [.env file]` flag).
### Accessing existing Talk installs with the cli
You may use the cli tool to 'remotely' control existing talk installs.
This is accomplished by running the cli tool on any box using the mongo/redis/etc... credentials for the environment that you would like to act on. For example, if you want something to happen periodically on your production Talk install, you could set up a utility box with a cron job that triggers the cli with the same db/cache credentials. If you want to do something quick on a staging server, you could run the cli locally with staging credentials.
The cli tool will connect directly with the install's db and redis instance(s) so ensure that your box can reach those servers on the appropriate ports.
Also, _please ensure your cli and the server(s) in an environment are using the same version of Talk._
Please secure your environments and credentials or the cli tool becomes a convenient way for someone to own your system.
## Extending the cli
The Talk cli is based on the excellent [commander](https://github.com/tj/commander.js/) library.
At the time of writing, there are no plugin hooks for the cli tool. If you would like to change this, whether by writing code yourself or recommending a need, please [write and issue](https://github.com/coralproject/talk/blob/053b687959d45bcd682a1a2a4b604ebfab7441bb/CONTRIBUTING.md#writing-code).
+92
View File
@@ -0,0 +1,92 @@
---
title: Metadata
keywords: architecture
sidebar: talk_sidebar
permalink: architecture-metadata.html
summary:
---
_Metadata_ allows you to add fields to models that are not represented in the core schema.
## Goals
The metadata api is designed to satisfy two product goals:
* Give developers flexibility in extending datatypes.
* Protect core fields that are essential to Talk's operation.
## Design
Metadata is represented by an [subdocument in our Schemas](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/models/comment.js#L74). This takes advantage of Mongo's flexibility allowing for any data to be stored therein.
### Setting Metadata
Talk provides [a service layer](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js) allowing developers to `set` and `unset` metadata on objects in a way similar to key-value stores.
Let's say that I want to add a custom field called `potency` to a comment.
```
const MetadataService = require('services/metadata');
const CommentModel = require('models/comment');
// Sets the property `potency` on the comment with `id=1`.
MetadataService.set(CommentModel, '1', 'potency', 42);
```
Note that the model passed here is the Model itself and not an individual comment object. This allows us to update the value on that document [in an atomic manner](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js#L60) for efficiency and to prevent race conditions.
### Accessing Metadata
The metadata api does not contain a `get` method. The metadata object is retrieved via database queries along with the rest of the data.
## Metadata and the Graph
One of the first principles of GraphQL is that the shape of the graph does not need to be the same as the shape of the data in the database. In fact, it probably shouldn't be.
This enables us to treat metadata fields in any way that makes sense as we design our Graph. The fact that a value is stored in the metadata object is an implementation detail invisible to the front end.
Take for example, the `reason` field in the `FlagAction` type. This stores the user provided reason why they flagged a comment. As far as the front end knows, it's [just another field](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/graph/typeDefs.graphql#L453) alongside the core fields:
```
# graph/typeDefs.graphql
type FlagAction implements Action {
...
# The reason for which the Flag Action was created.
reason: String
...
}
```
If, however, we [look at the resolver](https://github.com/coralproject/talk/blob/a47e2378e96f34f25447782f3e7ce59fa48ec791/graph/resolvers/dont_agree_action.js) for that field, we see that `reason` is [destructured](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) from the metadata object and returned.
```
// graph/resolvers/dont_agree_action.js
const DontAgreeAction = {
// Stored in the metadata, extract and return.
reason({metadata: {reason}}) {
return reason;
}
};
module.exports = DontAgreeAction;
```
This is an extremely powerful pattern as it allows us absolute freedom in designing our graph and complete isolation of the added fields in the database.
## Some things to keep in mind
### Namespace your metadata fields
Since metadata can be added by the core and multiple plugins, collisions may occur. As you create your plugins, please be careful to pick unique names for metadata fields. We recommend namespacing all your fields in a subdocument named after your plugin.
```
[model].metadata.[your_plugin_name].[the_field]
```
### Querying by metadata fields
We currently do not have a clean way to index metadata fields. As a result queries that match against metadata fields will not scale. If you have a need to match, sort, etc... by a metadata field, [please let us know](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md#writing-code).
+71
View File
@@ -0,0 +1,71 @@
---
title: Tags
keywords: architecture
sidebar: talk_sidebar
permalink: architecture-tags.html
summary:
---
Tags are essentially strings that can be added to models. Currently, tags can be added to [Users, Comments and Assets](https://github.com/coralproject/talk/blob/ced449a1489d47c25d604020fa2e0b3b7a741353/graph/typeDefs.graphql#L144). If you would like to add tags to other models, you can extend this schema using [GraphQL hooks](plugins-server.html#graphql-hooks).
## Tag Definitions
When handling tags, the Talk Server references a set of definitions that describe how tags are handled. These definitions are keyed off the tag `name`, the simple string that is stored on items.
The schema for Tag definitions [can be found here](https://github.com/coralproject/talk/blob/3545bf01cd91044fdb738d337a0ac94d9f71fbc3/models/schema/tag.js).
Note that along with the `name`, tag definitions contains:
* `permissions` information about who can see and set the tag,
* `models` which `ITEM_TYPES` this tag can be applied to, and
Whenever a tag is 'handled' by the server, it references this definition to determine that tag's behavior.
See [Plugin API Documentation](plugins-server.html#field-tags) for more information.
### Creating a Tag Definition
Tag Definitions must be created in order for the system to determine what tags are permitted on the server side.
Tag Definitions do not contain any logic themselves but provide information that other parts of the system can use to specify which models a tag can be applied to (models) and perform authorization logic (permissions).
Take the tag created by `coral-plugin-offtopic` as an example.
```
// coral-plugin-offtopic/index.js
module.exports = {
tags: [
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
};
```
This plugin allows users to self-report that their comment is "off topic" at the time of creation, then display a badge on those comments.
To accomplish this, the plugin creates the tag `OFF_TOPIC` with:
* `permissions.public: true` - will be sent over the wire to the client side
* `permissions.self: true` - can be added by the active user to themselves or assets they own
* `permissions.roles: []` - cannot be added by anyone based on their roles
* `models: ['COMMENTS']` - can only be added to COMMENTS (not to users/assets/etc...)
And [viola](https://youtu.be/Q0O9gFf-tiI?t=23s)! This tag is something that can only be created by the logged in user on their own comments and is sent over the wire to the client so it can display the badge.
## Tag Links
When tags are stored on objects in the database, they are represented by [TagLinks](https://github.com/coralproject/talk/blob/master/models/schema/tag_link.js).
A TagLinks says that `tag` was `assigned_by` a specific user at a specific time (`created_at`).
Note that the `tag` field in the TagLinkSchema is the full TagSchema itself. This allows for another level of flexibility. Server code may generate Tags on the fly, complete with programmatically generated permissions and item behaviors.
If a Tag definitions exists in the global/asset context then that definition will be used regardless of what is stored here. This allows high level controls on the behavior of tags, ensuring that plugins cannot produce unexpected definitions for already defined tags.
+80
View File
@@ -0,0 +1,80 @@
---
title: Architecture Overview
keywords: architecture
sidebar: talk_sidebar
permalink: architecture.html
summary:
---
## Talk's Architecture
Talk consists of four distinct layers of code:
* Plugins
* Plugin API
* Core
* cli
### Plugins
Talk plugins deliver the features and functionality that can be changed or removed. Much of the default functionality is delivered by core plugins allowing developers to have control over any non-essential functionality.
### Plugin API
Talk plugins interact exclusively with the Plugin API. Maintaining this layer of separation between plugins and core allows us to consciously design the api that we want it publish to plugin authors. We can then expose just the elements of core that make sense and maintain that contract as core changes.
### Core
Talk core consists of architecture and functionality that deliver stability, security, scalability, extendability, etc... In addition, the Core contains features and functionality that is essential to the operation of Talk as a product.
Our goal is to continually extend our plugin infrastructure making the Core as pluggable as possible. Ultimately, a day may come where the Core of Talk is simply a framework for delivering a certain flavor of web applications.
### cli
Talk ships with [a cli tool](architecture-cli.html) that exposes functionality to the command line. We seek to provide cli functionality for all features that could need to be accomplished programmatically or prior to the server's startup.
## Thinking about Plugins, the Plugin API and Core?
The following is a template for a thought process that may help clarify your ideas against the backdrop of Talk's architecture.
Think of a feature or capability. It could be something that's already in Talk or not. It could be something you want to build, or something you'd think would be a terrible idea. The important part here is to have something to interrogate.
```
wait(60000);
```
Now, ask these questions:
### Is it a Plugin?
Most work for Talk happens in the Plugin space. If the answers to any of these questions is Yes, then you're thinking of a Plugin.
* Does Talk's existing Plugin APIs support the thing you want to build?
* Is this something that only some users will want/need?
* Is this something that we want devs to iterate on widely?
You should [build it as a plugin](plugins-quickstart.html). Feel free to explore here on your own or reach out to us. We love to advise on plugins, so please feel free to [file an issue](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) and we will start a conversation. We will help you conceptualize, architect and promote your plugin if it is in line with our values.
### Does it need updates to the Plugin API?
If you answered yes above:
* Do I need to extend the Plugin API to support my plugin?
Often times all the functionality a plugin needs is in the Core, but the Plugin API doesn't expose it. In these cases, we seek to iteratively extend the Talk Plugin API. All Plugin API contributions from the community must begin by [filing an Issue](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md).
Note: we are stabilizing the process by which new Plugin API bindings are created, agreed upon and ultimately made part of our Plugins Contract. If you are interested in this process, please reach out to us.
### Does it require updates to the Plugin API _and_ Core?
What, if any, changes need to be made to Core so that the API can be extended?
Quite often the only things missing from Core are things like _events_, _slots_, _CSS classes_, etc... Adding these is a great way to become a Core Contributor and break new ground as a Plugin Developer.
We seek to keep Core as lean as possible.
### Is my idea really just Core?
Amazing! We are always looking to extend the capabilities of Talk. We look forward to discussing what you've got to bring!
Please see our [contributing guide](](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md)) for more information.
+1 -1
View File
@@ -17,7 +17,7 @@ sign and verify tokens via a `HS256` algorithm.
- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`)
- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`)
- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`)
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
- `TALK_SMTP_FROM_ADDRESS` (*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.
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
+9
View File
@@ -0,0 +1,9 @@
---
title: Contributor's Guide
keywords: homepage
sidebar: talk_sidebar
permalink: contribute.html
summary:
---
Our Contributor's Guide can be found [here](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md).
+98
View File
@@ -0,0 +1,98 @@
---
title: Microservice Deployments
keywords: install
sidebar: talk_sidebar
permalink: install-microservices.html
summary:
---
In Talk, we seek to deliver the simplicity of a monolith with the advantages of
a microservice based infrastructure for those who want them.
To accomplish this, Talk has the ability to run with subsets of its overall
functionality and contains architecture that allows them to operate logically as
microservices when running in a single environment.
## Talk Server Functionalities
The Talk server serves several logically/architecturally distinct functions:
* A web server that
* serves "public" assets (aka, the comment embed)
* the GraphQL endpoints
* A web socket server that handles subscriptions.
* A jobs processor that handles queued operations.
In the documentation so far, we've discussed how to deploy all of these
functionalities bundled into a single monolith application. This is convenient
as there is minimal configuration and horizontal scaling is as easy as upping
the number of servers behind a single load balancer.
## Separating Talk into Microservices
Talk can be run in two or more separate clusters of servers by
enabling/disabling different bits of functionality: webserver, socket server and
jobs server.
Each microservice would deploy with the same codebase and configuration.
Note that the `cli serve` command, which is responsible for starting the server,
contains flags that control whether `jobs` and `websockets` are enabled.
```
talk :) ]$ bin/cli serve --help
Options:
...
-j, --jobs enable job processing on this thread
-w, --websockets enable the websocket (subscriptions) handler on this thread
...
```
Each Talk Microservice cluster can be deployed in an identical manner described
in the other docs in this section with the omission of the `-j` and/or `-w`
flags.
With routing logic in front of the webserver cluster, separation between public
and protected assets can be achieved.
## When should I consider separating?
Consider a microservice deployment if:
* you are running plugins that require intensive job processing
* you do not want to simplicity of single cluster horizontal scaling and want to
tune the economy and performance of your install.
* You run into scaling issues serving websockets
At scale, combining separate concerns in a single process makes it very
difficult to understand what is taking up resources. With microservices, each
server could be configured to sit behind it's own load balance and scale
independently. Each variety of process can always have just enough resources.
An install that heavily utilizes the jobs queue could see delays in http service
because of heavy jobs processes and/or delays in the execution of jobs processes
due to increased server load as a result of Node's single thread infrustructure.
## Deployment Methodologies
Note that there is no flag to separate the http routes on the webserver.
Separating the http server functionalities can be accomplished by the routing of
various routes to the correct http server via an external upstream proxy like
Google Cloud's Load Balancer, AWS's ELB, or a websever like NGINX/Apache. This
can ensure that sensitive areas, such as the `/admin/` route are not available
outside the firewall.
Talk's job processors are synchronized by Redis, so as long as all Talk
instances access the same Redis cluster no additional configuration is needed
when launching an independent jobs cluster.
If there are any features of Talk that you believe should be disable-able via
server flags, please let us know and consider contributing it to the project!
## Deployment Flows/Scripts
We do not currently support any microservice based deployment flows. If you
develop one yourself that is completely based on open source tooling, please
consider contributing it to the project!
+22
View File
@@ -0,0 +1,22 @@
---
title: Installation Troubleshooting
keywords: install
sidebar: talk_sidebar
permalink: install-troubleshooting.html
summary:
---
This page tracks common issues that arise when installing Talk and provides resolutions.
## The Talk server seems to be working but the stream isn't showing up on my page.
Talk employs a _domain whitelist_ that controls which sites can contain comment threads. This prevents malicious folks from using your server to embed streams on unwanted pages.
If your comment thread isn't showing:
1. Log into your admin panel
1. Go to the Configure tab
1. Select the Tech Settings submenu
1. Ensure that your Domain is the Permitted Domains list
Note: if your site has multiple subdomains, listing the domain itself (ie: `mydomain.com`) will enable Talk on all subdomains. If you would like to restrict Talk to certain subdomains, you must list all of them here (ie: `thisone.mydomain.com thatone.mydomain.com`).
+1 -1
View File
@@ -215,7 +215,7 @@ It is important to realize that when you're writing a Talk plugin you are writin
### Publish to npm
In order to [register](http://localhost:4000/plugins.html#plugin-registration) your _published_ plugin, you will need to [publish it to npm](https://docs.npmjs.com/getting-started/publishing-npm-packages).
In order to [register](plugins.html#plugin-registration) your _published_ plugin, you will need to [publish it to npm](https://docs.npmjs.com/getting-started/publishing-npm-packages).
Once the package is published, update `plugins.json` to use the published plugin:
+34 -4
View File
@@ -50,13 +50,20 @@ const getCountsByAssetID = (context, asset_ids) => {
* @param {Array<String>} id The ID of the asset
* @param {Array<String>} excludeIgnored Exclude comments ignored by the requesting user
*/
const getCountsByAssetIDPersonalized = async (context, {assetId, excludeIgnored}) => {
const getCountsByAssetIDPersonalized = async (context, {assetId, excludeIgnored, tags}) => {
const query = {
asset_id: assetId,
status: {
$in: ['NONE', 'ACCEPTED'],
},
};
if (tags) {
query['tags.tag.name'] = {
$in: tags,
};
}
const user = context.user;
if (excludeIgnored && user) {
@@ -108,7 +115,7 @@ const getParentCountsByAssetID = (context, asset_ids) => {
* @param {Array<String>} id The ID of the asset
* @param {Array<String>} excludeIgnored Exclude comments ignored by the requesting user
*/
const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgnored}) => {
const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgnored, tags}) => {
const query = {
asset_id: assetId,
parent_id: null,
@@ -116,6 +123,13 @@ const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgn
$in: ['NONE', 'ACCEPTED'],
},
};
if (tags) {
query['tags.tag.name'] = {
$in: tags,
};
}
const user = context.user;
if (excludeIgnored && user) {
@@ -195,7 +209,7 @@ const getCountByParentIDPersonalized = async (context, {id, excludeIgnored}) =>
* @return {Promise} resolves to the counts of the comments from the
* query
*/
const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id}) => {
const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id, tags}) => {
let query = CommentModel.find();
if (ids) {
@@ -218,6 +232,14 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
query = query.where({author_id});
}
if (tags) {
query = query.find({
'tags.tag.name': {
$in: tags,
},
});
}
return CommentModel
.find(query)
.count();
@@ -229,7 +251,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
* @param {Object} context graph context
* @param {Object} query query terms to apply to the comments query
*/
const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored}) => {
const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored, tags}) => {
let comments = CommentModel.find();
// Only administrators can search for comments with statuses that are not
@@ -256,6 +278,14 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
});
}
if (tags) {
comments = comments.find({
'tags.tag.name': {
$in: tags,
},
});
}
// Only let an admin request any user or the current user request themself.
if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === author_id) && author_id != null) {
comments = comments.where({author_id});
+8 -1
View File
@@ -15,6 +15,10 @@ const {
EDIT_COMMENT
} = require('../../perms/constants');
const {
DISABLE_AUTOFLAG_SUSPECT_WORDS
} = require('../../config');
const debug = require('debug')('talk:graph:mutators:tags');
const plugins = require('../../services/plugins');
@@ -297,7 +301,10 @@ const createPublicComment = async (context, commentInput) => {
// Otherwise just return the new comment.
// TODO: Check why the wordlist is undefined
if (wordlist != null && wordlist.suspect != null) {
// If the wordlist has matched the suspect word filter and we haven't disabled
// auto-flagging suspect words, then we should flag the comment!
if (wordlist != null && wordlist.suspect != null && !DISABLE_AUTOFLAG_SUSPECT_WORDS) {
// TODO: this is kind of fragile, we should refactor this to resolve
// all these const's that we're using like 'COMMENTS', 'FLAG' to be

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