Compare commits

...
348 Commits
Author SHA1 Message Date
Wyatt Johnson 27ecbd1bdd Merge pull request #1204 from coralproject/document-optimization-reapply
Reapply optimizations
2017-12-08 16:01:47 -07:00
Chi Vinh Le 4477c0981f Reapply optimizations 2017-12-08 23:38:10 +01:00
Wyatt Johnson 2c9eae9264 Merge pull request #1202 from coralproject/fix-load-more-replies
Fix load more replies
2017-12-08 15:19:27 -07:00
Wyatt Johnson 812a4ee704 Update package.json 2017-12-08 15:19:01 -07:00
Wyatt Johnson d3caf350ac Merge branch 'master' into fix-load-more-replies 2017-12-08 15:18:35 -07:00
Wyatt Johnson 864e3f1a1f Merge pull request #1203 from coralproject/revert-1201-document-optimization-v2
Revert "Optimizations v2"
2017-12-08 15:18:23 -07:00
Wyatt Johnson 61d16fbe08 Revert "Optimizations v2" 2017-12-08 15:17:28 -07:00
Wyatt Johnson 000e26e3e1 Merge pull request #1201 from coralproject/document-optimization-v2
Optimizations v2
2017-12-08 15:15:47 -07:00
Chi Vinh Le 71f0200557 Fix load more replies 2017-12-08 23:14:40 +01:00
Wyatt Johnson d79edcd412 Merge branch 'master' into document-optimization-v2 2017-12-08 15:08:15 -07:00
Wyatt Johnson 485b9d036f version bump 2017-12-08 15:07:15 -07:00
Wyatt Johnson 041ba95a5b Merge pull request #1199 from coralproject/less-lazy-assets
Less Lazy Assets
2017-12-08 15:06:33 -07:00
Wyatt Johnson d0d047f641 fixed bug with load more 2017-12-08 15:04:33 -07:00
Wyatt Johnson b057131d34 Merge branch 'master' into less-lazy-assets 2017-12-08 13:59:15 -07:00
Chi Vinh Le ba572fbf48 Typo 2017-12-07 20:24:45 +01:00
Chi Vinh Le e3c1f8c231 Merge branch 'document-optimization-v2' of ssh://github.com/coralproject/talk into document-optimization-v2 2017-12-07 20:23:38 +01:00
Chi Vinh Le 07ddb94327 Fix yarn.lock 2017-12-07 20:23:05 +01:00
Kiwi 0addb938ae Merge branch 'master' into document-optimization-v2 2017-12-07 19:54:17 +01:00
Wyatt Johnson c0d01e55e1 Merge pull request #1200 from coralproject/delay-subscription-handler
Delay throttled subscription handler until query has loaded
2017-12-07 11:44:37 -07:00
Chi Vinh Le 3ac301142e Update graphql-ast-tools 2017-12-07 19:43:32 +01:00
Chi Vinh Le 7072aa013b document optimizations v2 2017-12-07 18:41:46 +01:00
Chi Vinh Le 091be25f36 Delay throttled subscription handler until query has loaded 2017-12-07 16:55:36 +01:00
Wyatt Johnson 41a3639d31 Merge branch 'master' into less-lazy-assets 2017-12-06 16:33:13 -07:00
Wyatt Johnson f96146bfd9 Merge pull request #1195 from coralproject/graphql-ast-tools
Use graphql-ast-tools
2017-12-06 16:32:59 -07:00
Wyatt Johnson 2eec87aa80 modified asset loading to be less lazy 2017-12-06 16:10:42 -07:00
Chi Vinh Le aabd40d53a Remove apollo-utilities 2017-12-06 11:45:16 +01:00
Wyatt Johnson 9bce69a7cd Merge branch 'master' into graphql-ast-tools 2017-12-05 13:41:48 -07:00
Kim Gardner 7afa6c018b Merge pull request #1196 from coralproject/logic-fix
Staff Configuration Fix
2017-12-05 19:51:00 +00:00
Wyatt Johnson 94b56b547f fixed small bug 2017-12-05 12:31:36 -07:00
Kiwi 6329e9f618 Merge branch 'master' into graphql-ast-tools 2017-12-05 19:52:27 +01:00
Kim Gardner ea7266595d Merge pull request #1194 from coralproject/perf-pass
Performance Pass
2017-12-05 18:44:31 +00:00
Kiwi 4f2d97cd82 Merge branch 'master' into graphql-ast-tools 2017-12-05 19:42:24 +01:00
Chi Vinh Le 6a00d3d249 Remove unused code 2017-12-05 19:40:53 +01:00
Chi Vinh Le d3040804a2 Upgrade graphql-anywhere-optimized and graphql-ast-tools 2017-12-05 19:39:14 +01:00
Wyatt Johnson 24596a48bd Merge branch 'master' into perf-pass 2017-12-05 11:29:21 -07:00
Chi Vinh Le 04ae844a1a Use graphql-ast-tools 2017-12-05 19:25:32 +01:00
Wyatt Johnson a8a8455f40 removed reference 2017-12-05 11:23:36 -07:00
Wyatt Johnson d7302e4b3c added support for deep route caching, settings cache, template optim 2017-12-05 11:17:44 -07:00
Kim Gardner f09ca6d060 Merge pull request #1193 from coralproject/fix-incorrect-class-reason-popup
Update CSS classname
2017-12-05 17:57:43 +00:00
Kim Gardner 73f24fd75b Update CSS classname 2017-12-05 17:16:39 +00:00
Wyatt Johnson 001beb7a5b Merge pull request #1186 from coralproject/performance-enhancements
Performance enhancements
2017-12-04 15:16:57 -07:00
Chi Vinh Le 0b04a011c4 Merge branch 'performance-enhancements' of ssh://github.com/coralproject/talk into performance-enhancements 2017-12-04 22:50:06 +01:00
Chi Vinh Le 00fb2e277b Merge branch 'master' into performance-enhancements 2017-12-04 22:49:06 +01:00
Chi Vinh Le a79dd76f4b Use graphql document optimizations only in production 2017-12-04 22:46:33 +01:00
Chi Vinh Le 0d91396b1c Remove death star button 2017-12-04 22:45:23 +01:00
Wyatt Johnson e1324a168b Merge pull request #1191 from coralproject/graph-flags
Graph flags
2017-12-04 14:44:48 -07:00
Kiwi af9180f114 Merge branch 'master' into graph-flags 2017-12-04 22:28:36 +01:00
Wyatt Johnson 8f5e54520b Merge branch 'master' into performance-enhancements 2017-12-04 14:26:29 -07:00
Chi Vinh Le 2061a7b97d Use @coralproject/graphql-anywhere-optimized 2017-12-04 22:21:40 +01:00
Wyatt Johnson 3b4e69f06e fixed based on e2e finding a bug! 2017-12-04 14:06:32 -07:00
Kim Gardner f71be09bca Merge pull request #1189 from coralproject/user-badges-fix
User Badges Styling Fix
2017-12-04 19:31:17 +00:00
Wyatt Johnson 87ef0f1cb9 Adjusted flag beheviour for staff 2017-12-04 12:17:39 -07:00
Belen Curcio 0640be2a77 Timestamp no break 2017-12-04 15:00:43 -03:00
Belen Curcio fdf11fa277 Adding extra container and styling 2017-12-04 13:42:22 -03:00
Wyatt Johnson 8b71175c46 adjusted logic flow for comment flags 2017-12-04 09:27:56 -07:00
Belen Curcio 6da4439b94 Adding header container 2017-12-02 00:20:59 -03:00
Belen Curcio 7731ec42de css tag label 2017-12-01 23:43:19 -03:00
Belen Curcio 67a4d63a23 Good margin 2017-12-01 15:31:05 -03:00
Belen Curcio ce9ee0a5b0 Adding styles for mob devs 2017-12-01 15:28:09 -03:00
Kiwi 2e8f5f2e69 Merge branch 'master' into performance-enhancements 2017-12-01 17:57:24 +01:00
Chi Vinh Le 099186d030 Make polling interval longer 2017-12-01 17:56:53 +01:00
Kim Gardner 99daa0b2c9 Merge pull request #1187 from coralproject/remove-dash
Remove Dashboard for the interim
2017-12-01 16:27:53 +00:00
Chi Vinh Le c2b488fa98 Don't use murmurhash 2017-12-01 17:11:38 +01:00
Chi Vinh Le 612d2596f9 Turn NamedFragments with directives always into InlineFragments 2017-12-01 17:02:45 +01:00
Chi Vinh Le 835d30d362 Put document optimizations behind a option 2017-12-01 16:54:45 +01:00
Chi Vinh Le 015e3d3ed9 Tiny refactor 2017-12-01 16:47:54 +01:00
Chi Vinh Le 4c7c852af7 Only merge definitions with directives when they are the same 2017-12-01 16:44:01 +01:00
Chi Vinh Le 72123cabe7 Rename getDefinitionName to getDefinitionID 2017-12-01 16:33:38 +01:00
Chi Vinh Le f17b296e2e Cache resolved fragments (globally) 2017-12-01 16:31:26 +01:00
Chi Vinh Le 2c72975137 Preoptimize mutation, fragments and batched subscription documents 2017-12-01 15:42:13 +01:00
Chi Vinh Le 5c1314165c Upgrade graphql-anywhere-optimized 2017-12-01 14:16:32 +01:00
Chi Vinh Le fd83fed7bb Preoptimize queries and subscriptions documents 2017-12-01 14:10:38 +01:00
Kim Gardner dff89ec6cd Remove Dashboard docs 2017-12-01 12:53:17 +00:00
Wyatt Johnson ef200a85cb removed backend for metrics 2017-11-30 17:16:32 -07:00
Kim Gardner 9c0e6ccb36 Remove Dashboard 2017-11-30 23:21:01 +00:00
Kim Gardner 64276cd157 Remove Dashboard 2017-11-30 23:20:01 +00:00
Chi Vinh Le 214de045fb One subscription queue per query 2017-11-30 22:44:29 +01:00
Chi Vinh Le 92a9a535c6 Merge branch 'master' into performance-enhancements 2017-11-30 22:23:11 +01:00
Chi Vinh Le 3db7439d76 Use custom optimized graphql-anywhere 2017-11-30 21:55:16 +01:00
Chi Vinh Le 65be2b89a4 Throttle instead of debounce 2017-11-30 21:44:32 +01:00
Kim Gardner c9f6ec327a Merge pull request #1185 from coralproject/qbox-fix
Fix Stream: Question Box
2017-11-30 19:33:52 +00:00
Chi Vinh Le 06ddb30d77 Decouple apollo store 2017-11-30 20:26:08 +01:00
Belen Curcio af3542aec5 adding and enabling question box 2017-11-30 15:30:21 -03:00
Kim Gardner 016eb472eb Merge pull request #1184 from coralproject/jobs-stream-fix
Jobs + Pending Box Fixes
2017-11-30 17:06:56 +00:00
Chi Vinh Le ab772839c5 Debounce and batch subscription updates 2017-11-30 16:38:38 +01:00
Wyatt Johnson 1b9c2404db fixed job cli, reverted comment box pending beheviour 2017-11-29 15:57:28 -07:00
Chi Vinh Le cf96d07a7a Better death star 2017-11-29 11:54:51 +01:00
Kim Gardner 1c01a3320f Merge pull request #1179 from coralproject/firefox-hover-tab
Firefox hover tab
2017-11-28 20:16:22 +00:00
Wyatt Johnson 8d018df0d1 Merge branch 'master' into firefox-hover-tab 2017-11-28 13:05:50 -07:00
Kim Gardner b2a2b135a4 Merge pull request #1173 from coralproject/refactor-configure
Rewrite/Refactor stream configure
2017-11-28 19:49:49 +00:00
Kim Gardner ccd22ea334 Merge branch 'master' of git+ssh://github.com/coralproject/talk into refactor-configure 2017-11-28 19:40:22 +00:00
Kim Gardner 83910f7fce Merge pull request #1163 from coralproject/extendable-embed-stream-tab
Extendable embed stream tabs
2017-11-28 19:39:36 +00:00
Wyatt Johnson 28873ed961 Merge branch 'master' into refactor-configure 2017-11-28 09:46:11 -07:00
Kim Gardner e3febf4a0c Merge branch 'master' into firefox-hover-tab 2017-11-28 16:32:57 +00:00
Kim Gardner c3b25d2d0a Merge branch 'master' into extendable-embed-stream-tab 2017-11-28 16:31:50 +00:00
Kim Gardner 96a375e38a Merge pull request #1183 from coralproject/bump-version-3-8-1
Version 3.8.1
2017-11-28 16:30:45 +00:00
Wyatt Johnson 417a7a3c36 added excemption as no fix available as of yet 2017-11-28 09:16:33 -07:00
Wyatt Johnson c903d8c0f9 Merge branch 'master' into bump-version-3-8-1 2017-11-28 08:44:46 -07:00
Kim Gardner 24e895460b Merge branch 'master' into firefox-hover-tab 2017-11-27 17:23:36 +00:00
Kim Gardner d6a83201ad Merge branch 'master' into extendable-embed-stream-tab 2017-11-27 17:21:24 +00:00
Kim Gardner bbdde50858 Merge pull request #1177 from coralproject/graph-context-connectors
Graph Context Connectors
2017-11-27 17:20:48 +00:00
Kim Gardner 8730ada87c Merge branch 'master' into extendable-embed-stream-tab 2017-11-27 16:30:22 +00:00
Kim Gardner 506b938536 Merge branch 'master' into firefox-hover-tab 2017-11-27 16:30:12 +00:00
Kim Gardner 043e722d95 Merge branch 'master' into graph-context-connectors 2017-11-27 16:30:04 +00:00
Kim Gardner fb1dc23fa9 Version 3.8.1 2017-11-27 16:28:46 +00:00
Kim Gardner e882b51ed0 Merge pull request #1172 from coralproject/update-bug-report-link
Update Report Bug link in Admin Settings
2017-11-27 16:27:40 +00:00
Kim Gardner e52a6fd9c1 Merge branch 'master' into update-bug-report-link 2017-11-27 16:15:04 +00:00
Kim Gardner 24af7b2812 Merge pull request #1180 from coralproject/autclose-firefox-bug
auto close input: fix styling bug in firefox
2017-11-27 13:17:30 +00:00
Kim Gardner 24b5a58584 Merge branch 'master' into autclose-firefox-bug 2017-11-27 13:07:10 +00:00
Kim Gardner a868cc9c67 Merge pull request #1178 from coralproject/more-padding-qbox
More padding for question box
2017-11-27 13:05:52 +00:00
Kim Gardner af83d32de1 Merge branch 'master' into more-padding-qbox 2017-11-27 12:51:50 +00:00
Kim Gardner fa82824d03 Merge pull request #1181 from coralproject/firefox-stories
Fix radiogroup not working properly in ff
2017-11-27 12:51:39 +00:00
Kim Gardner f0033a7912 Merge branch 'master' into firefox-stories 2017-11-27 12:41:14 +00:00
Kim Gardner 0924531677 Merge pull request #1182 from coralproject/sort-stories
Sort by publication_date
2017-11-27 12:40:50 +00:00
Kim Gardner 44b77597ed Merge branch 'master' into update-bug-report-link 2017-11-27 12:36:59 +00:00
Chi Vinh Le ec2bf3cb7f Sort by publication_date 2017-11-24 12:41:18 +01:00
Chi Vinh Le 37f3c11104 Fix radiogroup not working properly in ff 2017-11-24 12:29:29 +01:00
Chi Vinh Le 9359bfc322 Fix styling bug in firefox 2017-11-23 20:09:16 +01:00
Kiwi 111ebd67ba Merge branch 'master' into extendable-embed-stream-tab 2017-11-23 14:49:54 +01:00
Chi Vinh Le 31f87dc192 Pass hover props to extendable tabs in order to support firefox 2017-11-23 14:46:23 +01:00
Chi Vinh Le c9dad30c71 Merge branch 'extendable-embed-stream-tab' into firefox-hover-tab 2017-11-23 14:46:18 +01:00
Chi Vinh Le 7f53375c67 More padding for question box 2017-11-23 13:40:19 +01:00
Chi Vinh Le df0c016e37 Merge branch 'refactor-configure' of ssh://github.com/coralproject/talk into refactor-configure 2017-11-23 13:33:21 +01:00
Chi Vinh Le bf3fe2ab8e Merge branch 'master' into refactor-configure 2017-11-23 13:27:18 +01:00
Wyatt Johnson 9629ba2f83 added connector support 2017-11-22 14:48:34 -07:00
Kim Gardner ac7b4d56a1 Merge pull request #1174 from coralproject/show-all-comments-button
new design for show all comments button
2017-11-22 16:35:40 +00:00
Kim Gardner c29bb02b1c Merge branch 'master' into show-all-comments-button 2017-11-22 12:09:23 +00:00
Kim Gardner 8c72e32a25 Merge branch 'master' into update-bug-report-link 2017-11-22 12:09:17 +00:00
Kim Gardner 4a31bb7ba2 Merge branch 'master' into extendable-embed-stream-tab 2017-11-22 12:09:02 +00:00
Kim Gardner 3be1c94f8f Merge branch 'master' into refactor-configure 2017-11-22 12:07:26 +00:00
Kim Gardner 7fba0428bb Merge pull request #1175 from coralproject/cli-users-fix
User CLI Fix
2017-11-21 19:24:35 +00:00
Wyatt Johnson c7d433deb5 added better logic around removing users for clearning out their stale data. 2017-11-21 12:13:28 -07:00
Chi Vinh Le a665dbd88e Reproduce modqueue freeze issue 2017-11-21 18:07:00 +01:00
Chi Vinh Le c990a812a4 new design for show all comments button 2017-11-21 15:59:20 +01:00
Kim Gardner 3f26a8d7ad Merge branch 'master' into extendable-embed-stream-tab 2017-11-21 12:31:45 +00:00
Kiwi 4a3e5942b0 Merge branch 'master' into refactor-configure 2017-11-21 12:58:16 +01:00
Chi Vinh Le 5360bdc8ec Add some docs 2017-11-21 12:42:17 +01:00
Chi Vinh Le 548a204665 Expose StreamConfiguration 2017-11-21 11:13:41 +01:00
Chi Vinh Le a8e8b78441 Pass updatePending and errors through slots 2017-11-21 11:08:18 +01:00
Chi Vinh Le 258788fd88 Add updatePending and errors to admin settings slots 2017-11-21 11:06:11 +01:00
Chi Vinh Le 8cc3d19819 Fix query 2017-11-21 10:35:38 +01:00
Chi Vinh Le 0de5a328ab Slot streamSettings 2017-11-20 21:11:59 +01:00
Kim Gardner 4830aec138 Update Report Bug link in Admin Settings 2017-11-20 19:59:57 +00:00
Kim Gardner 4269fd1eb9 Merge pull request #1171 from coralproject/copy-updates
Update suspend copy
2017-11-20 19:53:00 +00:00
Kim Gardner 12e61bbc66 Update suspend copy 2017-11-20 19:41:14 +00:00
Chi Vinh Le 511e71f63a Change default settings=null to settings={} 2017-11-20 20:19:58 +01:00
Chi Vinh Le e675d716cb Remove deprecated code 2017-11-20 20:18:14 +01:00
Chi Vinh Le 2d6143af62 Fully implement mutation 2017-11-20 19:48:12 +01:00
Chi Vinh Le 873d1915c3 Implement apply 2017-11-20 19:20:09 +01:00
Chi Vinh Le 67f3f8cdef SimpleMDE don't trap keyboard 2017-11-20 18:14:17 +01:00
Chi Vinh Le 8f0eef7cb5 Reimplement QuestionBoxBuilder 2017-11-20 17:53:26 +01:00
Chi Vinh Le 4c7c3c9e42 Add premodLinks configuration 2017-11-20 12:36:49 +01:00
Chi Vinh Le 8f9724ed90 Implement configure and redux state 2017-11-20 12:29:23 +01:00
Chi Vinh Le 9fb7aa2657 Start implementing settings ui 2017-11-18 11:40:44 +01:00
Kiwi d714586279 Merge branch 'master' into extendable-embed-stream-tab 2017-11-17 17:12:42 +01:00
Chi Vinh Le 3e4c9dcb48 Refactor query tree 2017-11-17 17:02:32 +01:00
Chi Vinh Le 285ec98d54 Refactor styles 2017-11-17 16:53:56 +01:00
Chi Vinh Le 853b0ca6dd Automatic asset closure 2017-11-17 15:42:06 +01:00
Chi Vinh Le c58d9068e3 Implement open and close asset 2017-11-17 12:39:34 +01:00
Chi Vinh Le 78e0fb6cc6 Add UpdateAssetStatus mutation 2017-11-16 20:31:08 +01:00
Chi Vinh Le dfbb9ad96f Implement AssetStatusInfo UI 2017-11-16 20:31:00 +01:00
Kim Gardner 2d43eef508 Merge pull request #1167 from coralproject/query-status-history
Fix SetCommentStatus mutation on admin side
2017-11-16 19:05:23 +00:00
Kiwi fa22e45a4d Merge branch 'master' into query-status-history 2017-11-16 19:53:23 +01:00
Chi Vinh Le abdf5335da Add status_history to query 2017-11-16 19:52:47 +01:00
Wyatt Johnson c24454e234 Merge pull request #1165 from alleyinteractive/expose-recaptcha
Export CheckRecaptcha, so that it can be used in other services.
2017-11-16 11:36:25 -07:00
Wyatt Johnson 97e2e61a4b Merge branch 'master' into expose-recaptcha 2017-11-16 10:19:18 -07:00
Kim Gardner 57ced3c91f Merge pull request #1155 from coralproject/bump-version-3-8-0
Bump version 3.8.0
2017-11-16 16:57:36 +00:00
Chi Vinh Le f7c46b1a42 Move asset fragment up one level 2017-11-16 16:19:36 +01:00
Max Eddy c664438ade Export CheckRecaptcha, so that it can be used in other services. 2017-11-16 09:09:16 -05:00
Kim Gardner a5b5043a0d Merge branch 'master' into bump-version-3-8-0 2017-11-16 11:47:34 +00:00
Chi Vinh Le b5b6b113d4 Restructure coral-embed-stream 2017-11-16 10:57:16 +01:00
Chi Vinh Le a13d384cee Merge branch 'extendable-embed-stream-tab' into refactor-configure 2017-11-16 10:42:15 +01:00
Kiwi 3a0a854317 Merge branch 'master' into extendable-embed-stream-tab 2017-11-16 10:39:52 +01:00
Chi Vinh Le 73c3d31fbc Merge branch 'master' into refactor-configure 2017-11-16 10:30:16 +01:00
Belén Curcio 03a0d904d2 Merge pull request #1164 from coralproject/correct-proptype
Correct proptype
2017-11-15 16:45:30 -03:00
Chi Vinh Le 315d3babde Correct prop types 2 2017-11-15 20:03:41 +01:00
Chi Vinh Le 4d6055b9b8 Correct prop type 2017-11-15 19:53:58 +01:00
Chi Vinh Le d3d1148ee6 Merge branch 'master' into refactor-configure 2017-11-15 19:49:09 +01:00
Kiwi ecd9219e17 Merge branch 'master' into extendable-embed-stream-tab 2017-11-15 19:46:04 +01:00
Chi Vinh Le ce4ad426a7 Extendable embed stream tabs 2017-11-15 19:38:25 +01:00
Kim Gardner 77c8d966ab Merge pull request #1162 from coralproject/slot-classnames
Slot Classnames
2017-11-15 18:20:21 +00:00
Kim Gardner cabadf467f Merge branch 'master' into slot-classnames 2017-11-15 18:09:11 +00:00
Kim Gardner 0807acc98f Merge pull request #1159 from coralproject/more-undo
Enhanced undo
2017-11-15 18:09:04 +00:00
Belén Curcio 3c5b56c599 Merge branch 'master' into more-undo 2017-11-15 15:00:48 -03:00
Belen Curcio 899bba7327 Adding Proptypes and classNames for the Slots 🎉 2017-11-15 14:50:33 -03:00
Belen Curcio 41dd3eb180 Merge branch 'master' of github.com:coralproject/talk 2017-11-15 14:40:22 -03:00
Chi Vinh Le e02537df13 Fix mutation syntax 2017-11-15 17:53:24 +01:00
Kim Gardner 81d90cb138 Merge pull request #1161 from coralproject/break-word-2
Support long words for my and featured comments
2017-11-15 12:46:49 +00:00
Chi Vinh Le e4a359e0b4 Add mutation to framework 2017-11-15 13:38:02 +01:00
Chi Vinh Le 2882dfbadb Fix flexbox overflowing 2017-11-15 13:34:29 +01:00
Chi Vinh Le f2b9db9dbc Support long words for my and featured comments 2017-11-15 13:07:33 +01:00
Kim Gardner c563a13e7c Merge pull request #1160 from coralproject/break-word
Break long words in comment
2017-11-14 16:31:55 +00:00
Belen Curcio 5976595871 Merge branch 'master' of github.com:coralproject/talk 2017-11-14 13:03:10 -03:00
Chi Vinh Le 8a4a7d3589 Break long words in comment 2017-11-14 15:37:26 +01:00
Chi Vinh Le 5073e60df5 Translate 2017-11-14 14:05:30 +01:00
Chi Vinh Le 5a71db2568 Undo to previous status + Live updates 2017-11-14 13:58:18 +01:00
Kiwi ac0acea22b Merge pull request #1149 from devonJS/undo-reject
undo reject comment on stream
2017-11-14 12:49:48 +01:00
Belen Curcio 6198c9e730 Merge branch 'master' of github.com:coralproject/talk 2017-11-13 12:34:44 -03:00
Kiwi 9ac06281e9 Merge branch 'master' into undo-reject 2017-11-13 14:00:21 +01:00
Kim Gardner 78e5cb7aa6 Merge branch 'master' into bump-version-3-8-0 2017-11-13 12:49:47 +00:00
Kiwi 882bf57bbc Merge pull request #1152 from coralproject/user-status
More e2e tests - User Status
2017-11-10 21:16:59 +01:00
Chi Vinh Le 789fb3dfce Disable bs on this branch 2017-11-10 20:59:01 +01:00
Belen Curcio 819b1c597c refactor: working tests 2017-11-10 13:04:46 -03:00
Belen Curcio 38148ad51c Adding post animation test 2017-11-10 12:09:55 -03:00
Belen Curcio 8f6ff46244 passing suspendUserDialog to the adminPage scope 2017-11-10 11:16:35 -03:00
Belen Curcio 4727c52de4 camelCase lodash 2017-11-10 10:48:12 -03:00
Belen Curcio f461d27810 Merge branch 'master' of github.com:coralproject/talk into user-status 2017-11-10 10:46:53 -03:00
Belen Curcio fbc5a9d74b moderate 2017-11-10 10:45:26 -03:00
Belen Curcio 6ff4994d0d lodash.camelcase 2017-11-10 10:33:55 -03:00
Belen Curcio c793a840ea Merge branch 'user-status' of github.com:coralproject/talk into user-status 2017-11-10 10:27:28 -03:00
Belen Curcio ebda51dc0f Adding suspension test 2017-11-10 10:26:54 -03:00
Belen Curcio b0573749ee Adding classes and missingproptypes 2017-11-10 10:26:34 -03:00
Chi Vinh Le 933903de2f Remove IE 64bit from ci until they fix the keyboard input issues 2017-11-10 14:05:43 +01:00
Kim Gardner 4373c382e1 Merge pull request #1151 from coralproject/feature/fix-email-config-docs
Fix incorrect email configuration in docs
2017-11-10 12:02:22 +00:00
Kim Gardner af6787bc0f Merge branch 'master' into feature/fix-email-config-docs 2017-11-10 11:37:15 +00:00
Chi Vinh Le dd1fbf30ca Add navigateAndLogin 2017-11-10 10:51:30 +01:00
Chi Vinh Le a5aa59e255 Merge branch 'master' into user-status 2017-11-10 10:09:29 +01:00
Kiwi 19b8d0a6ea Merge pull request #1150 from coralproject/e2e-v2
e2e v2
2017-11-10 10:08:27 +01:00
Chi Vinh Le bcf8370359 Temporarily enable bs testing on this branch 2017-11-10 10:04:22 +01:00
Wyatt Johnson 0da0b1a37e Merge branch 'master' into feature/fix-email-config-docs 2017-11-09 16:23:24 -07:00
Chi Vinh Le 31ee8139b6 Use visible were suitable and wait for drawer to dissappear 2017-11-09 23:32:03 +01:00
Wyatt Johnson 51977dcf54 Merge branch 'master' into bump-version-3-8-0 2017-11-09 15:10:24 -07:00
Chi Vinh Le 0b18bb06e7 More tests 2017-11-09 22:31:53 +01:00
Chi Vinh Le 9abb3d30da e2e fixes 2017-11-09 22:04:32 +01:00
Chi Vinh Le f0839c57df Change to allowed name 2017-11-09 21:52:08 +01:00
Chi Vinh Le b2929e9a83 Fail fast 2017-11-09 21:51:41 +01:00
Chi Vinh Le ca7b790962 Return this 2017-11-09 21:51:31 +01:00
Chi Vinh Le 42a477ae41 Fix firefox popup bug e2e 2017-11-09 21:51:03 +01:00
Chi Vinh Le 53461df287 Increase wait for iframe time 2017-11-09 19:30:22 +01:00
Kiwi 9b18a78ce0 Merge branch 'master' into user-status 2017-11-09 19:20:06 +01:00
Chi Vinh Le da35f39baf Set resolution 2017-11-09 19:19:47 +01:00
Chi Vinh Le e885178dfc Refactor 2 2017-11-09 19:03:01 +01:00
Kim Gardner 24e8aab0fe Merge branch 'master' into e2e-v2 2017-11-09 17:09:56 +00:00
Kim Gardner 44d6ad340d Merge pull request #1156 from coralproject/queue-fix
Queue fix for custom assets
2017-11-09 17:09:25 +00:00
Chi Vinh Le 63b6315435 Refactor #1 2017-11-09 17:50:47 +01:00
Kiwi 95ca43ff8c Merge branch 'master' into e2e-v2 2017-11-09 16:23:20 +01:00
Chi Vinh Le 55e4aed511 Merge branch 'master' into user-status 2017-11-09 15:53:42 +01:00
Kim Gardner 6d5adc8f54 Bump version number to 3.8.0 2017-11-09 13:19:05 +00:00
Kim Gardner 247e05383e One more fix 2017-11-09 11:23:48 +00:00
Kim Gardner 3988fb9e93 Merge branch 'feature/fix-email-config-docs' of git+ssh://github.com/coralproject/talk into feature/fix-email-config-docs 2017-11-09 11:20:07 +00:00
Kim Gardner 7d524efaad Merge branch 'master' into feature/fix-email-config-docs 2017-11-09 11:19:57 +00:00
Kim Gardner d7a88cdc2a Fix config key 2017-11-09 11:18:58 +00:00
Wyatt Johnson 18f5b0cc5e queue fix for sometimes null settings 2017-11-08 14:24:48 -07:00
Kiwi 2df18f07cb Merge pull request #1140 from coralproject/user-pending
Users with PENDING status should not be allowed to post & E2E for User Status
2017-11-08 19:27:41 +01:00
Chi Vinh Le 786e85686d Fix invalid regexp for ie 2017-11-08 18:57:06 +01:00
Belen Curcio 9a651880a2 Updated login 2017-11-08 14:00:01 -03:00
Belen Curcio 82e01eac00 Merge branch 'master' of github.com:coralproject/talk into user-status 2017-11-08 13:56:48 -03:00
Belen Curcio 5efdc9865f refacotr 2017-11-08 12:59:08 -03:00
Belen Curcio 62d01cf2a0 perform login 2017-11-08 12:31:33 -03:00
Belen Curcio 81bf63836a tests 2017-11-08 12:23:40 -03:00
Belen Curcio 54ac2985ba Adding classes 2017-11-08 12:23:29 -03:00
Belen Curcio e9bd5703f1 refactor 2017-11-08 11:49:32 -03:00
Jeff Nelson a394ba29b3 Update 02-02-advanced-configuration.md
Change TALK_SMTP_EMAIL to TALK_SMTP_FROM_EMAIL
2017-11-08 07:11:03 -05:00
Belen Curcio a2470fa01b More 2017-11-07 16:43:58 -03:00
Belen Curcio 9b98e3cac9 missing classes 2017-11-07 15:20:14 -03:00
Chi Vinh Le 4f89c1e1c3 Add some comments and description 2017-11-07 17:50:56 +01:00
Chi Vinh Le c3e551deb5 Disable bs for branches 2017-11-07 17:48:12 +01:00
Chi Vinh Le 0678ea0344 Small tweak 2017-11-07 17:07:50 +01:00
Chi Vinh Le b09b56bd24 Temporarily enable browserstack 2017-11-07 16:52:27 +01:00
Chi Vinh Le 2a0f70f084 Clean DB before serve 2017-11-07 16:42:37 +01:00
Kiwi bf2e11ef9a Merge branch 'master' into e2e-v2 2017-11-07 16:29:51 +01:00
Chi Vinh Le 0fc810a7b1 e2e v2 2017-11-07 16:21:09 +01:00
Belen Curcio 918ad915b8 Tests 2017-11-06 23:53:33 -03:00
Belen Curcio d51b6b31f6 Adding missing classes and proptypes 2017-11-06 23:53:19 -03:00
Belén Curcio e88445beac Merge branch 'master' into user-pending 2017-11-06 13:48:26 -03:00
Belen Curcio 33be7531a0 Merge branch 'master' of github.com:coralproject/talk 2017-11-06 12:58:15 -03:00
Devon Jue 2131638f48 undo reject comment on stream 2017-11-04 17:17:30 -07:00
Kim Gardner 39a980b22f Merge pull request #1146 from coralproject/isStaff
Use isStaff instead
2017-11-01 17:38:37 +00:00
Chi Vinh Le ce0b63fe75 Use isStaff instead 2017-11-01 13:29:04 +01:00
Kim Gardner c0cb641df3 Merge branch 'master' into user-pending 2017-11-01 11:27:04 +00:00
Kiwi cee68ae471 Merge pull request #1142 from PepeFranco/reportedComments
Reported comments
2017-11-01 12:17:25 +01:00
Kiwi 33cd783b37 Merge branch 'master' into reportedComments 2017-11-01 12:10:39 +01:00
Kiwi 82ef16a4ee Merge pull request #1144 from blackcathacker/queue_shortcuts
add numbered shortcuts for mod queue
2017-11-01 12:10:17 +01:00
Kiwi 736ab6066f Merge branch 'master' into reportedComments 2017-11-01 12:06:34 +01:00
Kiwi 3327604a58 Merge branch 'master' into queue_shortcuts 2017-11-01 12:04:03 +01:00
Kiwi 0c3cfd21a9 Merge pull request #1143 from blackcathacker/staff_badge
show staff instead of user badge when reported by staff
2017-11-01 11:59:56 +01:00
blackcathacker 903e70d641 updates per review 2017-10-31 17:10:25 -07:00
blackcathacker 21f6caa942 cleaner method chain per review 2017-10-31 16:50:35 -07:00
PepeFranco f70ccf641a Fixed lint issue 2017-10-31 13:29:44 -06:00
PepeFranco dd4e3d7ec8 Merge branch 'reportedComments' of https://github.com/PepeFranco/talk into reportedComments 2017-10-31 13:00:05 -06:00
PepeFranco 886a468993 Reverting comment changes 2017-10-31 12:56:32 -06:00
PepeFranco de0dad6243 Merge branch 'master' of https://github.com/coralproject/talk into reportedComments
Updating
2017-10-31 12:49:44 -06:00
Kiwi 7226c475e3 Merge branch 'master' into user-pending 2017-10-31 19:25:51 +01:00
Kim Gardner 173c9262d1 Merge branch 'master' into queue_shortcuts 2017-10-31 17:18:59 +00:00
Kim Gardner 5e7ee344ec Merge branch 'master' into staff_badge 2017-10-31 17:18:54 +00:00
Kim Gardner b2d0c58a72 Merge branch 'master' into reportedComments 2017-10-31 17:12:02 +00:00
Kiwi b34b033a18 Merge pull request #1139 from coralproject/bs-stabilize
Stabilize browserstack tests
2017-10-31 17:47:50 +01:00
Chi Vinh Le 4195d8b419 Merge branch 'bs-stabilize' of ssh://github.com/coralproject/talk into bs-stabilize 2017-10-31 17:34:59 +01:00
Chi Vinh Le bfb2da519f Exit code can be negative 2017-10-31 17:34:28 +01:00
Chi Vinh Le 8821c0aa5c Typos 2017-10-31 17:33:44 +01:00
Kiwi a56031ac47 Merge branch 'master' into bs-stabilize 2017-10-31 17:13:25 +01:00
Wyatt Johnson a3eeec34df Merge pull request #1137 from blackcathacker/premod_setting
Send user an email when they are banned #1050
2017-10-31 10:06:00 -06:00
Kim Gardner c4ad667788 Merge branch 'master' into user-pending 2017-10-31 16:03:07 +00:00
Wyatt Johnson d20666adb9 Update users.js
Prefix added by mailer service
2017-10-31 10:00:47 -06:00
Chi Vinh Le 96b44b2b99 Prefix env variables 2017-10-31 16:39:41 +01:00
Wyatt Johnson b92b4027b0 Merge branch 'master' into premod_setting 2017-10-31 09:32:43 -06:00
Jose Franco f1423abdea Merge branch 'master' into reportedComments 2017-10-31 09:31:32 -06:00
Chi Vinh Le c31e6347d0 Sleep between retries 2017-10-31 16:29:32 +01:00
Chi Vinh Le 48179b413c Merge branch 'bs-stabilize' of ssh://github.com/coralproject/talk into bs-stabilize 2017-10-31 16:25:24 +01:00
Kiwi c955fcd29e Merge branch 'master' into bs-stabilize 2017-10-31 16:25:18 +01:00
Kim Gardner 457322e3d0 Merge pull request #1145 from coralproject/readme-updates
Readme updates
2017-10-31 14:40:53 +00:00
Kim Gardner 9ba8cbcbe1 Fix logo link 2017-10-31 14:30:32 +00:00
Kim Gardner 93363a0650 Remove indenting 2017-10-31 14:27:38 +00:00
Chi Vinh Le 7f442ce91e Let MAX_RETRIES be customizable 2017-10-31 14:58:40 +01:00
Chi Vinh Le 42f0d76d98 Add retry number to console outpu 2017-10-31 14:58:25 +01:00
Chi Vinh Le 1578316363 Retry browsers when failed, print summary 2017-10-31 14:25:58 +01:00
Kim Gardner 5c033dbfdf Tweaks 2017-10-31 11:40:33 +00:00
Kim Gardner 3cdf2a3118 Formatting 2017-10-31 11:31:13 +00:00
Kim Gardner 4e5e989d22 Update one-click directions 2017-10-31 11:25:22 +00:00
Kim Gardner 62660ed144 Fix header 2017-10-31 11:18:24 +00:00
Kim Gardner 2d44f7abf4 Spruce up README 2017-10-31 11:12:14 +00:00
Kim Gardner ea98f0275d Merge branch 'master' into premod_setting 2017-10-31 10:35:44 +00:00
blackcathacker 07e6c15c3a add numbered shortcuts for mod queue 2017-10-30 23:08:33 -07:00
blackcathacker 05e91129f4 show staff instead of user badge when reported by staff 2017-10-30 22:16:59 -07:00
PepeFranco 350e679abf Added check for user & comments 2017-10-30 16:58:51 -06:00
PepeFranco ae07dfb2cd Added check for user & comments 2017-10-30 16:56:52 -06:00
Chi Vinh Le 88d012755c Always use windows 10 2017-10-30 23:16:23 +01:00
Chi Vinh Le 0cdefa0066 Fix wrong typed characters 2017-10-30 21:52:36 +01:00
Chi Vinh Le e4688f9876 Pause to let iframe load 2017-10-30 19:08:42 +01:00
Chi Vinh Le e091fa552a Upgrade selenium standalone 2017-10-30 17:02:20 +01:00
Chi Vinh Le 53b1104cf8 Add little pause waiting for popup to open 2017-10-30 16:09:16 +01:00
Chi Vinh Le 975fe550e0 Harmonize local chrome headless window-size 2017-10-30 16:08:03 +01:00
Chi Vinh Le 8adff14263 Reactivate e2e on ci 2017-10-30 15:53:52 +01:00
Kiwi 5f07d0256c Merge branch 'master' into bs-stabilize 2017-10-30 15:40:17 +01:00
Kim Gardner f573504a3c Merge pull request #1141 from coralproject/update-version
Update version number to be correct
2017-10-30 14:34:26 +00:00
Kim Gardner 8c0118106b Update version number to be correct 2017-10-30 14:24:15 +00:00
Kiwi 4ce720e510 Merge branch 'master' into bs-stabilize 2017-10-30 14:46:32 +01:00
Kim Gardner 81c669d63f Merge branch 'master' into user-pending 2017-10-30 13:34:09 +00:00
Chi Vinh Le 97a8f95b93 Use win10 for ie browsers 2017-10-30 14:25:18 +01:00
Kim Gardner 7bf114c01a Merge branch 'master' into premod_setting 2017-10-30 13:25:14 +00:00
Kim Gardner 6460cbd536 Merge pull request #1138 from blackcathacker/disable_report
disable report button when comment already reported
2017-10-30 13:24:10 +00:00
Chi Vinh Le 05b326b91c Add comments 2017-10-30 14:14:49 +01:00
Chi Vinh Le 9af206da1a Correct echo output 2017-10-30 14:08:13 +01:00
Chi Vinh Le 0c3a9db443 Add more console logs 2017-10-30 13:52:11 +01:00
Chi Vinh Le 511f4f0975 Refactor 2017-10-30 13:26:50 +01:00
Chi Vinh Le 95545a0e7d Merge branch 'bs-stabilize' of ssh://github.com/coralproject/talk into bs-stabilize 2017-10-30 13:13:23 +01:00
Chi Vinh Le 12a71fa117 Wait before click 2017-10-30 13:11:41 +01:00
Belen Curcio 302d30cde1 Adding missing classnames and proptypes, plus e2e testing covering userstatus 2017-10-30 08:54:32 -03:00
Kiwi 3da4a70442 Merge branch 'master' into bs-stabilize 2017-10-30 12:50:15 +01:00
Belen Curcio 6d7afc7bbc Adding missing classes and props 2017-10-30 08:07:03 -03:00
Belen Curcio b6786f614e Adding missing props 2017-10-30 07:56:51 -03:00
Belen Curcio a6a903c459 e2e tests 2017-10-30 07:52:08 -03:00
Belen Curcio 34b58779e8 User PENDING status should not be able to post 2017-10-30 07:04:39 -03:00
blackcathacker 82a332c787 disable report button when comment already reported 2017-10-29 19:48:42 -07:00
blackcathacker bfe82218ed Send user an email when they are banned #1050 2017-10-29 18:48:15 -07:00
Chi Vinh Le 5f5c64870a Stabilize e2e 2017-10-27 19:00:48 +02:00
Chi Vinh Le ad4a5595f9 Enable screenshot on failure for bs 2017-10-27 13:19:00 +02:00
Chi Vinh Le b84daf0422 Set resolution to 1600x1200 bs 2017-10-26 17:30:55 +02:00
231 changed files with 4001 additions and 2758 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
{
"exceptions": [
"https://nodesecurity.io/advisories/531"
"https://nodesecurity.io/advisories/531",
"https://nodesecurity.io/advisories/532"
]
}
}
+7 -3
View File
@@ -4,8 +4,12 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied.
See the License for the specific language governing permissions and limitations under the License.
See the License for the specific language governing permissions
and limitations under the License.
+22 -31
View File
@@ -1,46 +1,37 @@
# Talk
# Talk · [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) · [![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8/badge)](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8) · [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests)
[![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
[![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8/badge)](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
Built with <3 by The Coral Project & Mozilla.
## Getting Started
## Try Talk!
Check out our Docs: https://coralproject.github.io/talk/
You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
## Technical Documentation
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://coralproject.github.io/talk/) have everything you need to know.
## Product Guide
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://coralproject.github.io/talk/how-talk-works).
## Relevant Links
- Blog: https://blog.coralproject.net/
- Community Forums: https://community.coralproject.net/
- Community Guides for Journalism: https://guides.coralproject.net/
- Project: https://coralproject.net/
- Roadmap: https://www.pivotaltracker.com/n/projects/1863625
- [Our Blog](https://blog.coralproject.net/)
- [Community Forums](https://community.coralproject.net/)
- [Community Guides for Journalism](https://guides.coralproject.net/)
- [More About Us](https://coralproject.net/)
- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
## End-to-End Testing
Talk uses [Nightwatch](http://nightwatchjs.org/) to write e2e tests. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at Browserstack.
Talk uses [Nightwatch](https://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com).
![](/public/img/browserstack_logo.png)
[![Browserstack](/public/img/browserstack_logo.png)](https://browserstack.com)
## License
Copyright 2017 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied.
See the License for the specific language governing permissions
and limitations under the License.
Talk is released under the [Apache License, v2.0](/LICENSE).
-12
View File
@@ -1,14 +1,11 @@
const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const merge = require('lodash/merge');
const helmet = require('helmet');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const {HELMET_CONFIGURATION} = require('./config');
const {MOUNT_PATH} = require('./url');
const {applyLocals} = require('./services/locals');
const routes = require('./routes');
const debug = require('debug')('talk:app');
@@ -36,12 +33,6 @@ app.use(helmet(merge(HELMET_CONFIGURATION, {
// Compress the responses if appropriate.
app.use(compression());
// Parse the cookies on the request.
app.use(cookieParser());
// Parse the body json if it's there.
app.use(bodyParser.json());
//==============================================================================
// VIEW CONFIGURATION
//==============================================================================
@@ -53,9 +44,6 @@ app.set('view engine', 'ejs');
// ROUTES
//==============================================================================
// Add the locals to the app renderer.
applyLocals(app.locals);
debug(`mounting routes on the ${MOUNT_PATH} path`);
// Actually apply the routes.
+44 -23
View File
@@ -12,7 +12,7 @@ const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
util.onshutdown([
() => mongoose.disconnect()
() => mongoose.disconnect(),
]);
/**
@@ -20,17 +20,17 @@ util.onshutdown([
*/
function processJobs() {
// Start the scraper processor.
scraper.process();
// Start the mail processor.
mailer.process();
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => kue.Task.shutdown()
]);
// Start the scraper processor.
scraper.process();
// Start the mail processor.
mailer.process();
}
/**
@@ -48,22 +48,13 @@ function removeJob(job) {
}));
}
/**
* Removes the jobs passed in and returns a promise.
* @param {Array} jobs array of jobs
* @return {Promise}
*/
function removeJobs(jobs) {
return Promise.all(jobs.map(removeJob));
}
/**
* Get the top n jobs with a specific state.
* @param {String} [state='complete'] state to list jobs by
* @param {Number} limit limit of jobs to load
* @return {Promise}
*/
function rangeJobsByState(state = 'complete', limit) {
function rangeJobsByState(state, limit) {
return new Promise((resolve, reject) => {
kue.Job.rangeByState(state, 0, limit, 'asc', (err, jobs) => {
if (err) {
@@ -75,22 +66,52 @@ function rangeJobsByState(state = 'complete', limit) {
});
}
async function getJobBatch(n, includeStuck) {
let jobs = [];
jobs = await rangeJobsByState('complete', n);
if (includeStuck) {
jobs = jobs.concat(await rangeJobsByState('failed', n));
}
return jobs;
}
/**
* Cleans up the jobs that are in the queue.
*/
async function cleanupJobs(options) {
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => kue.Task.shutdown()
]);
const n = 100;
try {
const joblists = await Promise.all([
rangeJobsByState('complete', n),
options.stuck ? rangeJobsByState('failed', n) : false
]);
await joblists.filter((jobs) => jobs).map(removeJobs);
// Connect to redis by establishing a queue.
kue.Task.connect();
let jobCount = 0;
let jobs = await getJobBatch(n, options.stuck);
while (jobs.length > 0) {
// Remove all the jobs.
await Promise.all(jobs.map((job) => removeJob(job)));
jobCount += jobs.length;
// Get the next batch of jobs.
jobs = await getJobBatch(n, options.stuck);
}
util.shutdown();
console.log('Removed old jobs');
console.log(`Removed ${jobCount} jobs`);
} catch (err) {
console.error(err);
util.shutdown(1);
+5 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
const program = require('./commander');
const util = require('./util');
const serve = require('../serve');
//==============================================================================
@@ -13,5 +14,8 @@ program
.parse(process.argv);
// Start serving.
serve({jobs: program.jobs, websockets: program.websockets});
serve({jobs: program.jobs, websockets: program.websockets}).catch((err) => {
console.error(err);
util.shutdown(1);
});
+39 -14
View File
@@ -8,10 +8,13 @@ const program = require('./commander');
const inquirer = require('inquirer');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const CommentModel = require('../models/comment');
const ActionModel = require('../models/action');
const USER_ROLES = require('../models/enum/user_roles');
const mongoose = require('../services/mongoose');
const util = require('./util');
const Table = require('cli-table');
const databaseVerifications = require('./verifications/database');
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
if (input && input.length >= len) {
@@ -122,26 +125,48 @@ async function createUser(options) {
} catch (err) {
console.error(err);
util.shutdown();
util.shutdown(1);
}
}
/**
* Deletes a user.
*/
function deleteUser(userID) {
UserModel
.findOneAndRemove({
id: userID
})
.then(() => {
console.log('Deleted user');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown();
});
async function deleteUser(userID) {
try {
// Find the user we're removing.
const user = await UserModel.findOne({id: userID});
if (!user) {
throw new Error(`user with id ${userID} not found`);
}
// Remove all the user's actions.
await ActionModel
.where({user_id: user.id})
.setOptions({multi: true})
.remove();
// Remove all the user's comments.
await CommentModel
.where({author_id: user.id})
.setOptions({multi: true})
.remove();
// Update the counts that might have changed.
for (const verification of databaseVerifications) {
await verification({fix: true, limit: Infinity, batch: 1000});
}
// Remove the user.
await user.remove();
util.shutdown();
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
/**
+1 -1
View File
@@ -50,7 +50,7 @@ test:
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
# Check dependancies using nsp.
- nsp check
# - yarn e2e-ci
- yarn e2e-ci
deployment:
release:
-2
View File
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import Configure from 'routes/Configure';
import Dashboard from 'routes/Dashboard';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community/containers/Community';
@@ -18,7 +17,6 @@ const routes = (
<IndexRedirect to='/admin/moderate' />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
<Route path='dashboard' component={Dashboard} />
{/* Community Routes */}
@@ -32,8 +32,9 @@ class ActionsMenu extends React.Component {
};
render() {
const {className = ''} = this.props;
return (
<div className={styles.root} onBlur={this.syncOpenState} >
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
@@ -59,6 +60,7 @@ class ActionsMenu extends React.Component {
ActionsMenu.propTypes = {
icon: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
};
export default ActionsMenu;
@@ -3,12 +3,14 @@ import cn from 'classnames';
import {MenuItem} from 'react-mdl';
import PropTypes from 'prop-types';
import styles from './ActionsMenu.css';
import camelCase from 'lodash/camelCase';
const ActionsMenuItem = (props) =>
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
ActionsMenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.string,
};
export default ActionsMenuItem;
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick}) => {
const ApproveButton = ({active, minimal, onClick, className}) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
@@ -21,6 +21,7 @@ const ApproveButton = ({active, minimal, onClick}) => {
};
ApproveButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
@@ -1,4 +1,5 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import styles from './BanUserDialog.css';
@@ -8,28 +9,34 @@ import t from 'coral-framework/services/i18n';
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
<Dialog
className={styles.dialog}
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')}>
<span className={styles.close} onClick={onCancel}>×</span>
<div>
<div className={styles.header}>
<h2>{t('bandialog.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{t('bandialog.are_you_sure', username)}</h3>
<i>{info}</i>
</div>
<div className={styles.buttons}>
<Button cStyle="cancel" className={styles.cancel} onClick={onCancel} raised>
{t('bandialog.cancel')}
</Button>
<Button cStyle="black" className={styles.ban} onClick={onPerform} raised>
{t('bandialog.yes_ban_user')}
</Button>
</div>
<div className={styles.header}>
<h2>{t('bandialog.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{t('bandialog.are_you_sure', username)}</h3>
<i>{info}</i>
</div>
<div className={styles.buttons}>
<Button
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
cStyle="cancel"
onClick={onCancel}
raised >
{t('bandialog.cancel')}
</Button>
<Button
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
{t('bandialog.yes_ban_user')}
</Button>
</div>
</Dialog>
);
@@ -15,7 +15,15 @@ function generateRegExp(phrases) {
.join('[\\s"?!.]+')
).join('|');
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
@@ -6,10 +6,21 @@ import FlagLabel from 'coral-ui/components/FlagLabel';
import cn from 'classnames';
import styles from './CommentLabels.css';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
function isUserFlagged(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
}
function getUserFlaggedType(actions) {
return actions
.some((action) =>
action.__typename === 'FlagAction' &&
action.user &&
action.user.roles.some((role) => staffRoles.includes(role))
) ? 'Staff' : 'User';
}
function hasSuspectedWords(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
}
@@ -24,7 +35,7 @@ const CommentLabels = ({comment, comment: {className, status, actions, hasParent
<div className={styles.coreLabels}>
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
{isUserFlagged(actions) && <FlagLabel iconName="person">User</FlagLabel>}
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
</div>
+2 -8
View File
@@ -12,20 +12,14 @@ const CoralDrawer = ({handleLogout, auth = {}}) => (
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<div>
<Navigation className={styles.nav}>
<IndexLink
className={cn('talk-admin-nav-dashboard', styles.navLink)}
to="/admin/dashboard"
activeClassName={styles.active}>
{t('configure.dashboard')}
</IndexLink>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<Link
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</Link>
</IndexLink>
)
}
<Link
@@ -4,34 +4,38 @@ import Modal from 'components/Modal';
import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
const shortcuts = [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
];
export default class ModerationKeysModal extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired
shortcutsNoteVisible: PropTypes.string.isRequired,
queueCount: PropTypes.number.isRequired
}
buildShortcuts = () => {
return [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
];
}
render () {
@@ -51,7 +55,7 @@ export default class ModerationKeysModal extends React.Component {
<Modal open={open} onClose={onClose}>
<h3>{t('modqueue.shortcuts')}</h3>
<div className={styles.container}>
{shortcuts.map((shortcut, i) => (
{this.buildShortcuts().map((shortcut, i) => (
<table className={styles.table} key={i}>
<thead>
<tr>
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick}) => {
const RejectButton = ({active, minimal, onClick, className}) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
@@ -21,6 +21,7 @@ const RejectButton = ({active, minimal, onClick}) => {
};
RejectButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import styles from './SuspendUserDialog.css';
import cn from 'classnames';
import Button from 'coral-ui/components/Button';
@@ -87,7 +88,7 @@ class SuspendUserDialog extends React.Component {
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
{t('suspenduser.cancel')}
</Button>
<Button cStyle="black" className={styles.perform} onClick={this.goToStep1} raised>
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
{t('suspenduser.suspend_user')}
</Button>
</div>
@@ -120,7 +121,7 @@ class SuspendUserDialog extends React.Component {
</Button>
<Button
cStyle="black"
className={styles.perform}
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
onClick={this.handlePerform}
disabled={this.state.message.length === 0}
raised
@@ -137,7 +138,7 @@ class SuspendUserDialog extends React.Component {
const {step} = this.state;
return (
<Dialog
className={styles.dialog}
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
onCancel={onCancel}
open={open}
>
+6 -13
View File
@@ -23,23 +23,16 @@ const CoralHeader = ({
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
<IndexLink
id='dashboardNav'
className={cn('talk-admin-nav-dashboard', styles.navLink)}
to="/admin/dashboard"
activeClassName={styles.active}>
{t('configure.dashboard')}
</IndexLink>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<Link
<IndexLink
id='moderateNav'
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
</Link>
</IndexLink>
)
}
<Link
@@ -76,9 +69,9 @@ const CoralHeader = ({
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings"/>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
@@ -87,11 +80,11 @@ const CoralHeader = ({
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
<a href="https://support.coralproject.net" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
{t('configure.sign_out')}
</MenuItem>
</Menu>
@@ -25,6 +25,7 @@ export default withFragments({
}
user {
id
roles
}
}
${getSlotFragmentSpreads(slots, 'comment')}
+1 -1
View File
@@ -19,6 +19,6 @@ export default withQuery(gql`
}
`, {
options: {
pollInterval: 5000
pollInterval: 10000
}
})(Header);
@@ -1,4 +1,5 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import SuspendUserDialog from '../components/SuspendUserDialog';
@@ -44,6 +45,12 @@ class SuspendUserDialogContainer extends Component {
}
}
SuspendUserDialogContainer.propTypes = {
open: PropTypes.bool,
hideSuspendUserDialog: PropTypes.func,
username: PropTypes.string,
};
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
@@ -19,7 +19,7 @@ import update from 'immutability-helper';
import {notify} from 'coral-framework/actions/notification';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
nodes {
...${getDefinitionName(UserDetailComment.fragments.comment)}
}
@@ -155,7 +155,7 @@ export const withUserDetailQuery = withQuery(gql`
author_id: $author_id,
statuses: $statuses
}) {
...CoralAdmin_Moderation_CommentConnection
...CoralAdmin_UserDetail_CommentConnection
}
...${getDefinitionName(UserDetailComment.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
+1 -11
View File
@@ -1,15 +1,5 @@
import update from 'immutability-helper';
import mapValues from 'lodash/mapValues';
// Map nested object leaves. Array objects are considered leaves.
function mapLeaves(o, mapper) {
return mapValues(o, (val) => {
if (typeof val === 'object' && !Array.isArray(val)) {
return mapLeaves(val, mapper);
}
return mapper(val);
});
}
import {mapLeaves} from 'coral-framework/utils';
export default {
mutations: {
@@ -1,15 +0,0 @@
// this is initialized here because
// currently you have to reload the dashboard to get new stats
// cleaner updates are planned in the future.
const DASHBOARD_WINDOW_MINUTES = 5;
let then = new Date();
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
const initialState = {
windowStart: then.toISOString(),
windowEnd: new Date().toISOString(),
};
export default function dashboard (state = initialState, _action) {
return state;
}
-2
View File
@@ -1,6 +1,5 @@
import auth from './auth';
import stories from './stories';
import dashboard from './dashboard';
import configure from './configure';
import community from './community';
import moderation from './moderation';
@@ -13,7 +12,6 @@ import userDetail from './userDetail';
export default {
auth,
banUserDialog,
dashboard,
configure,
suspendUserDialog,
userDetail,
@@ -13,11 +13,11 @@ const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div>
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-flagged-accounts`} activeClassName={styles.active}>
{t('community.flaggedaccounts')}
<CountBadge count={flaggedUsernamesCount} />
</Link>
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`} activeClassName={styles.active}>
{t('community.people')}
</Link>
</div>
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import LoadMore from '../../../components/LoadMore';
@@ -23,7 +24,7 @@ class FlaggedAccounts extends React.Component {
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={styles.container}>
<div className={cn('talk-adnin-community-flagged-accounts', styles.container)}>
<div className={styles.mainFlaggedContent}>
{
hasResults
@@ -70,4 +71,15 @@ class FlaggedAccounts extends React.Component {
}
}
FlaggedAccounts.propTypes = {
users: PropTypes.object,
loadMore: PropTypes.func,
showBanUserDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
me: PropTypes.object,
viewUserDetail: PropTypes.func,
};
export default FlaggedAccounts;
@@ -1,18 +1,14 @@
import React from 'react';
import styles from './FlaggedUser.css';
// TODO: Should not rely on plugin.
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
// TODO: Should work with custom flags too.
const shortReasons = {
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
@@ -21,7 +17,6 @@ const shortReasons = {
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
class User extends React.Component {
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
@@ -50,12 +45,10 @@ class User extends React.Component {
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
>
<div className={styles.container}>
<div className={styles.header}>
<li tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})} >
<div className={cn('talk-admin-community-flagged-user', styles.container)}>
<div className={cn('talk-admin-community-flagged-user-header', styles.header)}>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
@@ -78,8 +71,7 @@ class User extends React.Component {
}
</div>
</div>
<div className={styles.body}>
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
@@ -127,9 +119,11 @@ class User extends React.Component {
<div className={styles.sideActions}>
<div className={styles.actions}>
<ApproveButton
className="talk-admin-flagged-user-approve-button"
onClick={this.approveUser}
/>
<RejectButton
className="talk-admin-flagged-user-reject-button"
onClick={this.showRejectUsernameDialog}
/>
</div>
@@ -141,4 +135,16 @@ class User extends React.Component {
}
}
User.propTypes = {
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
viewUserDetail: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
user: PropTypes.object,
className: PropTypes.string,
selected: PropTypes.bool,
me: PropTypes.object,
};
export default User;
@@ -1,4 +1,5 @@
import React from 'react';
import cn from 'classnames';
import styles from './styles.css';
import Table from './Table';
import {Icon} from 'coral-ui';
@@ -24,7 +25,7 @@ const People = (props) => {
const hasResults = !!users.length;
return (
<div className={styles.container}>
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Dialog, Button} from 'coral-ui';
import styles from './RejectUsernameDialog.css';
@@ -29,12 +29,6 @@ class RejectUsernameDialog extends Component {
state = {email: '', stage: 0}
static propTypes = {
stage: PropTypes.number,
handleClose: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired
}
componentDidMount() {
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
}
@@ -76,7 +70,7 @@ class RejectUsernameDialog extends Component {
const {stage} = this.state;
return <Dialog
className={styles.suspendDialog}
className={cn(styles.suspendDialog, 'talk-reject-username-dialog')}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
@@ -96,15 +90,18 @@ class RejectUsernameDialog extends Component {
<div className={styles.emailContainer}>
<textarea
rows={5}
className={styles.emailInput}
className={cn(styles.emailInput, 'talk-reject-username-dialog-suspension-message')}
value={this.state.email}
onChange={this.onEmailChange}/>
</div>
</div>
}
<div className={styles.modalButtons}>
<div className={cn(styles.modalButtons, 'talk-reject-username-dialog-buttons')}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button key={i} onClick={this.onActionClick(stage, i)}>
<Button
key={i}
className={cn('talk-reject-username-dialog-button', `talk-reject-username-dialog-button-${key}`)}
onClick={this.onActionClick(stage, i)} >
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
@@ -114,4 +111,12 @@ class RejectUsernameDialog extends Component {
}
}
RejectUsernameDialog.propTypes = {
stage: PropTypes.number,
handleClose: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired,
user: PropTypes.object,
open: PropTypes.bool,
};
export default RejectUsernameDialog;
@@ -42,7 +42,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
</thead>
<tbody>
{users.map((row, i)=> (
<tr key={i}>
<tr key={i} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
@@ -52,6 +52,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-status"
value={row.status}
placeholder={t('community.status')}
onChange={(status) => setCommenterStatus(row.id, status)}>
@@ -61,6 +62,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={row.roles[0] || ''}
placeholder={t('community.role')}
onChange={(role) => setRole(row.id, role)}>
@@ -39,7 +39,7 @@ class ModerationSettings extends React.Component {
};
render() {
const {settings, data, root} = this.props;
const {settings, data, root, updatePending, errors} = this.props;
return (
<ConfigurePage
@@ -74,6 +74,8 @@ class ModerationSettings extends React.Component {
fill="adminModerationSettings"
data={data}
queryData={{root, settings}}
updatePending={updatePending}
errors={errors}
/>
</ConfigurePage>
);
@@ -82,6 +84,7 @@ class ModerationSettings extends React.Component {
ModerationSettings.propTypes = {
updatePending: PropTypes.func.isRequired,
errors: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
@@ -53,3 +53,6 @@
.autoCloseWrapper {
display: flex;
}
@@ -100,7 +100,7 @@ class StreamSettings extends React.Component {
};
render() {
const {settings, data, root, errors} = this.props;
const {settings, data, root, errors, updatePending} = this.props;
return (
<ConfigurePage
@@ -180,22 +180,24 @@ class StreamSettings extends React.Component {
onCheckbox={this.updateAutoClose}
title={t('configure.close_after')}
>
<Textfield
type='number'
pattern='[0-9]+'
style={{width: 50}}
onChange={this.updateClosedTimeout}
value={getTimeoutAmount(settings.closedTimeout)}
label={t('configure.closed_comments_label')} />
<div className={styles.configTimeoutSelect}>
<SelectField
label="comments closed time window"
value={getTimeoutMeasure(settings.closedTimeout)}
onChange={this.updateClosedTimeoutMeasure}>
<Option value={'hours'}>{t('configure.hours')}</Option>
<Option value={'days'}>{t('configure.days')}</Option>
<Option value={'weeks'}>{t('configure.weeks')}</Option>
</SelectField>
<div className={styles.autoCloseWrapper}>
<Textfield
type='number'
pattern='[0-9]+'
style={{width: 50}}
onChange={this.updateClosedTimeout}
value={getTimeoutAmount(settings.closedTimeout)}
label={t('configure.closed_comments_label')} />
<div className={styles.configTimeoutSelect}>
<SelectField
label="comments closed time window"
value={getTimeoutMeasure(settings.closedTimeout)}
onChange={this.updateClosedTimeoutMeasure}>
<Option value={'hours'}>{t('configure.hours')}</Option>
<Option value={'days'}>{t('configure.days')}</Option>
<Option value={'weeks'}>{t('configure.weeks')}</Option>
</SelectField>
</div>
</div>
</ConfigureCard>
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
@@ -203,6 +205,8 @@ class StreamSettings extends React.Component {
fill="adminStreamSettings"
data={data}
queryData={{root, settings}}
updatePending={updatePending}
errors={errors}
/>
</ConfigurePage>
);
@@ -31,7 +31,7 @@ class TechSettings extends React.Component {
};
render() {
const {settings, data, root} = this.props;
const {settings, data, root, errors, updatePending} = this.props;
return (
<ConfigurePage
title={t('configure.tech_settings')}
@@ -51,6 +51,8 @@ class TechSettings extends React.Component {
fill="adminTechSettings"
data={data}
queryData={{root, settings}}
updatePending={updatePending}
errors={errors}
/>
</ConfigurePage>
);
@@ -59,6 +61,7 @@ class TechSettings extends React.Component {
TechSettings.propTypes = {
updatePending: PropTypes.func.isRequired,
errors: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
@@ -2,38 +2,20 @@ import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {withQuery, withMergedSettings} from 'coral-framework/hocs';
import {Spinner} from 'coral-ui';
import {notify} from 'coral-framework/actions/notification';
import PropTypes from 'prop-types';
import assignWith from 'lodash/assignWith';
import {withUpdateSettings} from 'coral-framework/graphql/mutations';
import {getErrorMessages, getDefinitionName} from 'coral-framework/utils';
import StreamSettings from './StreamSettings';
import TechSettings from './TechSettings';
import ModerationSettings from './ModerationSettings';
import {clearPending, setActiveSection} from '../../../actions/configure';
import Configure from '../components/Configure';
// Like lodash merge but does not recurse into arrays.
const mergeExcludingArrays = (objValue, srcValue) => {
if (typeof srcValue === 'object' && !Array.isArray(srcValue)) {
return assignWith({}, objValue, srcValue, mergeExcludingArrays);
}
return srcValue;
};
class ConfigureContainer extends Component {
// Merge current settings with pending settings.
getMergedSettings = (props = this.props) => {
return assignWith({}, props.root.settings, props.pending, mergeExcludingArrays);
}
// Cached merged settings.
mergedSettings = this.getMergedSettings();
savePending = async () => {
try {
await this.props.updateSettings(this.props.pending);
@@ -44,14 +26,6 @@ class ConfigureContainer extends Component {
}
};
componentWillReceiveProps(nextProps) {
// Recalculate merged settings when necessary.
if (this.props.root.settings !== nextProps.root.settings || this.props.pending !== nextProps.pending) {
this.mergedSettings = this.getMergedSettings(nextProps);
}
}
render () {
if(this.props.data.loading) {
return <Spinner/>;
@@ -62,7 +36,7 @@ class ConfigureContainer extends Component {
auth={this.props.auth}
data={this.props.data}
root={this.props.root}
settings={this.mergedSettings}
settings={this.props.mergedSettings}
canSave={this.props.canSave}
savePending={this.savePending}
setActiveSection={this.props.setActiveSection}
@@ -112,6 +86,7 @@ export default compose(
withUpdateSettings,
withConfigureQuery,
connect(mapStateToProps, mapDispatchToProps),
withMergedSettings('root.settings', 'pending', 'mergedSettings'),
)(ConfigureContainer);
ConfigureContainer.propTypes = {
@@ -124,5 +99,6 @@ ConfigureContainer.propTypes = {
root: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
pending: PropTypes.object.isRequired,
mergedSettings: PropTypes.object.isRequired,
activeSection: PropTypes.string.isRequired,
};
@@ -10,6 +10,10 @@ const slots = [
'adminModerationSettings',
];
const mapStateToProps = (state) => ({
errors: state.configure.errors,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
updatePending,
@@ -36,5 +40,5 @@ export default compose(
}
`
}),
connect(null, mapDispatchToProps),
connect(mapStateToProps, mapDispatchToProps),
)(ModerationSettings);
@@ -10,6 +10,10 @@ const slots = [
'adminTechSettings',
];
const mapStateToProps = (state) => ({
errors: state.configure.errors,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
updatePending,
@@ -33,5 +37,5 @@ export default compose(
}
`
}),
connect(null, mapDispatchToProps),
connect(mapStateToProps, mapDispatchToProps),
)(TechSettings);
@@ -1,47 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import styles from './Widget.css';
import t from 'coral-framework/services/i18n';
const ActivityWidget = ({assets}) => {
return (
<div className={styles.widget}>
<h2 className={styles.heading}>{t('dashboard.most_conversations')}</h2>
<div className={styles.widgetHead}>
<p>{t('streams.article')}</p>
<p>{t('dashboard.comment_count')}</p>
</div>
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{asset.commentCount}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
}
</div>
</div>
);
};
ActivityWidget.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
commentCount: PropTypes.number,
author: PropTypes.string,
created_at: PropTypes.string
})).isRequired
};
export default ActivityWidget;
@@ -1,93 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './Dashboard.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const refreshIntervalSeconds = 60 * 5;
// TODO: refactor out storage code into redux.
class CountdownTimer extends React.Component {
static contextTypes = {
storage: PropTypes.object,
};
static propTypes = {
handleTimeout: PropTypes.func.isRequired
}
constructor (props, context) {
super(props, context);
const {storage} = context;
try {
if (storage && storage.getItem('coral:dashboardNote') === null) {
storage.setItem('coral:dashboardNote', 'show');
}
} catch (e) {
// above will fail in Private Mode in some browsers.
}
this.state = {
secondsUntilRefresh: refreshIntervalSeconds,
dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show'
};
}
componentWillMount () {
this.interval = setInterval(() => { // the countdown timer
let nextCount = this.state.secondsUntilRefresh - 1;
if (nextCount < 0) {
nextCount = refreshIntervalSeconds;
return this.props.handleTimeout();
}
this.setState({secondsUntilRefresh: nextCount});
}, 1000);
}
componentWillUnmount () {
window.clearInterval(this.interval);
}
formatTime = () => {
const minutes = Math.floor(this.state.secondsUntilRefresh / 60);
let seconds = (this.state.secondsUntilRefresh % 60).toString();
if (seconds.length < 2) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
}
dismissNote = () => {
const {storage} = this.context;
try {
if (storage) {
storage.setItem('coral:dashboardNote', 'hide');
}
} catch (e) {
// when setItem fails in Safari Private mode
this.setState({dashboardNote: 'hide'});
}
}
render () {
const {storage} = this.context;
const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') ||
this.state.dashboardNote === 'hide'; // for Safari Incognito
return (
<p
style={{display: hideReloadNote ? 'none' : 'block'}}
className={styles.autoUpdate}
onClick={this.dismissNote}>
<b>×</b>
<Icon name='timer' /> <strong>{t('dashboard.next_update', this.formatTime())}</strong> {t('dashboard.auto_update')}
</p>
);
}
}
export default CountdownTimer;
@@ -1,40 +0,0 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
.Dashboard {
display: flex;
max-width: 1280px;
margin: 0 auto;
}
.heading {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
}
.autoUpdate {
background-color: #d5d5d5;
padding: 3px 10px 10px 10px;
margin-bottom: 0;
i {
position: relative;
top: 7px;
}
b {
float: right;
border-radius: 20px;
cursor: pointer;
background-color: #c0c0c0;
width: 30px;
height: 30px;
text-align: center;
top: 4px;
position: relative;
line-height: 1.7em;
font-size: 1.3em;
}
}
@@ -1,15 +0,0 @@
import React from 'react';
import FlagWidget from './FlagWidget';
import ActivityWidget from './ActivityWidget';
import CountdownTimer from './CountdownTimer';
import styles from './Dashboard.css';
export default ({root: {assetsByActivity, assetsByFlag}, reloadData}) => (
<div>
<CountdownTimer handleTimeout={reloadData} />
<div className={styles.Dashboard}>
<FlagWidget assets={assetsByFlag} />
<ActivityWidget assets={assetsByActivity} />
</div>
</div>
);
@@ -1,54 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import styles from './Widget.css';
import t from 'coral-framework/services/i18n';
const FlagWidget = ({assets}) => {
return (
<div className={styles.widget}>
<h2 className={styles.heading}>{t('dashboard.most_flags')}</h2>
<div className={styles.widgetHead}>
<p>{t('streams.article')}</p>
<p>{t('dashboard.flags')}</p>
</div>
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
}
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
}
</div>
</div>
);
};
FlagWidget.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
action_summaries: PropTypes.array,
author: PropTypes.string,
created_at: PropTypes.string
})).isRequired
};
export default FlagWidget;
@@ -1,49 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import styles from './Widget.css';
import t from 'coral-framework/services/i18n';
const LikeWidget = ({assets}) => {
return (
<div className={styles.widget}>
<h2 className={styles.heading}>Articles with the most likes</h2>
<div className={styles.widgetHead}>
<p>{t('streams.article')}</p>
<p>{t('modqueue.likes')}</p>
</div>
<div className={styles.widgetTable}>
{
assets.length
? assets.map((asset) => {
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
})
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
}
</div>
</div>
);
};
LikeWidget.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
action_summaries: PropTypes.array,
author: PropTypes.string,
created_at: PropTypes.string
})).isRequired
};
export default LikeWidget;
@@ -1,38 +0,0 @@
import React from 'react';
import ModerationQueue from 'coral-admin/src/containers/ModerationQueue/ModerationQueue';
import styles from './Widget.css';
import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
import t from 'coral-framework/services/i18n';
const MostLikedCommentsWidget = (props) => {
const {
comments,
moderation,
settings,
handleBanUser,
showBanUserDialog,
hideBanUserDialog,
acceptComment,
rejectComment
} = props;
return (
<div className={styles.widget}>
<h2 className={styles.heading}>{t('most_liked_comments')}</h2>
<ModerationQueue
comments={comments}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={showBanUserDialog}
acceptComment={acceptComment}
rejectComment={rejectComment} />
<BanUserDialog
open={moderation.banDialog}
user={moderation.user}
handleClose={hideBanUserDialog}
handleBanUser={handleBanUser} />
</div>
);
};
export default MostLikedCommentsWidget;
@@ -1,110 +0,0 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
:root {
--row-height: 60px;
}
.widget {
margin: 10px 5px 5px 5px;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
flex: 1;
background-color: white;
box-sizing: border-box;
}
.widget * {
box-sizing: border-box;
}
.heading {
margin: 0;
padding-left: 10px;
font-size: 1.3rem;
font-weight: 600;
color: #2c2c2c;
}
.widgetTable {
height: calc(var(--row-height) * 10);
}
.widgetTable + div:after {
content: '';
clear: both;
display: block;
}
.widgetHead p {
color: #2c2c2c;
font-weight: 500;
padding: 10px;
text-align: left;
text-transform: capitalize;
display: inline-block;
box-sizing: border-box;
margin-bottom: 0;
}
.widgetHead p:last-child {
float: right;
margin-right: 100px;
}
.rowLinkify {
border-bottom: 1px solid lightgrey;
color: #555;
height: var(--row-height);
padding: 10px;
transition: background-color 200ms;
}
.rowLinkify:last-child {
border-bottom: none;
}
.rowLinkify:hover {
background-color: #f8f8f8;
pointer: default;
}
.linkToAsset {
display: inline-block;
text-decoration: none;
}
.linkToModerate {
background-color: #BDBDBD;
padding: 10px 14px;
text-decoration: none;
color: black;
float: right;
margin-left: 15px;
transition: background-color 200ms;
}
.linkToModerate:hover {
background-color: #9E9E9E;
}
.lede {
font-size: 0.9em;
color: #aaa;
}
.assetTitle {
color: #555;
text-decoration: none;
font-size: 1.2em;
font-weight: 500;
margin: 0;
}
.widgetCount {
color: #555;
font-size: 1.3em;
font-weight: 400;
float: right;
margin-top: 7px;
}
@@ -1,65 +0,0 @@
import React from 'react';
import {connect} from 'react-redux';
import Dashboard from '../components/Dashboard';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
class DashboardContainer extends React.Component {
reloadData = () => {
this.props.data.refetch();
}
render () {
if (this.props.data.loading) {
return <Spinner />;
}
return <Dashboard {...this.props} reloadData={this.reloadData} />;
}
}
export const witDashboardQuery = withQuery(gql`
query CoralAdmin_Dashboard($from: Date!, $to: Date!) {
assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) {
...CoralAdmin_Metrics
}
assetsByActivity: assetMetrics(from: $from, to: $to, sortBy: ACTIVITY) {
...CoralAdmin_Metrics
}
}
fragment CoralAdmin_Metrics on Asset {
id
title
url
author
created_at
commentCount
action_summaries {
actionCount
actionableItemCount
}
}
`, {
options: ({windowStart, windowEnd}) => {
return {
variables: {
from: windowStart,
to: windowEnd,
}
};
}
});
const mapStateToProps = (state) => {
return {
windowStart: state.dashboard.windowStart,
windowEnd: state.dashboard.windowEnd,
};
};
export default compose(
connect(mapStateToProps),
witDashboardQuery,
)(DashboardContainer);
@@ -1 +0,0 @@
export {default} from './containers/Dashboard.js';
@@ -73,7 +73,7 @@ class Comment extends React.Component {
return (
<li
tabIndex={0}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected})}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected}, 'talk-admin-moderate-comment')}
id={`comment_${comment.id}`}
>
<div className={styles.container}>
@@ -95,7 +95,7 @@ class Comment extends React.Component {
: null
}
{currentUserId !== comment.user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenu icon="not_interested" className="talk-admin-moderate-comment-actions-menu">
<ActionsMenuItem
disabled={comment.user.status === 'BANNED'}
onClick={this.showSuspendUserDialog}>
@@ -34,6 +34,8 @@ class Moderation extends Component {
key('k', () => this.select(false));
key('f', () => this.moderate(false));
key('d', () => this.moderate(true));
this.getMenuItems()
.forEach((menuItem, idx) => key(`${idx + 1}`, () => this.selectQueue(menuItem)));
}
onClose = () => {
@@ -41,20 +43,23 @@ class Moderation extends Component {
}
nextQueue = () => {
const queueConfig = this.props.queueConfig;
const activeTab = this.props.activeTab;
const assetId = this.props.data.variables.asset_id;
const menuItems = Object.keys(queueConfig).map((queue) => ({
key: queue
}));
const menuItems = this.getMenuItems();
const activeTabIndex = menuItems.findIndex((item) => item.key === activeTab);
const activeTabIndex = menuItems.findIndex((item) => item === activeTab);
const nextQueueIndex = (activeTabIndex === menuItems.length - 1) ? 0 : activeTabIndex + 1;
this.props.router.push(this.props.getModPath(menuItems[nextQueueIndex].key, assetId));
this.selectQueue(menuItems[nextQueueIndex]);
}
selectQueue = (key) => {
const assetId = this.props.data.variables.asset_id;
this.props.router.push(this.props.getModPath(key, assetId));
}
getMenuItems = () => Object.keys(this.props.queueConfig);
closeSearch = () => {
const {toggleStorySearch} = this.props;
toggleStorySearch(false);
@@ -175,6 +180,8 @@ class Moderation extends Component {
key.unbind('k');
key.unbind('f');
key.unbind('d');
this.getMenuItems()
.forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
}
componentWillReceiveProps(nextProps) {
@@ -276,6 +283,7 @@ class Moderation extends Component {
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
open={moderation.modalOpen}
onClose={this.onClose}
queueCount={this.getMenuItems().length}
/>
</div>
<StorySearch
@@ -47,6 +47,9 @@ export default withFragments({
editing {
edited
}
status_history {
type
}
hasParent
${getSlotFragmentSpreads(slots, 'comment')}
...${getDefinitionName(CommentLabels.fragments.comment)}
@@ -71,10 +71,10 @@ class ModerationContainer extends Component {
const id = getAssetId(this.props);
const tab = getTab(this.props);
// Grab premod from asset or from settings
const premod = !id ? settings.moderation : asset.settings.moderation;
// Grab premod from asset or from settings if it's defined.
const setting = id && asset && asset.settings ? asset.settings.moderation : settings.moderation;
const queue = isPremod(premod) ? 'premod' : 'new';
const queue = isPremod(setting) ? 'premod' : 'new';
const activeTab = tab ? tab : queue;
return activeTab;
@@ -111,6 +111,17 @@ class ModerationContainer extends Component {
return this.handleCommentChange(prev, comment, notifyText);
},
},
{
document: COMMENT_RESET_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentReset: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_reset', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
{
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
@@ -127,7 +138,7 @@ class ModerationContainer extends Component {
},
];
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMore(param));
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMoreThrottled(param));
}
unsubscribe() {
@@ -299,6 +310,23 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
${Comment.fragments.comment}
`;
const COMMENT_RESET_SUBSCRIPTION = gql`
subscription CommentReset($asset_id: ID){
commentReset(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
status_history {
type
created_at
assigned_by {
id
username
}
}
}
}
${Comment.fragments.comment}
`;
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
@@ -48,7 +48,7 @@ class Stories extends Component {
<div className={styles.optionHeader}>{t('streams.filter_streams')}</div>
<div className={styles.optionDetail}>{t('streams.stream_status')}</div>
<RadioGroup
name='status filter'
name='statusFilter'
value={filter}
childContainer='div'
onChange={onSettingChange('filter')}
@@ -60,7 +60,7 @@ class Stories extends Component {
</RadioGroup>
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
<RadioGroup
name='sort by'
name='sortBy'
value={asc}
childContainer='div'
onChange={onSettingChange('asc')}
@@ -1,29 +0,0 @@
import React from 'react';
import {Button} from 'coral-ui';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
const CloseCommentsInfo = ({status, onClick}) => (
status === 'open' ? (
<div className="close-comments-intro-wrapper">
<p>
{t('configure.open_stream_configuration')}
</p>
<Button onClick={onClick}>{t('configure.close_stream')}</Button>
</div>
) : (
<div className="close-comments-intro-wrapper">
<p>
{t('configure.close_stream_configuration')}
</p>
<Button onClick={onClick}>{t('configure.open_stream')}</Button>
</div>
)
);
CloseCommentsInfo.propTypes = {
status: PropTypes.string,
onClick: PropTypes.func,
};
export default CloseCommentsInfo;
@@ -1,71 +0,0 @@
import React from 'react';
import {Button, Checkbox} from 'coral-ui';
import QuestionBoxBuilder from './QuestionBoxBuilder';
import cn from 'classnames';
import styles from './ConfigureCommentStream.css';
import t from 'coral-framework/services/i18n';
export default ({handleChange, handleApply, changed, ...props}) => (
<form onSubmit={handleApply}>
<div className={styles.wrapper}>
<div className={styles.container}>
<h3>{t('configure.title')}</h3>
<Button
type="submit"
className={cn(styles.apply, 'talk-embed-stream-configuration-submit-button')}
onChange={handleChange}
cStyle={changed ? 'green' : 'darkGrey'} >
{t('configure.apply')}
</Button>
<p>{t('configure.description')}</p>
</div>
<ul>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premod"
onChange={handleChange}
defaultChecked={props.premod}
info={{
title: t('configure.enable_premod'),
description: t('configure.enable_premod_description')
}} />
</li>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="plinksenable"
onChange={handleChange}
defaultChecked={props.premodLinksEnable}
info={{
title: t('configure.enable_premod_links'),
description: t('configure.enable_premod_links_description')
}} />
</li>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="qboxenable"
onChange={handleChange}
defaultChecked={props.questionBoxEnable}
info={{
title: t('configure.enable_questionbox'),
description: t('configure.enable_questionbox_description')
}} />
{
props.questionBoxEnable && <QuestionBoxBuilder
questionBoxIcon={props.questionBoxIcon}
questionBoxContent={props.questionBoxContent}
handleChange={handleChange}
/>
}
</li>
</ul>
</div>
</form>
);
@@ -1,24 +0,0 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import marked from 'marked';
const renderer = new marked.Renderer();
// Set link target to `_parent` to work properly in an embed.
renderer.link = (href, title, text) =>
`<a target="_parent" href="${href}" ${title ? `title="${title}"` : ''}>${text}</a>`;
marked.setOptions({renderer});
export default class Markdown extends PureComponent {
render() {
const {content, ...rest} = this.props;
const __html = marked(content);
return <div {...rest} dangerouslySetInnerHTML={{__html}} />;
}
}
Markdown.propTypes = {
content: PropTypes.string,
};
@@ -1,45 +0,0 @@
.qbBuilder {
margin-left: 50px;
}
.qbItemIconList {
padding: 0;
margin: 10px 0;
}
.qbItemIcon {
background: #F0F0F0;
width: 45px;
height: 45px;
font-size: 24px;
text-align: center;
line-height: 45px;
color: #252525;
border-radius: 3px;
display: inline-block;
overflow: hidden;
margin-right: 10px;
position: relative;
border: solid 2px #F0F0F0;
transition: border 0.3s cubic-bezier(.4,0,.2,1);
}
.qbItemIcon:hover {
cursor: pointer;
}
.qbItemIconActive {
border: solid 2px #00796B;
}
.defaultIcon {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.qb {
margin: 10px 0;
}
@@ -1,98 +0,0 @@
import React from 'react';
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import {Icon, Spinner} from 'coral-ui';
import DefaultIcon from './DefaultIcon';
import cn from 'classnames';
import styles from './QuestionBoxBuilder.css';
class QuestionBoxBuilder extends React.Component {
constructor() {
super();
this.state = {
loading: true
};
}
componentWillMount() {
this.loadEditor();
}
async loadEditor() {
const {default: MarkdownEditor} = await import('coral-framework/components/MarkdownEditor');
return this.setState({
loading : false,
MarkdownEditor
});
}
render() {
const {handleChange, questionBoxIcon, questionBoxContent} = this.props;
const {loading, MarkdownEditor} = this.state;
if (loading) {
return <Spinner/>;
}
return (
<div className={styles.qbBuilder}>
<h4>Include an Icon</h4>
<ul className={styles.qbItemIconList}>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'default'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="default" >
<DefaultIcon className={styles.defaultIcon} />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'forum'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="forum" >
<Icon name="forum" />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'build'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="build" >
<Icon name="build" />
</li>
<li className={cn(
styles.qbItemIcon,
{[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'}
)}
id="qboxicon"
onClick={handleChange}
data-icon="format_quote" >
<Icon name="format_quote" />
</li>
</ul>
<QuestionBox
className={styles.qb}
enable={true}
icon={questionBoxIcon}
content={questionBoxContent}
/>
<MarkdownEditor
value={questionBoxContent}
onChange={(value) => handleChange({}, {questionBoxContent: value})}
/>
</div>
);
}
}
export default QuestionBoxBuilder;
@@ -1,145 +0,0 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import PropTypes from 'prop-types';
import {updateOpenStatus, updateConfiguration} from 'coral-embed-stream/src/actions/asset';
import CloseCommentsInfo from '../components/CloseCommentsInfo';
import ConfigureCommentStream from '../components/ConfigureCommentStream';
import t, {timeago} from 'coral-framework/services/i18n';
class ConfigureStreamContainer extends Component {
constructor (props) {
super(props);
this.state = {
changed: false,
dirtySettings: {...props.asset.settings},
closedAt: !props.asset.isClosed ? 'open' : 'closed'
};
this.toggleStatus = this.toggleStatus.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleApply = this.handleApply.bind(this);
}
handleApply (e) {
e.preventDefault();
const {elements} = e.target;
const {questionBoxIcon, questionBoxContent} = this.state.dirtySettings;
const premod = elements.premod.checked;
const questionBoxEnable = elements.qboxenable.checked;
const premodLinksEnable = elements.plinksenable.checked;
const {changed} = this.state;
const newConfig = {
moderation: premod ? 'PRE' : 'POST',
questionBoxEnable,
questionBoxContent,
questionBoxIcon,
premodLinksEnable
};
if (changed) {
this.props.updateConfiguration(newConfig);
setTimeout(() => {
this.setState({
changed: false
});
}, 300);
}
}
handleChange (e, newChanges) {
let changes = {};
if (changes) {
changes = {...newChanges};
}
if (e.target && e.target.id === 'qboxenable') {
changes.questionBoxEnable = e.target.checked;
}
if (e.currentTarget && e.currentTarget.id === 'qboxicon') {
changes.questionBoxIcon = e.currentTarget.dataset.icon;
}
if (e.target && e.target.id === 'plinksenable') {
changes.premodLinksEnable = e.target.value;
}
this.setState({
changed: true,
dirtySettings: {
...this.state.dirtySettings,
...changes,
},
});
}
toggleStatus () {
// update the closedAt status for the asset
this.props.updateStatus(
this.state.closedAt === 'open' ? 'closed' : 'open'
);
this.setState({
closedAt: (this.state.closedAt === 'open' ? 'closed' : 'open')
});
}
getClosedIn () {
const {closedTimeout} = this.props.asset.settings;
const {created_at} = this.props.asset;
return timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
}
render () {
const {dirtySettings} = this.state;
const premod = dirtySettings.moderation === 'PRE';
const {closedAt} = this.state;
const closedTimeout = dirtySettings.closedTimeout;
return (
<div className='talk-embed-stream-configuration-container'>
<ConfigureCommentStream
handleChange={this.handleChange}
handleApply={this.handleApply}
changed={this.state.changed}
premodLinksEnable={dirtySettings.premodLinksEnable}
premod={premod}
questionBoxIcon={dirtySettings.questionBoxIcon}
questionBoxEnable={dirtySettings.questionBoxEnable}
questionBoxContent={dirtySettings.questionBoxContent}
/>
<hr />
<h3>{closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}</h3>
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
<CloseCommentsInfo
onClick={this.toggleStatus}
status={closedAt}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
asset: state.asset
});
const mapDispatchToProps = (dispatch) => ({
updateStatus: (status) => dispatch(updateOpenStatus(status)),
updateConfiguration: (newConfig) => dispatch(updateConfiguration(newConfig)),
});
ConfigureStreamContainer.propTypes = {
updateStatus: PropTypes.func,
closedTimeout: PropTypes.string,
created_at: PropTypes.string,
updateConfiguration: PropTypes.func,
asset: PropTypes.object,
};
export default compose(
connect(mapStateToProps, mapDispatchToProps)
)(ConfigureStreamContainer);
@@ -0,0 +1,9 @@
import * as actions from '../constants/configure';
export const updatePending = ({updater, errorUpdater}) => {
return {type: actions.UPDATE_PENDING, updater, errorUpdater};
};
export const clearPending = () => {
return {type: actions.CLEAR_PENDING};
};
@@ -16,12 +16,5 @@
color: #262626;
}
.qbIconContainer {
position: relative;
border: 0;
color: white;
display: inline-block;
padding: 5px 20px;
vertical-align: middle;
width: 10px;
}
.root {
}
@@ -1,13 +1,13 @@
import React from 'react';
import cn from 'classnames';
import styles from './DefaultIcon.css';
import styles from './DefaultQuestionBoxIcon.css';
import {Icon} from 'coral-ui';
const DefaultIcon = ({className}) => (
<div className={cn(styles.qbIconContainer, className)}>
const DefaultQuestionBoxIcon = ({className}) => (
<div className={cn(styles.root, className)}>
<Icon name="chat_bubble" className={cn(styles.iconBubble)} />
<Icon name="person" className={cn(styles.iconPerson)} />
</div>
);
export default DefaultIcon;
export default DefaultQuestionBoxIcon;
@@ -1,16 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import Stream from '../containers/Stream';
import Stream from '../tabs/stream/containers/Stream';
import Configure from '../tabs/configure/containers/Configure';
import Slot from 'coral-framework/components/Slot';
import {can} from 'coral-framework/services/perms';
import t from 'coral-framework/services/i18n';
import AutomaticAssetClosure from '../containers/AutomaticAssetClosure';
import {TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import ExtendableTabPanel from '../containers/ExtendableTabPanel';
import {Tab, 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 {
@@ -21,20 +22,37 @@ export default class Embed extends React.Component {
case 'profile':
this.props.data.refetch();
break;
case 'config':
this.props.data.refetch();
break;
}
this.props.setActiveTab(tab);
};
render() {
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
getTabs() {
const {user} = this.props.auth;
const tabs = [
<Tab key='stream' tabId='stream' className='talk-embed-stream-comments-tab'>
{t('embed_comments_tab')}
</Tab>,
<Tab key='profile' tabId='profile' className='talk-embed-stream-profile-tab'>
{t('framework.my_profile')}
</Tab>,
];
if (can(user, 'UPDATE_CONFIG')) {
tabs.push(
<Tab key='config' tabId='config'>
{t('framework.configure_stream')}
</Tab>
);
}
return tabs;
}
render() {
const {activeTab, commentId, root, root: {asset}, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
const hasHighlightedComment = !!commentId;
return (
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
<AutomaticAssetClosure asset={asset} />
<IfSlotIsNotEmpty slot="login">
<Popup
href={`embed/stream/login?parentUrl=${encodeURIComponent(parentUrl)}`}
@@ -47,44 +65,36 @@ export default class Embed extends React.Component {
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
data={data}
queryData={{root}}
fill="embed"
/>
<TabContent
<ExtendableTabPanel
className='talk-embed-stream-tab-bar'
activeTab={activeTab}
id='talk-embed-stream-tab-content'
>
<TabPane tabId={'stream'} className={'talk-embed-stream-comments-tab-pane'}>
<Stream data={data} root={root} />
</TabPane>
<TabPane tabId={'profile'} className={'talk-embed-stream-profile-tab-pane'}>
<ProfileContainer />
</TabPane>
<TabPane tabId={'config'} className={'talk-embed-stream-configuration-tab-pane'}>
<ConfigureStreamContainer />
</TabPane>
</TabContent>
setActiveTab={this.changeTab}
fallbackTab='stream'
tabSlot='embedStreamTabs'
tabSlotPrepend='embedStreamTabsPrepend'
tabPaneSlot='embedStreamTabPanes'
slotProps={{data}}
queryData={{root}}
tabs={this.getTabs()}
tabPanes={[
<TabPane key='stream' tabId='stream' className='talk-embed-stream-comments-tab-pane'>
<Stream data={data} root={root} asset={root.asset} />
</TabPane>,
<TabPane key='profile' tabId='profile' className='talk-embed-stream-profile-tab-pane'>
<ProfileContainer />
</TabPane>,
<TabPane key='config' tabId='config' className='talk-embed-stream-configuration-tab-pane'>
<Configure data={data} root={root} asset={root.asset} />
</TabPane>,
]}
/>
</div>
);
}
@@ -0,0 +1,35 @@
import React from 'react';
import {Tab} from 'coral-ui';
import PropTypes from 'prop-types';
/**
* ExtendableTab adds a hover property to its children, because
* Tab is rendered as a button and under Firefox its children do
* not support mouse events.
*/
class ExtendableTab extends React.Component {
state = {
hover: false,
}
handleMouseEnter = () => this.setState({hover: true});
handleMouseLeave = () => this.setState({hover: false});
render() {
return (
<Tab
{...this.props}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{React.cloneElement(this.props.children, {hover: this.state.hover})}
</Tab>
);
}
}
ExtendableTab.propTypes = {
children: PropTypes.node,
};
export default ExtendableTab;
@@ -1,9 +1,9 @@
import React from 'react';
import {Spinner, TabBar, TabContent} from 'coral-ui';
import PropTypes from 'prop-types';
import styles from './StreamTabPanel.css';
import styles from './ExtendableTabPanel.css';
class StreamTabPanel extends React.Component {
class ExtendableTabPanel extends React.Component {
render() {
const {activeTab, setActiveTab, tabs, tabPanes, sub, loading, ...rest} = this.props;
@@ -23,7 +23,7 @@ class StreamTabPanel extends React.Component {
}
}
StreamTabPanel.propTypes = {
ExtendableTabPanel.propTypes = {
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
loading: PropTypes.bool,
@@ -39,4 +39,4 @@ StreamTabPanel.propTypes = {
sub: PropTypes.bool,
};
export default StreamTabPanel;
export default ExtendableTabPanel;
@@ -13,6 +13,8 @@
min-height: 50px;
display: flex;
border-radius: 3px;
padding-right: 40px;
box-sizing: border-box;
}
.icon {
@@ -63,8 +65,3 @@
justify-content: center;
font-weight: 400;
}
.hidden {
visibility: hidden;
display: none;
}
@@ -3,16 +3,14 @@ import cn from 'classnames';
import styles from './QuestionBox.css';
import {Icon} from 'coral-ui';
import Markdown from 'coral-framework/components/Markdown';
import DefaultQuestionBoxIcon from './DefaultQuestionBoxIcon';
import Slot from 'coral-framework/components/Slot';
const QuestionBox = ({content, enable, icon = '', className = ''}) => (
<div className={cn(styles.qbInfo, {[styles.hidden]: !enable}, 'questionbox-info', className)}>
const QuestionBox = ({content, icon, className, children}) => (
<div className={cn(styles.qbInfo, 'questionbox-info', className)}>
{
icon === 'default' ? (
<div className={cn(styles.qbIconContainer)}>
<Icon name="chat_bubble" className={cn(styles.iconBubble)} />
<Icon name="person" className={cn(styles.iconPerson)} />
<DefaultQuestionBoxIcon />
</div>
) : (
<div className={cn(styles.qbIconContainer)}>
@@ -23,8 +21,7 @@ const QuestionBox = ({content, enable, icon = '', className = ''}) => (
<div className={cn(styles.qbContent, 'questionbox-content')}>
<Markdown content={content} />
</div>
<Slot fill="streamQuestionArea" />
{children}
</div>
);
@@ -0,0 +1,4 @@
const prefix = 'TALK_EMBED_STREAM_CONFIGURE';
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import {gql} from 'react-apollo';
import {gql, compose} from 'react-apollo';
import {withFragments} from 'coral-framework/hocs';
const FRAGMENT = gql`
fragment CoralEmbedStream_AutomaticAssetClosure_Fragment on Asset {
id
isClosed
}
`;
@@ -26,15 +26,15 @@ class AutomaticAssetClosure extends React.Component {
timer = null;
componentWillMount() {
this.setupTimer(this.props.assetId, this.props.closedAt);
this.setupTimer(this.props.asset.id, this.props.asset.closedAt);
}
componentWillReceiveProps(next) {
if (
this.props.assetId !== next.assetId ||
this.props.closedAt !== next.closedAt
this.props.asset.id !== next.asset.id ||
this.props.asset.closedAt !== next.asset.closedAt
) {
this.setupTimer(next.assetId, next.closedAt);
this.setupTimer(next.asset.id, next.asset.closedAt);
}
}
@@ -43,6 +43,7 @@ class AutomaticAssetClosure extends React.Component {
fragment: FRAGMENT,
id: getFragmentId(assetId),
data: {
__typename: 'Asset',
isClosed: true,
},
});
@@ -74,9 +75,21 @@ class AutomaticAssetClosure extends React.Component {
}
}
AutomaticAssetClosure.PropTypes = {
assetId: PropTypes.string,
closedAt: PropTypes.string,
AutomaticAssetClosure.propTypes = {
asset: PropTypes.object.isRequired,
};
export default AutomaticAssetClosure;
const withAutomaticAssetClosureFragments = withFragments({
asset: gql`
fragment CoralEmbedStream_AutomaticAssetClosure_asset on Asset {
id
closedAt
}
`,
});
const enhance = compose(
withAutomaticAssetClosureFragments,
);
export default enhance(AutomaticAssetClosure);
@@ -13,7 +13,9 @@ import * as assetActions from '../actions/asset';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {withQuery} from 'coral-framework/hocs';
import Embed from '../components/Embed';
import Stream from './Stream';
import Stream from '../tabs/stream/containers/Stream';
import AutomaticAssetClosure from './AutomaticAssetClosure';
import Configure from '../tabs/configure/containers/Configure';
import {notify} from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
import PropTypes from 'prop-types';
@@ -151,6 +153,9 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
const slots = [
'embed',
'embedStreamTabs',
'embedStreamTabsPrepend',
'embedStreamTabPanes',
];
const EMBED_QUERY = gql`
@@ -167,10 +172,20 @@ const EMBED_QUERY = gql`
id
status
}
asset(id: $assetId, url: $assetUrl) {
...${getDefinitionName(Configure.fragments.asset)}
...${getDefinitionName(Stream.fragments.asset)}
...${getDefinitionName(AutomaticAssetClosure.fragments.asset)}
}
${getSlotFragmentSpreads(slots, 'root')}
...${getDefinitionName(Stream.fragments.root)}
...${getDefinitionName(Configure.fragments.root)}
}
${Stream.fragments.root}
${Stream.fragments.asset}
${Configure.fragments.root}
${Configure.fragments.asset}
${AutomaticAssetClosure.fragments.asset}
`;
export const withEmbedQuery = withQuery(EMBED_QUERY, {
@@ -1,23 +1,23 @@
import React from 'react';
import StreamTabPanel from '../components/StreamTabPanel';
import ExtendableTabPanel from '../components/ExtendableTabPanel';
import {connect} from 'react-redux';
import omit from 'lodash/omit';
import {Tab, TabPane} from 'coral-ui';
import {TabPane} from 'coral-ui';
import ExtendableTab from '../components/ExtendableTab';
import {getShallowChanges} from 'coral-framework/utils';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
class StreamTabPanelContainer extends React.Component {
class ExtendableTabPanelContainer extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
componentDidMount() {
this.fallbackAllTab();
this.handleFallback();
}
componentWillReceiveProps(next) {
this.fallbackAllTab(next);
this.handleFallback(next);
}
shouldComponentUpdate(next) {
@@ -35,30 +35,50 @@ class StreamTabPanelContainer extends React.Component {
return changes.length !== 0;
}
fallbackAllTab(props = this.props) {
if (props.activeTab !== props.fallbackTab) {
const slotPlugins = this.getSlotElements(props.tabSlot, props).map((el) => el.type.talkPluginName);
if (slotPlugins.indexOf(props.activeTab) === -1) {
props.setActiveTab(props.fallbackTab);
}
handleFallback(props = this.props) {
if (this.getTabNames(props).indexOf(props.activeTab) === -1) {
props.setActiveTab(props.fallbackTab);
}
}
getTabNames(props = this.props) {
return this.getTabElements(props).map((el) => el.props.tabId);
}
getSlotElements(slot, props = this.props) {
const {plugins} = this.context;
return plugins.getSlotElements(slot, props.reduxState, props.slotProps, props.queryData);
}
getPluginTabElements(props = this.props) {
return this.getSlotElements(props.tabSlot).map((el) => {
return this.getSlotTabElements(props.tabSlot);
}
getPluginTabElementsPrepend(props = this.props) {
return this.getSlotTabElements(props.tabSlotPrepend);
}
getSlotTabElements(slot) {
return this.getSlotElements(slot).map((el) => {
return (
<Tab tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
<ExtendableTab tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
{React.cloneElement(el, {active: this.props.activeTab === el.type.talkPluginName})}
</Tab>
</ExtendableTab>
);
});
}
getTabElements(props = this.props) {
const elements = [...this.getPluginTabElementsPrepend(props)];
if (Array.isArray(props.tabs)) {
elements.push(...props.tabs);
} else {
elements.push(props.tabs);
}
elements.push(...this.getPluginTabElements(props));
return elements;
}
getPluginTabPaneElements(props = this.props) {
return this.getSlotElements(props.tabPaneSlot).map((el) => {
return (
@@ -71,12 +91,12 @@ class StreamTabPanelContainer extends React.Component {
render() {
return (
<StreamTabPanel
<ExtendableTabPanel
className={this.props.className}
activeTab={this.props.activeTab}
setActiveTab={this.props.setActiveTab}
tabs={this.getPluginTabElements().concat(this.props.appendTabs)}
tabPanes={this.getPluginTabPaneElements().concat(this.props.appendTabPanes)}
tabs={this.getTabElements()}
tabPanes={this.getPluginTabPaneElements().concat(this.props.tabPanes)}
loading={this.props.loading}
sub={this.props.sub}
/>
@@ -84,19 +104,20 @@ class StreamTabPanelContainer extends React.Component {
}
}
StreamTabPanelContainer.propTypes = {
ExtendableTabPanelContainer.propTypes = {
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
appendTabs: PropTypes.oneOfType([
tabs: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
]),
appendTabPanes: PropTypes.oneOfType([
tabPanes: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
]),
fallbackTab: PropTypes.string.isRequired,
tabSlot: PropTypes.string.isRequired,
tabSlotPrepend: PropTypes.string.isRequired,
tabPaneSlot: PropTypes.string.isRequired,
slotProps: PropTypes.object.isRequired,
queryData: PropTypes.object,
@@ -106,7 +127,7 @@ StreamTabPanelContainer.propTypes = {
};
const mapStateToProps = (state) => ({
reduxState: omit(state, 'apollo'),
reduxState: state,
});
export default connect(mapStateToProps, null)(StreamTabPanelContainer);
export default connect(mapStateToProps, null)(ExtendableTabPanelContainer);
@@ -2,6 +2,7 @@ import {gql} from 'react-apollo';
import update from 'immutability-helper';
import uuid from 'uuid/v4';
import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils';
import {mapLeaves} from 'coral-framework/utils';
export default {
fragments: {
@@ -102,6 +103,9 @@ export default {
}
}
}
status_history {
type
}
action_summaries {
count
current_user {
@@ -182,6 +186,7 @@ export default {
editableUntil: new Date().toISOString(),
edited: false,
},
status_history: [],
id: `pending-${uuid()}`,
}
}
@@ -214,6 +219,18 @@ export default {
},
},
}),
UpdateAssetSettings: ({variables: {input}}) => ({
updateQueries: {
CoralEmbedStream_Embed: (prev) => {
const updated = update(prev, {
asset: {
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
},
});
return updated;
}
}
}),
},
};
@@ -142,17 +142,21 @@ export function findCommentWithId(nodes, id) {
}
export function findCommentInEmbedQuery(root, callbackOrId) {
return findCommentInAsset(root.asset, callbackOrId);
}
export function findCommentInAsset(asset, callbackOrId) {
let callback = callbackOrId;
if (typeof callbackOrId === 'string') {
callback = (node) => node.id === callbackOrId;
}
if (root.asset.comment) {
return findComment([getTopLevelParent(root.asset.comment)], callback);
if (asset.comment) {
return findComment([getTopLevelParent(asset.comment)], callback);
}
if (!root.asset.comments) {
if (!asset.comments) {
return false;
}
return findComment(root.asset.comments.nodes, callback);
return findComment(asset.comments.nodes, callback);
}
function findAndInsertFetchedComments(parent, comments, parent_id) {
@@ -0,0 +1,42 @@
import * as actions from '../constants/configure';
import isEmpty from 'lodash/isEmpty';
import update from 'immutability-helper';
const initialState = {
canSave: false,
pending: {},
errors: {},
};
export default function config(state = initialState, action) {
switch (action.type) {
case actions.UPDATE_PENDING: {
let next = state;
if (action.updater) {
next = update(next, {
pending: action.updater,
});
}
if (action.errorUpdater) {
next = update(next, {
errors: action.errorUpdater,
});
}
const noErrors = Object.keys(next.errors).reduce((res, error) => res && !next.errors[error], true);
const canSave = !isEmpty(next.pending) && noErrors;
next = update(next, {
canSave: {$set: canSave},
});
return next;
}
case actions.CLEAR_PENDING:
return {
...state,
pending: {},
canSave: false,
};
default:
return state;
}
}
@@ -2,6 +2,7 @@ import auth from './auth';
import asset from './asset';
import embed from './embed';
import config from './config';
import configure from './configure';
import stream from './stream';
import {reducer as commentBox} from '../../../talk-plugin-commentbox';
@@ -11,5 +12,6 @@ export default {
commentBox,
embed,
config,
configure,
stream,
};
@@ -0,0 +1,10 @@
.wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
.button {
width: 300px;
margin-left: 20px;
}
@@ -0,0 +1,72 @@
import React from 'react';
import {Button} from 'coral-ui';
import PropTypes from 'prop-types';
import t, {timeago} from 'coral-framework/services/i18n';
import cn from 'classnames';
import styles from './AssetStatusInfo.css';
class AssetStatusInfo extends React.Component {
timer = null;
constructor(props) {
super(props);
this.setupTimer(props);
}
componentWillReceiveProps(nextProps) {
this.setupTimer(nextProps);
}
// Rerendering interval. If remaining time > 1min, rerender every minute, otherwise evey second.
interval(closedAt) {
const diff = new Date(closedAt).getTime() - new Date().getTime();
return diff > 60000 ? 60000 : 1000;
}
// Timer that counts down the remaining time.
setupTimer({closedAt, isClosed} = this.props) {
if (this.timer && (isClosed || !closedAt)) {
clearTimeout(this.timer);
this.timer = null;
}
if (isClosed || !closedAt) {
this.timer = null;
return;
}
if (!this.timer) {
this.timer = setTimeout(() => {
this.timer = null;
this.forceUpdate();
this.setupTimer();
}, this.interval(closedAt));
}
}
render() {
const {isClosed, closedAt, onClose, onOpen} = this.props;
return (
<div>
<h3>{!isClosed ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}</h3>
{(!isClosed && closedAt) ? <p>{t('configure.comment_stream_will_close')} {timeago(new Date(closedAt))}.</p> : ''}
<div className={cn('close-comments-intro-wrapper', styles.wrapper)}>
<p>
{!isClosed ? t('configure.open_stream_configuration') : t('configure.close_stream_configuration')}
</p>
<Button className={styles.button} onClick={!isClosed ? onClose : onOpen}>
{!isClosed ? t('configure.close_stream') : t('configure.open_stream')}
</Button>
</div>
</div>
);
}
}
AssetStatusInfo.propTypes = {
isClosed: PropTypes.bool.isRequired,
closedAt: PropTypes.string,
onClose: PropTypes.func.isRequired,
onOpen: PropTypes.func.isRequired,
};
export default AssetStatusInfo;
@@ -0,0 +1,30 @@
import React from 'react';
import AssetStatusInfo from '../containers/AssetStatusInfo';
import Settings from '../containers/Settings';
import PropTypes from 'prop-types';
class Configure extends React.Component {
render() {
return (
<div className='talk-embed-stream-configuration-container'>
<Settings
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
/>
<hr />
<AssetStatusInfo
asset={this.props.asset}
/>
</div>
);
}
}
Configure.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
asset: PropTypes.object.isRequired,
};
export default Configure;
@@ -0,0 +1,59 @@
.root {
}
.iconList {
display: flex;
padding: 0;
margin: 10px 0;
}
.item {
list-style-type: none;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: #F0F0F0;
width: 48px;
height: 48px;
font-size: 24px;
text-align: center;
line-height: 48px;
color: #252525;
border-radius: 3px;
overflow: hidden;
margin-right: 10px;
position: relative;
border: solid 2px #F0F0F0;
outline: 0;
&:hover {
cursor: pointer;
}
}
.button:focus {
border: solid 2px #00c96B;
}
.buttonActive {
border: solid 2px #00796B;
}
.defaultIcon {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.questionBox {
margin: 10px 0;
}
@@ -0,0 +1,93 @@
import React from 'react';
import QuestionBox from '../../../components/QuestionBox';
import {Icon, Spinner} from 'coral-ui';
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
import cn from 'classnames';
import styles from './QuestionBoxBuilder.css';
const DefaultIcon = <DefaultQuestionBoxIcon className={styles.defaultIcon} />;
const icons = [
{'default': DefaultIcon},
'forum',
'build',
'format_quote',
];
class QuestionBoxBuilder extends React.Component {
constructor() {
super();
this.state = {
loading: true
};
}
componentWillMount() {
this.loadEditor();
}
async loadEditor() {
const {default: MarkdownEditor} = await import('coral-framework/components/MarkdownEditor');
return this.setState({
loading : false,
MarkdownEditor
});
}
render() {
const {questionBoxIcon, questionBoxContent, onContentChange, onIconChange} = this.props;
const {loading, MarkdownEditor} = this.state;
if (loading) {
return <Spinner/>;
}
return (
<div className={styles.root}>
<h4>Include an Icon</h4>
<ul className={styles.iconList}>
{icons.map((item) => {
const name = typeof item === 'object' ? Object.keys(item)[0] : item;
const icon = typeof item === 'object' ? item[name] : item;
return (
<li
className={styles.item}
key={name}
>
<button
className={cn(
styles.button,
{[styles.buttonActive]: questionBoxIcon === name},
)}
onClick={() => onIconChange(name)}
>
{typeof icon === 'string'
? <Icon name={icon} />
: icon
}
</button>
</li>
);
})}
</ul>
<QuestionBox
className={styles.questionBox}
icon={questionBoxIcon}
content={questionBoxContent}
/>
<MarkdownEditor
value={questionBoxContent}
onChange={onContentChange}
/>
</div>
);
}
}
export default QuestionBoxBuilder;
@@ -7,9 +7,8 @@
margin: 0 10px;
}
.wrapper ul {
list-style: none;
padding: 0;
.description {
max-width: 380px;
}
.checkbox {
@@ -17,19 +16,14 @@
margin: 12px 12px 12px 0;
}
.wrapper h4 {
font-size: 14px;
margin-bottom: 5px;
}
.wrapper p {
max-width: 380px;
.list {
margin-top: 26px;
}
.wrapper {
margin-bottom: 20px;
}
.hidden {
display: none;
}
.questionBoxContainer {
margin-top: 24px;
}
@@ -0,0 +1,102 @@
import React from 'react';
import {Button} from 'coral-ui';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
import styles from './Settings.css';
import Configuration from 'coral-framework/components/StreamConfiguration';
import QuestionBoxBuilder from './QuestionBoxBuilder';
import Slot from 'coral-framework/components/Slot';
class Settings extends React.Component {
render() {
const {
settings: {
moderation,
premodLinksEnable,
questionBoxEnable,
questionBoxContent,
questionBoxIcon,
},
onToggleModeration,
onTogglePremodLinks,
onToggleQuestionBox,
onQuestionBoxIconChange,
onQuestionBoxContentChange,
canSave,
onApply,
slotProps,
queryData,
} = this.props;
return (
<div className={styles.wrapper}>
<div className={styles.container}>
<h3>{t('configure.title')}</h3>
<Button
type="submit"
className={cn(styles.apply, 'talk-embed-stream-configuration-submit-button')}
checked={canSave}
cStyle={canSave ? 'green' : 'darkGrey'}
onClick={onApply}
disabled={!canSave}
>
{t('configure.apply')}
</Button>
<p className={styles.description}>{t('configure.description')}</p>
</div>
<div className={styles.list}>
<Configuration
checked={moderation === 'PRE'}
title={t('configure.enable_premod')}
description={t('configure.enable_premod_description')}
onCheckbox={onToggleModeration}
/>
<Configuration
checked={premodLinksEnable}
title={t('configure.enable_premod_links')}
description={t('configure.enable_premod_description')}
onCheckbox={onTogglePremodLinks}
/>
<Configuration
checked={questionBoxEnable}
title={t('configure.enable_questionbox')}
description={t('configure.enable_questionbox_description')}
onCheckbox={onToggleQuestionBox}
>
{
questionBoxEnable &&
<div className={styles.questionBoxContainer}>
<QuestionBoxBuilder
questionBoxIcon={questionBoxIcon}
questionBoxContent={questionBoxContent}
onIconChange={onQuestionBoxIconChange}
onContentChange={onQuestionBoxContentChange}
/>
</div>
}
</Configuration>
<Slot
fill="streamSettings"
queryData={queryData}
{...slotProps}
/>
</div>
</div>
);
}
}
Settings.propTypes = {
queryData: PropTypes.object.isRequired,
slotProps: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
onToggleModeration: PropTypes.func.isRequired,
onTogglePremodLinks: PropTypes.func.isRequired,
onToggleQuestionBox: PropTypes.func.isRequired,
onQuestionBoxContentChange: PropTypes.func.isRequired,
onQuestionBoxIconChange: PropTypes.func.isRequired,
onApply: PropTypes.func.isRequired,
};
export default Settings;
@@ -0,0 +1,44 @@
import React from 'react';
import {gql, compose} from 'react-apollo';
import {withFragments} from 'coral-framework/hocs';
import AssetStatusInfo from '../components/AssetStatusInfo';
import PropTypes from 'prop-types';
import {withUpdateAssetStatus} from 'coral-framework/graphql/mutations';
class AssetStatusInfoContainer extends React.Component {
openAsset = () => this.props.updateAssetStatus(this.props.asset.id, {closedAt: null});
closeAsset = () => this.props.updateAssetStatus(this.props.asset.id, {closedAt: new Date().toISOString()});
render() {
return <AssetStatusInfo
settings={this.props.asset.settings}
isClosed={this.props.asset.isClosed}
closedAt={this.props.asset.closedAt}
onOpen={this.openAsset}
onClose={this.closeAsset}
/>;
}
}
AssetStatusInfoContainer.propTypes = {
asset: PropTypes.object.isRequired,
updateAssetStatus: PropTypes.func.isRequired,
};
const withAssetStatusInfoFragments = withFragments({
asset: gql`
fragment CoralEmbedStream_AssetStatusInfo_asset on Asset {
id
closedAt
isClosed
}
`,
});
const enhance = compose(
withAssetStatusInfoFragments,
withUpdateAssetStatus,
);
export default enhance(AssetStatusInfoContainer);
@@ -0,0 +1,50 @@
import React from 'react';
import {gql, compose} from 'react-apollo';
import {withFragments} from 'coral-framework/hocs';
import Configure from '../components/Configure';
import AssetStatusInfo from './AssetStatusInfo';
import Settings from './Settings';
import PropTypes from 'prop-types';
import {getDefinitionName} from 'coral-framework/utils';
class ConfigureContainer extends React.Component {
render() {
return <Configure
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
/>;
}
}
ConfigureContainer.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
asset: PropTypes.object.isRequired,
};
const withConfigureFragments = withFragments({
root: gql`
fragment CoralEmbedStream_Configure_root on RootQuery {
__typename
...${getDefinitionName(Settings.fragments.root)}
}
${Settings.fragments.root}
`,
asset: gql`
fragment CoralEmbedStream_Configure_asset on Asset {
__typename
...${getDefinitionName(AssetStatusInfo.fragments.asset)}
...${getDefinitionName(Settings.fragments.asset)}
}
${AssetStatusInfo.fragments.asset}
${Settings.fragments.asset}
`,
});
const enhance = compose(
withConfigureFragments,
);
export default enhance(ConfigureContainer);
@@ -0,0 +1,129 @@
import React from 'react';
import {gql, compose} from 'react-apollo';
import {withFragments, withMergedSettings} from 'coral-framework/hocs';
import {getErrorMessages, getSlotFragmentSpreads} from 'coral-framework/utils';
import Settings from '../components/Settings.js';
import PropTypes from 'prop-types';
import {withUpdateAssetSettings} from 'coral-framework/graphql/mutations';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {notify} from 'coral-framework/actions/notification';
import {clearPending, updatePending} from '../../../actions/configure';
const slots = [
'streamSettings',
];
class SettingsContainer extends React.Component {
toggleModeration = () => {
const updater = {moderation: {$set: this.props.mergedSettings.moderation === 'PRE' ? 'POST' : 'PRE'}};
this.props.updatePending({updater});
};
togglePremodLinks = () => {
const updater = {premodLinksEnable: {$set: !this.props.mergedSettings.premodLinksEnable}};
this.props.updatePending({updater});
};
toggleQuestionBox = () => {
const updater = {questionBoxEnable: {$set: !this.props.mergedSettings.questionBoxEnable}};
this.props.updatePending({updater});
};
setQuestionBoxIcon = (icon) => {
const updater = {questionBoxIcon: {$set: icon}};
this.props.updatePending({updater});
};
setQuestionBoxContent = (content) => {
const updater = {questionBoxContent: {$set: content}};
this.props.updatePending({updater});
};
savePending = async () => {
try {
await this.props.updateAssetSettings(this.props.asset.id, this.props.pending);
this.props.clearPending();
}
catch(err) {
this.props.notify('error', getErrorMessages(err));
}
};
render() {
const {mergedSettings, canSave, data, root, asset, errors, updatePending} = this.props;
return <Settings
settings={mergedSettings}
queryData={{root, asset, settings: mergedSettings}}
slotProps={{data, updatePending, errors}}
savePending={this.savePending}
onToggleModeration={this.toggleModeration}
onTogglePremodLinks={this.togglePremodLinks}
onToggleQuestionBox={this.toggleQuestionBox}
onQuestionBoxIconChange={this.setQuestionBoxIcon}
onQuestionBoxContentChange={this.setQuestionBoxContent}
onApply={this.savePending}
canSave={canSave}
/>;
}
}
SettingsContainer.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
asset: PropTypes.object.isRequired,
pending: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
mergedSettings: PropTypes.object.isRequired,
updateAssetSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
notify: PropTypes.func.isRequired,
updatePending: PropTypes.func.isRequired,
canSave: PropTypes.bool.isRequired,
};
const withSettingsFragments = withFragments({
root: gql`
fragment CoralEmbedStream_Settings_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
asset: gql`
fragment CoralEmbedStream_Settings_asset on Asset {
id
settings {
moderation
premodLinksEnable
questionBoxEnable
questionBoxIcon
questionBoxContent
${getSlotFragmentSpreads(slots, 'settings')}
}
${getSlotFragmentSpreads(slots, 'asset')}
}
`,
});
const mapStateToProps = (state) => ({
pending: state.configure.pending,
canSave: state.configure.canSave,
errors: state.configure.errors,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
notify,
clearPending,
updatePending,
}, dispatch);
const enhance = compose(
withSettingsFragments,
withUpdateAssetSettings,
connect(mapStateToProps, mapDispatchToProps),
withMergedSettings('asset.settings', 'pending', 'mergedSettings'),
);
export default enhance(SettingsContainer);
@@ -14,7 +14,7 @@ const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// 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;
const comments = props.asset.comments;
if (comments && comments.nodes.length) {
const idCursors = [comments.nodes[0].id];
if (comments.nodes[1]) {
@@ -30,7 +30,7 @@ function resetCursors(state, props) {
// 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 comments = props.asset.comments;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
@@ -119,6 +119,8 @@
.commentInfoBar {
margin-left: auto;
flex: 1;
text-align: right;
}
@keyframes enter {
@@ -131,9 +133,6 @@
animation: enter 1000ms;
}
.commentContainer {
}
.commentAvatar {
max-width: 60px;
}
@@ -152,12 +151,36 @@
}
.header {
display: flex;
align-items: center;
margin: 10px 0;
display: flex;
}
.headerContainer {
flex: 1;
flex-direction: column;
align-items: flex-start;
@media (min-width: 480px) {
display: flex;
align-items: center;
flex-direction: row;
}
}
.tagsContainer {
display: flex;
}
.tagsContainer > * {
margin: 3px;
&:first-child {
margin-left: 0px;
}
}
.content {
word-wrap: break-word;
}
.footer {
@@ -9,13 +9,13 @@ 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 {THREADING_LEVEL} from '../../../constants/stream';
import merge from 'lodash/merge';
import mapValues from 'lodash/mapValues';
import LoadMore from './LoadMore';
import {getEditableUntilDate} from './util';
import {findCommentWithId} from '../graphql/utils';
import {findCommentWithId} from '../../../graphql/utils';
import CommentContent from 'coral-framework/components/CommentContent';
import Slot from 'coral-framework/components/Slot';
import CommentTombstone from './CommentTombstone';
@@ -182,6 +182,7 @@ export default class Comment extends React.Component {
editableUntil: PropTypes.string,
})
}).isRequired,
setCommentStatus: PropTypes.func.isRequired,
// edit a comment, passed (id, asset_id, { body })
editComment: PropTypes.func,
@@ -343,7 +344,12 @@ export default class Comment extends React.Component {
} = this.props;
if (!highlighted && this.commentIsRejected(comment)) {
return <CommentTombstone action='reject' />;
return <CommentTombstone action='reject' onUndo={() => {
this.props.setCommentStatus({
commentId: comment.id,
status: comment.status_history[comment.status_history.length - 2].type,
});
}}/>;
}
if (this.commentIsIgnored(comment)) {
@@ -446,39 +452,43 @@ export default class Comment extends React.Component {
<div className={cn(styles.commentContainer, 'talk-stream-comment-container')}>
<div className={cn(styles.header, 'talk-stream-comment-header')}>
<Slot
className={cn(styles.username, 'talk-stream-comment-user-name')}
fill="commentAuthorName"
defaultComponent={CommentAuthorName}
queryData={queryData}
{...slotProps}
/>
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
<Slot
className={cn('talk-stream-comment-author-tags')}
fill="commentAuthorTags"
queryData={queryData}
{...slotProps}
inline
/>
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<div className={cn(styles.headerContainer, 'talk-stream-comment-header-container')}>
<Slot
fill="commentTimestamp"
defaultComponent={CommentTimestamp}
className={'talk-stream-comment-published-date'}
created_at={comment.created_at}
className={cn(styles.username, 'talk-stream-comment-user-name')}
fill="commentAuthorName"
defaultComponent={CommentAuthorName}
queryData={queryData}
{...slotProps}
/>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
</span>
<div className={cn(styles.tagsContainer, 'talk-stream-comment-header-tags-container')}>
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
<Slot
className={cn(styles.commentAuthorTagsSlot, 'talk-stream-comment-author-tags')}
fill="commentAuthorTags"
queryData={queryData}
{...slotProps}
inline
/>
</div>
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<Slot
fill="commentTimestamp"
defaultComponent={CommentTimestamp}
className={'talk-stream-comment-published-date'}
created_at={comment.created_at}
queryData={queryData}
{...slotProps}
/>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
</span>
</div>
<Slot
className={styles.commentInfoBar}
@@ -3,4 +3,10 @@
text-align: center;
padding: 1em;
color: #3E4F71;
}
.undo {
cursor: pointer;
text-decoration: underline;
margin-left: 5px;
}
@@ -24,6 +24,9 @@ class CommentTombstone extends React.Component {
<hr aria-hidden={true} />
<p className={styles.commentTombstone}>
{this.getCopy()}
{this.props.action === 'reject' &&
<span className={styles.undo} onClick={this.props.onUndo}>{t('comment.undo_reject')}</span>
}
</p>
</div>
);
@@ -32,6 +35,7 @@ class CommentTombstone extends React.Component {
CommentTombstone.propTypes = {
action: PropTypes.string,
onUndo: PropTypes.func,
};
export default CommentTombstone;

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