Compare commits

..
306 Commits
Author SHA1 Message Date
Kiwi 519464ce57 Merge pull request #1226 from coralproject/handle-empty-queue
Fix error when queue is empty
2017-12-19 20:58:34 +01:00
Chi Vinh Le f27de815e6 Minor syntax issue 2017-12-19 20:48:59 +01:00
Chi Vinh Le e8553c5035 Revert debug code :-D 2017-12-19 20:46:59 +01:00
Chi Vinh Le 349341400d Fix error when queue is empty 2017-12-19 20:39:41 +01:00
Kim Gardner 7b92e58ff3 Merge pull request #1225 from coralproject/portal-dialog
Use Portal to put dialogs at the end of the document
2017-12-19 13:48:30 -05:00
Chi Vinh Le f2f7742709 Adapt e2e 2017-12-19 19:40:16 +01:00
Chi Vinh Le 689fb4baf0 Remove z-index 2017-12-19 19:26:22 +01:00
Chi Vinh Le 650f98a080 Merge branch 'portal-dialog' of ssh://github.com/coralproject/talk into portal-dialog 2017-12-19 19:25:35 +01:00
Chi Vinh Le 88d3a53165 Make backdrop work 2017-12-19 19:24:39 +01:00
Kiwi 6e88976294 Merge branch 'master' into portal-dialog 2017-12-19 19:17:50 +01:00
Chi Vinh Le f25cc56ddb Use Portal to put dialogs to the end of the document 2017-12-19 19:15:45 +01:00
Kiwi 4134b2edb2 Merge pull request #1222 from coralproject/app-plugin
Added support for app plugin hook
2017-12-19 19:08:27 +01:00
Kiwi 5b886b6eea Merge branch 'master' into app-plugin 2017-12-19 18:57:31 +01:00
Kiwi 6c80de2981 Merge pull request #1224 from coralproject/e2e-stability
Increase e2e stability
2017-12-19 18:57:17 +01:00
Kiwi b9b3355317 Merge branch 'master' into e2e-stability 2017-12-19 18:49:35 +01:00
Chi Vinh Le 88e9faea00 Increase e2e stability 2017-12-19 18:48:44 +01:00
Kiwi b1c9fbedd8 Merge branch 'master' into app-plugin 2017-12-19 18:37:01 +01:00
Kiwi c9b7eb3494 Merge pull request #1223 from coralproject/fix-dialog
Fix dialog not rendering fully in firefox
2017-12-19 18:34:49 +01:00
Chi Vinh Le 66287f5b7d Fix dialog not rendering fully in firefox 2017-12-19 18:27:19 +01:00
Kiwi b61eeeba74 Merge branch 'master' into app-plugin 2017-12-19 18:02:43 +01:00
Kim Gardner 2179632c94 Merge pull request #1221 from coralproject/e2e-fixes
E2e improvements and fixes
2017-12-19 11:42:10 -05:00
Wyatt Johnson 4342161a70 Merge branch 'master' into app-plugin 2017-12-19 09:31:13 -07:00
Kim Gardner a48688ad35 Merge branch 'master' into e2e-fixes 2017-12-19 11:18:28 -05:00
Kim Gardner 1b4f783abd Merge pull request #1208 from coralproject/virtualized
Mod Queue improvements
2017-12-19 11:18:09 -05:00
Wyatt Johnson e2d8562868 added support for app plugin hook 2017-12-19 09:13:01 -07:00
Kim Gardner 91602b9d34 Merge branch 'master' into virtualized 2017-12-19 10:33:22 -05:00
Kim Gardner eba530eda0 Merge branch 'master' into e2e-fixes 2017-12-19 10:33:18 -05:00
Kim Gardner 8e1cfc3a29 Merge pull request #1219 from coralproject/bump-version-3-9-0
Version 3.9.0
2017-12-19 10:33:05 -05:00
Chi Vinh Le c78bb598e7 Merge branch 'e2e-fixes' of ssh://github.com/coralproject/talk into e2e-fixes 2017-12-19 16:27:20 +01:00
Chi Vinh Le f12abbf39e Default timeout to 10000 for ci 2017-12-19 16:26:55 +01:00
Kiwi 7dcb1dfbaa Merge branch 'master' into e2e-fixes 2017-12-19 16:16:59 +01:00
Chi Vinh Le 1fce2451c7 Remove some blank lines 2017-12-19 16:16:59 +01:00
Chi Vinh Le e4f1f5d08b Allow disabling e2e in ci 2017-12-19 16:14:59 +01:00
Chi Vinh Le d06ff8c93c Double default WaitForTimeout 2017-12-19 15:56:54 +01:00
Kim Gardner 9d9056ef86 Merge branch 'master' into bump-version-3-9-0 2017-12-19 08:28:01 -06:00
Kim Gardner 1b61ca8abf Merge branch 'master' into virtualized 2017-12-19 08:27:58 -06:00
Kim Gardner f75a743090 Merge pull request #1220 from coralproject/schema-level-plugin-support
Schema + Connector Plugin Support
2017-12-19 08:27:32 -06:00
Chi Vinh Le 82e617b19b Enable firefox and edge 2017-12-19 14:48:58 +01:00
Chi Vinh Le 7a4b87f743 Fix popup issues 2017-12-19 14:42:54 +01:00
Chi Vinh Le 71f0887a82 Make browsers configurable through ENV E2E_BROWSERS 2017-12-19 14:42:29 +01:00
Chi Vinh Le 4116e206a3 Configurable timeout 2017-12-19 14:41:51 +01:00
Chi Vinh Le e78f17460f Fix wrong spec 2017-12-19 14:27:34 +01:00
Kim Gardner 70bfd91359 Merge branch 'master' into virtualized 2017-12-18 17:37:04 -06:00
Kim Gardner b3432a5e0d Merge branch 'master' into bump-version-3-9-0 2017-12-18 17:36:56 -06:00
Wyatt Johnson 382cb42dcf Merge branch 'master' into schema-level-plugin-support 2017-12-18 16:27:38 -07:00
Wyatt Johnson dbd106d523 added support for plugging into schema level resolve 2017-12-18 16:24:28 -07:00
Kim Gardner e8f94d4d4a Merge pull request #1209 from coralproject/i18n-serverside-plugins
i18n + Email Support
2017-12-18 17:03:47 -06:00
Kim Gardner 3ea141d4b0 Merge branch 'master' into i18n-serverside-plugins 2017-12-18 16:44:23 -06:00
Kim Gardner f783c8423d Merge pull request #1211 from coralproject/premod-admin-comments
Fix new admin comments not showing up in premod
2017-12-18 16:44:15 -06:00
Kim Gardner 9d4a9861f2 Merge branch 'master' into premod-admin-comments 2017-12-18 16:34:13 -06:00
Kim Gardner 6d69286df6 Merge pull request #1214 from coralproject/confirmation-featured
Confirmation Dialog for Featuring Comments on the Admin Side
2017-12-18 16:34:01 -06:00
Kim Gardner 3121ffb55a Version 3.9.0 2017-12-18 17:31:25 -05:00
Kim Gardner c0af51fbc0 Merge branch 'master' into confirmation-featured 2017-12-18 16:27:00 -06:00
Kim Gardner ad85b0fcc8 Merge pull request #1216 from coralproject/change-permalink-share
Update permalink copy to "Share" on stream
2017-12-18 16:26:43 -06:00
Kim Gardner 5ad73b1c14 Merge branch 'master' into change-permalink-share 2017-12-18 16:17:31 -06:00
Kim Gardner bf83f462e0 Merge pull request #1217 from coralproject/fix-login-profile
Fix sign in on profile tab
2017-12-18 16:17:19 -06:00
Kim Gardner 8dd79a19c3 Merge branch 'master' into fix-login-profile 2017-12-18 16:09:37 -06:00
Kim Gardner a8368eb78d Merge pull request #1218 from coralproject/ci-fix
Disabling Firefox/Edge on E2E until fix pass
2017-12-18 13:58:02 -06:00
Wyatt Johnson e88bac6e0f Merge branch 'master' into ci-fix 2017-12-18 12:45:40 -07:00
Kim Gardner 281d1f65f1 Merge branch 'master' into i18n-serverside-plugins 2017-12-18 13:44:20 -06:00
Kim Gardner 2704d71b09 Merge branch 'master' into fix-login-profile 2017-12-18 13:43:45 -06:00
Wyatt Johnson 583ab0ff74 disabling firefox,edge 2017-12-18 12:42:58 -07:00
Kim Gardner ad5c0444f7 Merge branch 'master' into virtualized 2017-12-18 12:43:16 -06:00
Belén Curcio 2a74e75506 Merge branch 'master' into confirmation-featured 2017-12-18 15:39:55 -03:00
Wyatt Johnson fb7ad76b8b Merge pull request #1215 from coralproject/ci-fix
*Fixes for sorted window handler
2017-12-18 11:36:47 -07:00
Kiwi eddb471366 Merge branch 'master' into virtualized 2017-12-18 19:29:35 +01:00
okbel 35c88ffab4 spreading the rest first 2017-12-18 15:21:02 -03:00
okbel 1cdef8f864 bounded 2017-12-18 15:20:06 -03:00
Kim Gardner 82fd56dc67 Update permalink copy to Share on stream 2017-12-18 13:09:39 -05:00
Chi Vinh Le 0e01d40aa7 Fix sign in on profile tab 2017-12-18 19:01:43 +01:00
Wyatt Johnson f7d1543aa3 fixes for sorted window handler 2017-12-18 10:24:11 -07:00
Belén Curcio d2ebc81e25 Merge branch 'master' into confirmation-featured 2017-12-18 13:52:01 -03:00
Belen Curcio 278e1cec1d done 2017-12-18 13:42:59 -03:00
Kim Gardner 31436e627d Merge branch 'master' into i18n-serverside-plugins 2017-12-18 10:39:27 -06:00
Kim Gardner 4e50865927 Merge branch 'master' into premod-admin-comments 2017-12-18 10:39:06 -06:00
Belen Curcio 6ad218782e styling 2017-12-18 13:39:02 -03:00
Belen Curcio 8f1f875d76 less data to the store 2017-12-18 13:36:30 -03:00
Belen Curcio 9fb8e01cd2 working, still wip 2017-12-18 13:34:48 -03:00
Kim Gardner 950a35310b Merge pull request #1212 from coralproject/user-detail-load-more-fix
Fix load more in UserDetail issue
2017-12-18 10:23:50 -06:00
Kiwi 12884cce01 Merge branch 'master' into i18n-serverside-plugins 2017-12-18 17:12:54 +01:00
Kiwi f8d153cf95 Merge branch 'master' into virtualized 2017-12-18 17:12:49 +01:00
Kiwi c871f1c797 Merge branch 'master' into premod-admin-comments 2017-12-18 17:12:41 +01:00
Kiwi 9af7ae0357 Merge branch 'master' into user-detail-load-more-fix 2017-12-18 17:12:36 +01:00
Wyatt Johnson 073eea2847 Merge pull request #1213 from coralproject/version-bump
Version Bump
2017-12-18 09:11:52 -07:00
Kiwi 9cb6ff3847 Merge branch 'master' into virtualized 2017-12-18 16:59:31 +01:00
Wyatt Johnson c699e044fd Update package.json 2017-12-18 08:55:49 -07:00
Kiwi 7d3ec7da1e Merge branch 'master' into premod-admin-comments 2017-12-18 16:55:38 +01:00
Belen Curcio 5d288da69d Merge branch 'master' of github.com:coralproject/talk into confirmation-featured 2017-12-18 12:55:31 -03:00
Kiwi 4ef7abc132 Merge branch 'master' into user-detail-load-more-fix 2017-12-18 16:55:31 +01:00
Wyatt Johnson 9e486f3a9d Merge pull request #1210 from coralproject/redis-optim
Redis Optimization Pass
2017-12-18 08:55:18 -07:00
Chi Vinh Le fbcd390220 Fix load more issue 2017-12-18 16:52:57 +01:00
okbel 338c23dc3d unnecesary class 2017-12-18 12:46:24 -03:00
okbel 4d6f38a3d9 wip 2017-12-18 12:43:46 -03:00
Chi Vinh Le 5b8f335b2a Fix new admin comments not showing up in premod 2017-12-18 16:41:05 +01:00
Wyatt Johnson f0330ed21c fixed version paths 2017-12-18 08:35:17 -07:00
okbel 02da749c65 Adding containers and components 2017-12-18 12:20:20 -03:00
okbel 5a1393c039 Adding translations 2017-12-18 12:20:06 -03:00
okbel d7a5e613fa Adding reducer 2017-12-18 12:19:46 -03:00
Wyatt Johnson 75a944eba8 upgrades, fixes to comment count logic 2017-12-18 08:02:43 -07:00
Chi Vinh Le 9a0fb95d06 Pagination fixes 2017-12-18 15:03:54 +01:00
Wyatt Johnson 6850dd1a53 redis optim
- removes call for comment count when we already have
  totalCommentCount
- removes job processor cleaner on non-processing nodes
- fixed some template issues
2017-12-15 15:49:56 -06:00
Wyatt Johnson ecb01224fd Merge branch 'master' into i18n-serverside-plugins 2017-12-15 10:40:08 -06:00
Wyatt Johnson fdd5cbb0d7 moved email contents to translation files 2017-12-14 16:56:24 -06:00
Wyatt Johnson eb9827a3ae more fixes for i18n 2017-12-14 16:48:03 -06:00
Wyatt Johnson d207b240f2 added support for changing the subject prefi 2017-12-14 16:28:08 -06:00
Wyatt Johnson d9654b52cf some fixes 2017-12-14 16:09:41 -06:00
Wyatt Johnson d3965924aa fixed template 2017-12-14 15:51:53 -06:00
Wyatt Johnson 36d535546c plugins serverside can now provide translations 2017-12-14 15:03:35 -06:00
Chi Vinh Le 19568afde5 Reduce view more movements 2017-12-14 21:08:13 +01:00
Chi Vinh Le 2b312dbf8e Next try 2017-12-14 20:41:05 +01:00
Chi Vinh Le 5446abe3a9 Fix bug with featured comments 2017-12-14 20:37:51 +01:00
Chi Vinh Le c66c100dcb Cache callbacks to preserve immutability 2017-12-14 20:27:19 +01:00
Chi Vinh Le d4051f6828 Fix view more styling 2017-12-14 20:08:09 +01:00
Chi Vinh Le 33781c3d16 Fix view new comments 2017-12-14 20:01:33 +01:00
Chi Vinh Le defc4049cb Sort correctly + refactors and comments 2017-12-14 19:47:42 +01:00
Chi Vinh Le 243d0c1493 Limit to 100 comments 2017-12-14 18:33:49 +01:00
Kiwi a448c69a2a Merge branch 'master' into virtualized 2017-12-14 18:32:28 +01:00
Chi Vinh Le 0b774402cf Introduce dangling comments and queue cleanup 2017-12-14 18:19:00 +01:00
Chi Vinh Le 0bda44e97f Fix cursor bug 2017-12-14 16:51:50 +01:00
Chi Vinh Le 60f6c63c8a Restrict notifications to comments which are rendered 2017-12-14 16:29:31 +01:00
Chi Vinh Le 62c2d551f8 Append comments only 2017-12-14 15:57:44 +01:00
Chi Vinh Le f32bb5ce8f Small adjustments 2017-12-14 13:55:08 +01:00
Chi Vinh Le 5e6d22f1af Handle removing comments from queue 2017-12-14 13:38:01 +01:00
Chi Vinh Le 34289e3340 Refactor view 2017-12-14 12:35:26 +01:00
Wyatt Johnson 98ef60a48e Merge pull request #1205 from stephendonner/patch-1
s/setup/setup
2017-12-13 12:34:30 -06:00
Wyatt Johnson 8323344d7d Merge branch 'master' into patch-1 2017-12-13 12:26:24 -06:00
Wyatt Johnson fbecc363cf Merge pull request #1206 from coralproject/feature/infer-asset-url
Fix logic to generate asset_url even if the asset_id is provided
2017-12-13 12:25:27 -06:00
Chi Vinh Le 2779734ec0 Workaround 2017-12-13 15:25:47 +01:00
Chi Vinh Le 5548deb6b5 Auto Load More and other bug fixes 2017-12-13 15:06:01 +01:00
Jeff Nelson df925f5a1a Fix logic to generate asset_url even if the asset_id is provided so
an error is avoided
2017-12-13 07:38:10 -06:00
Stephen Donner 30073b88f1 s/setup/setup 2017-12-13 03:49:35 -06:00
Chi Vinh Le 51bd109f13 Fix some bugs 2017-12-12 17:28:38 +01:00
Chi Vinh Le b12d5f6bd7 Reimplement keyboard navigation 2017-12-12 17:19:11 +01:00
Chi Vinh Le f32e4523e8 Make view more coments work with virtualized 2017-12-12 12:53:54 +01:00
Chi Vinh Le ed959ca07a Integrate react-virtualized 2017-12-11 20:21:27 +01:00
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
Chi Vinh Le f7c46b1a42 Move asset fragment up one level 2017-11-16 16:19:36 +01: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
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
Chi Vinh Le e02537df13 Fix mutation syntax 2017-11-15 17:53:24 +01:00
Chi Vinh Le e4a359e0b4 Add mutation to framework 2017-11-15 13:38:02 +01:00
209 changed files with 3873 additions and 2964 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"
]
}
}
+21 -12
View File
@@ -1,19 +1,37 @@
const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const uuid = require('uuid');
const merge = require('lodash/merge');
const helmet = require('helmet');
const plugins = require('./services/plugins');
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');
const app = express();
// Request Identity Middleware
app.use((req, res, next) => {
req.id = uuid.v4();
next();
});
//==============================================================================
// PLUGIN PRE APPLICATION MIDDLEWARE
//==============================================================================
// Inject server route plugins.
plugins.get('server', 'app').forEach(({plugin, app: callback}) => {
debug(`added plugin '${plugin.name}'`);
// Pass the app to the plugin to mount it's routes.
callback(app);
});
//==============================================================================
// APPLICATION WIDE MIDDLEWARE
//==============================================================================
@@ -36,12 +54,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 +65,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);
}
}
/**
-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 */}
@@ -34,3 +34,8 @@ export const storySearchChange = (value) => ({
export const clearState = () => ({
type: actions.MODERATION_CLEAR_STATE
});
export const selectCommentId = (id) => ({
type: actions.MODERATION_SELECT_COMMENT,
id,
});
@@ -21,10 +21,11 @@ class CommentDetails extends Component {
this.setState((state) => ({
showDetail: !state.showDetail
}));
this.props.clearHeightCache && this.props.clearHeightCache();
}
render() {
const {data, root, comment} = this.props;
const {data, root, comment, clearHeightCache} = this.props;
const {showDetail} = this.state;
const queryData = {
root,
@@ -44,12 +45,14 @@ class CommentDetails extends Component {
<Slot
fill="adminCommentDetailArea"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>}
</div>
@@ -61,6 +64,7 @@ CommentDetails.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
clearHeightCache: PropTypes.func,
};
export default CommentDetails;
+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
+3 -10
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
@@ -87,7 +80,7 @@ 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>
@@ -6,3 +6,4 @@ export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
export const MODERATION_SELECT_COMMENT = 'MODERATION_SELECT_COMMENT';
+1 -1
View File
@@ -19,6 +19,6 @@ export default withQuery(gql`
}
`, {
options: {
pollInterval: 5000
pollInterval: 10000
}
})(Header);
@@ -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)}
}
@@ -128,7 +128,7 @@ class UserDetailContainer extends React.Component {
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
...CoralAdmin_Moderation_CommentConnection
...CoralAdmin_UserDetail_CommentConnection
}
}
${commentConnectionFragment}
@@ -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,
@@ -7,6 +7,7 @@ const initialState = {
storySearchString: '',
shortcutsNoteVisible: 'show',
sortOrder: 'DESC',
selectedCommentId: '',
};
export default function moderation (state = initialState, action) {
@@ -51,6 +52,11 @@ export default function moderation (state = initialState, action) {
...state,
sortOrder: action.order,
};
case actions.MODERATION_SELECT_COMMENT:
return {
...state,
selectedCommentId: action.id,
};
default:
return state;
}
@@ -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';
@@ -0,0 +1,25 @@
import React from 'react';
import {Spinner} from 'coral-ui';
import PropTypes from 'prop-types';
/**
* AutoLoadMore with call `loadMore` the moment it is rendered and shows a Spinner.
*/
class AutoLoadMore extends React.Component {
componentDidMount() {
if(!this.props.loading) {
this.props.loadMore();
}
}
render() {
return <Spinner />;
}
}
AutoLoadMore.propTypes = {
loading: PropTypes.bool.isRequired,
loadMore: PropTypes.func.isRequired,
};
export default AutoLoadMore;
@@ -10,7 +10,9 @@
position: relative;
transition: all 200ms;
padding: 10px 0;
margin-top: 13px;
min-height: 0;
outline: 0;
/*
Fix rendering issues in Safari by promoting this
@@ -25,6 +27,10 @@
}
}
.dangling {
background-color: #efefef;
}
.container {
padding: 0 14px;
}
@@ -20,6 +20,16 @@ import t, {timeago} from 'coral-framework/services/i18n';
class Comment extends React.Component {
ref = null;
handleRef = (ref) => this.ref = ref;
handleFocusOrClick = () => {
if (!this.props.selected) {
this.props.selectComment();
}
};
showSuspendUserDialog = () => {
const {comment, showSuspendUserDialog} = this.props;
return showSuspendUserDialog({
@@ -55,6 +65,12 @@ class Comment extends React.Component {
: this.props.rejectComment({commentId: this.props.comment.id})
);
componentDidUpdate(prev) {
if (!prev.selected && this.props.selected) {
this.ref.focus();
}
}
render() {
const {
comment,
@@ -65,6 +81,8 @@ class Comment extends React.Component {
root: {settings},
currentUserId,
currentAsset,
clearHeightCache,
dangling,
} = this.props;
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
@@ -73,8 +91,11 @@ class Comment extends React.Component {
return (
<li
tabIndex={0}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected}, 'talk-admin-moderate-comment')}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected, [styles.dangling]: dangling}, 'talk-admin-moderate-comment')}
id={`comment_${comment.id}`}
onClick={this.handleFocusOrClick}
ref={this.handleRef}
onFocus={this.handleFocusOrClick}
>
<div className={styles.container}>
<div className={styles.itemHeader}>
@@ -114,6 +135,7 @@ class Comment extends React.Component {
<Slot
fill="adminCommentInfoBar"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
</div>
@@ -145,6 +167,7 @@ class Comment extends React.Component {
<Slot
fill="adminCommentContent"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
<div className={styles.sideActions}>
@@ -166,6 +189,7 @@ class Comment extends React.Component {
<Slot
fill="adminSideActions"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
</div>
@@ -176,6 +200,7 @@ class Comment extends React.Component {
data={data}
root={root}
comment={comment}
clearHeightCache={clearHeightCache}
/>
</li>
);
@@ -185,12 +210,16 @@ class Comment extends React.Component {
Comment.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
selectComment: PropTypes.func,
rejectComment: PropTypes.func.isRequired,
onClick: PropTypes.func,
className: PropTypes.string,
dangling: PropTypes.bool,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
clearHeightCache: PropTypes.func,
comment: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
@@ -12,15 +12,10 @@ import Slot from 'coral-framework/components/Slot';
import ViewOptions from './ViewOptions';
class Moderation extends Component {
constructor(props) {
super(props);
const comments = this.getComments(props);
this.state = {
selectedCommentId: comments[0] ? comments[0].id : null,
};
}
state = {
isLoadingMore: false,
};
componentWillMount() {
const {toggleModal, singleView} = this.props;
@@ -30,8 +25,6 @@ class Moderation extends Component {
key('esc', () => toggleModal(false));
key('ctrl+f', () => this.openSearch());
key('t', () => this.nextQueue());
key('j', () => this.select(true));
key('k', () => this.select(false));
key('f', () => this.moderate(false));
key('d', () => this.moderate(true));
this.getMenuItems()
@@ -74,8 +67,7 @@ class Moderation extends Component {
}
moderate = (accept) => {
const {acceptComment, rejectComment} = this.props;
const {selectedCommentId} = this.state;
const {acceptComment, rejectComment, moderation: {selectedCommentId}} = this.props;
// Accept or reject only if there's a selected comment
if(selectedCommentId != null){
@@ -96,74 +88,16 @@ class Moderation extends Component {
return root[activeTab].nodes;
}
scrollTo = (toId, smooth = true) =>
document.querySelector(`#comment_${toId}`).scrollIntoView(smooth ? {behavior: 'smooth'} : {});
select = async (next, props = this.props, selectedCommentId = this.state.selectedCommentId) => {
const comments = this.getComments(props);
// No comments to be selected.
if (comments.length === 0){
return;
}
// Find current index if we have a selected comment.
const index = selectedCommentId
? comments.findIndex((comment) => comment.id === selectedCommentId)
: null;
if (next) {
// Grab first one if we don't have a selected comment yet.
if (!selectedCommentId) {
this.setState({selectedCommentId: comments[0].id}, () => this.scrollTo(comments[0].id));
return;
}
// Select next one when we still have more comments left.
if (index < comments.length - 1) {
this.setState({selectedCommentId: comments[index + 1].id}, () => this.scrollTo(comments[index + 1].id));
return;
} else {
// We hit the end of the list, load more comments if we have.
if (comments.length < this.getActiveTabCount()) {
const res = await this.loadMore();
// If `loadMore` was already in progress, res would be false.
if (res) {
// Select next comment after loading has completed.
this.select(true);
}
}
return;
}
} else {
// We have no selected comment, so just skip it.
if (!selectedCommentId) {
return;
}
// If we still have previous comments take the one before.
if (index > 0) {
this.setState({selectedCommentId: comments[index - 1].id}, () => this.scrollTo(comments[index - 1].id));
return;
}
}
}
loadMore = async () => {
if (!this.isLoadingMore) {
this.isLoadingMore = true;
if (!this.state.isLoadingMore) {
this.setState({isLoadingMore: true});
try {
const result = await this.props.loadMore(this.props.activeTab);
this.isLoadingMore = false;
this.setState({isLoadingMore: false});
return result;
}
catch (e) {
this.isLoadingMore = false;
this.setState({isLoadingMore: false});
throw e;
}
}
@@ -176,56 +110,12 @@ class Moderation extends Component {
key.unbind('esc');
key.unbind('ctrl+f');
key.unbind('t');
key.unbind('j');
key.unbind('k');
key.unbind('f');
key.unbind('d');
this.getMenuItems()
.forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
}
componentWillReceiveProps(nextProps) {
if (this.props.activeTab !== nextProps.activeTab) {
// Reset selection when changing tabs.
this.select(true, nextProps, null);
} else {
// Detect if comment has left the queue and find next or prev selected comment to set it
// as the new selectedCommentId.
const prevComments = this.getComments(this.props);
const nextComments = this.getComments(nextProps);
if (nextComments.length < prevComments.length) {
// Comments have changed, now check if our selected comment has left the queue.
if (
this.state.selectedCommentId &&
!nextComments.some((comment) => comment.id === this.state.selectedCommentId)
) {
// Determine a comment to select.
const prevIndex = prevComments.findIndex((comment) => comment.id === this.state.selectedCommentId);
if (prevIndex !== prevComments.length - 1) {
this.setState({selectedCommentId: prevComments[prevIndex + 1].id});
} else if(prevIndex > 0) {
this.setState({selectedCommentId: prevComments[prevIndex - 1].id});
} else {
this.setState({selectedCommentId: null});
}
}
}
}
}
componentDidUpdate(prevProps) {
// Scroll to comment when changing from single wiew to normal view.
if (prevProps.moderation.singleView !== this.props.moderation.singleView && this.state.selectedCommentId) {
this.scrollTo(this.state.selectedCommentId, false);
}
}
render () {
const {root, data, moderation, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
const {asset} = root;
@@ -266,17 +156,22 @@ class Moderation extends Component {
root={this.props.root}
currentAsset={asset}
comments={comments.nodes}
hasNextPage={comments.hasNextPage}
activeTab={activeTab}
singleView={moderation.singleView}
selectedCommentId={this.state.selectedCommentId}
selectedCommentId={moderation.selectedCommentId}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
loadMore={this.loadMore}
commentBelongToQueue={this.props.commentBelongToQueue}
isLoadingMore={this.state.isLoadingMore}
commentCount={activeTabCount}
currentUserId={this.props.auth.user.id}
viewUserDetail={viewUserDetail}
selectCommentId={props.selectCommentId}
cleanUpQueue={props.cleanUpQueue}
/>
<ModerationKeysModal
hideShortcutsNote={props.hideShortcutsNote}
@@ -307,12 +202,15 @@ class Moderation extends Component {
Moderation.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
toggleModal: PropTypes.func.isRequired,
selectedCommentId: PropTypes.string,
toggleStorySearch: PropTypes.func.isRequired,
getModPath: PropTypes.func.isRequired,
cleanUpQueue: PropTypes.func.isRequired,
storySearchChange: PropTypes.func.isRequired,
moderation: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
queueConfig: PropTypes.object.isRequired,
commentBelongToQueue: PropTypes.func.isRequired,
handleCommentChange: PropTypes.func.isRequired,
setSortOrder: PropTypes.func.isRequired,
showBanUserDialog: PropTypes.func.isRequired,
@@ -0,0 +1,3 @@
.root {
height: 100%;
}
@@ -2,29 +2,15 @@
padding: 8px 0;
list-style: none;
display: block;
min-height: 650px;
margin-top: 16px;
}
:global(html) {
height: inherit;
}
.list {
padding: 0;
margin: 0;
}
.commentLeave {
opacity: 1.0;
}
.commentLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.commentEnter {
opacity: 0;
}
.commentEnterActive {
opacity: 1.0;
transition: opacity 800ms;
outline: none;
}
@@ -4,26 +4,29 @@ import PropTypes from 'prop-types';
import Comment from '../containers/Comment';
import styles from './ModerationQueue.css';
import EmptyCard from '../../../components/EmptyCard';
import LoadMore from '../../../components/LoadMore';
import AutoLoadMore from './AutoLoadMore';
import ViewMore from './ViewMore';
import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
import {WindowScroller, CellMeasurer, CellMeasurerCache, List} from 'react-virtualized';
import throttle from 'lodash/throttle';
import key from 'keymaster';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
// the current comment The spare cursor functions as a backup in case one
// of the comments gets deleted. Additionally the new view based on the new
// cursors are also returned.
function resetCursors(state, props) {
let idCursors = [];
if (props.comments && props.comments.length) {
const idCursors = [props.comments[0].id];
idCursors.push(props.comments[0].id);
if (props.comments[1]) {
idCursors.push(props.comments[1].id);
}
return {idCursors};
}
return {idCursors: []};
const view = getVisibleComments(props.comments, idCursors[0]);
return {idCursors, view};
}
// invalidateCursor is called whenever a comment is removed which is referenced
@@ -40,91 +43,306 @@ function invalidateCursor(invalidated, state, props) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
return idCursors;
}
// getVisibileComments returns a list containing comments
// which comes after the `idCursor`.
function getVisibleComments(comments, idCursor) {
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor) {
view.push(comment);
}
});
return view;
}
// Current keymapper to use for the CellMeasurer Cache.
let keyMapper = null;
// CellMeasurerCache is used to measure the size of the elements
// of the virtual list. We use a global one with a keyMapper that
// should resolve to a comment id, which is then used to cache the height.
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 250,
keyMapper: (index) => keyMapper(index),
});
class ModerationQueue extends React.Component {
isLoadingMore = false;
listRef = null;
callbackCaches = {
clearHeightCache: {},
selectCommentId: {},
};
constructor(props) {
super(props);
this.state = {
...resetCursors(this.state, props),
};
// Set keyMapper to map to comment ids.
keyMapper = (index) => {
const view = this.state.view;
if (index < view.length) {
return view[index].id;
}
else if (index === view.length) {
return 'loadMore';
}
throw new Error(`unknown index ${index}`);
};
// Select first comment.
if (this.state.view.length) {
props.selectCommentId(this.state.view[0].id);
}
}
componentDidUpdate (prev) {
const {comments, commentCount} = this.props;
componentDidMount() {
key('j', () => this.selectDown());
key('k', () => this.selectUp());
// if the user just moderated the last (visible) comment
// AND there are more comments available on the server,
// go ahead and load more comments
if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) {
this.props.loadMore();
}
// TODO: Workaround for issue https://github.com/bvaughn/react-virtualized/issues/866
this.reflowList();
}
componentWillUnmount() {
key.unbind('j');
key.unbind('k');
// When switching queues, clean it up first.
// Removes dangling comments and reduce overly large
// lists and restore chronological order.
this.props.cleanUpQueue(this.props.activeTab);
}
componentWillReceiveProps(next) {
const {comments: prevComments} = this.props;
const {comments: nextComments} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
// New comments where added and our cursor list is incomplete.
if (this.state.idCursors.length < 2 && nextComments.length > this.state.idCursors.length) {
this.setState(resetCursors(this.state, next));
return;
}
let idCursors = this.state.idCursors;
// Comments have been removed.
if (
prevComments && nextComments &&
nextComments.length < prevComments.length
nextComments.length < prevComments.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
idCursors = invalidateCursor(0, this.state, next);
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
idCursors = invalidateCursor(1, this.state, next);
}
// Selected comment was removed, determine and set next selected comment.
if (
this.props.selectedCommentId &&
!hasComment(nextComments, this.props.selectedCommentId)
) {
const view = this.state.view;
let nextSelectedCommentId = null;
// Determine a comment to select.
const prevIndex = view.findIndex((comment) => comment.id === this.props.selectedCommentId);
if (prevIndex !== view.length - 1) {
nextSelectedCommentId = view[prevIndex + 1].id;
} else if(prevIndex > 0) {
nextSelectedCommentId = view[prevIndex - 1].id;
}
this.props.selectCommentId(nextSelectedCommentId);
}
}
// Comments changed.
if (prevComments !== nextComments) {
const nextView = getVisibleComments(nextComments, idCursors[0]);
this.setState({idCursors, view: nextView});
// TODO: removing or adding a comment from the list seems to render incorrect, is this a bug?
// Find first changed comment and perform a reflow.
const index = this.state.view.findIndex((comment, i) => !nextView[i] || nextView[i].id !== comment.id);
this.reflowList(index);
}
}
viewNewComments = () => {
this.setState(resetCursors);
componentDidUpdate (prev) {
const {commentCount, selectedCommentId} = this.props;
// If the user just moderated the last (visible) comment
// AND there are more comments available on the server,
// go ahead and load more comments
if (prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0 && commentCount > 0) {
this.props.loadMore();
}
// Scroll to selected comment.
if (prev.selectedCommentId !== selectedCommentId && this.listRef) {
const view = this.state.view;
const index = view.findIndex(({id}) => id === selectedCommentId);
this.listRef.scrollToRow(index);
}
}
// Returns comment counts without dangling comments.
getCommentCountWithoutDagling(props = this.props) {
return props.comments.filter((comment) => props.commentBelongToQueue(props.activeTab, comment)).length;
}
async selectDown() {
const view = this.state.view;
const index = view.findIndex(({id}) => id === this.props.selectedCommentId);
if (index === view.length - 1 && this.getCommentCountWithoutDagling() !== this.props.commentCount) {
await this.props.loadMore();
this.selectDown();
return;
}
if (index < view.length - 1) {
this.props.selectCommentId(view[index + 1].id);
}
}
selectUp() {
const view = this.state.view;
const index = view.findIndex(({id}) => id === this.props.selectedCommentId);
if (index === 0 && view.length < this.props.comments.length) {
this.viewNewComments(() => this.selectUp());
return;
}
if (index > 0) {
this.props.selectCommentId(view[index - 1].id);
}
}
handleListRef = (list) => {
this.listRef = list;
};
// getVisibileComments returns a list containing comments
// which comes after the `idCursor`.
getVisibleComments() {
const {comments} = this.props;
const idCursor = this.state.idCursors[0];
viewNewComments = (callback) => {
this.setState(resetCursors, () => {
this.reflowList();
callback && callback();
});
};
if (!comments) {
return [];
reflowList = throttle((index) => {
if (index >= 0) {
cache.clear(index);
this.listRef && this.listRef.recomputeRowHeights(index);
}
else {
cache.clearAll();
this.listRef && this.listRef.recomputeRowHeights();
}
}, 500);
rowRenderer = ({
index, // Index of row within collection
parent,
style // Style object to be applied to row (to position it)
}) => {
const view = this.state.view;
const rowCount = view.length + 1;
let child = null;
let key = null;
// Last element of list is our AutoLoadMore component and contains an
// id indicating that this is the last element in list.
if (index === rowCount - 1) {
key = 'end-of-comment-list';
child = (
<div
style={style}
id={'end-of-comment-list'}
>
{this.props.hasNextPage && <AutoLoadMore
loadMore={this.props.loadMore}
loading={this.props.isLoadingMore}
/>}
</div>
);
}
else {
const comment = view[index];
// Use callback cache so not to change the identity of these arrow functions.
// Otherwise shallow compare will fail to optimize.
if (!this.callbackCaches.clearHeightCache[index]) {
this.callbackCaches.clearHeightCache[index] = () => this.reflowList(index);
}
if (!this.callbackCaches.selectCommentId[comment.id]) {
this.callbackCaches.selectCommentId[comment.id] = () => this.props.selectCommentId(comment.id);
}
key = comment.id;
child = (
<div
style={style}
>
<Comment
data={this.props.data}
root={this.props.root}
comment={comment}
dangling={!this.props.commentBelongToQueue(this.props.activeTab, comment)}
selected={comment.id === this.props.selectedCommentId}
viewUserDetail={this.props.viewUserDetail}
showBanUserDialog={this.props.showBanUserDialog}
showSuspendUserDialog={this.props.showSuspendUserDialog}
acceptComment={this.props.acceptComment}
rejectComment={this.props.rejectComment}
currentAsset={this.props.currentAsset}
currentUserId={this.props.currentUserId}
clearHeightCache={this.callbackCaches.clearHeightCache[index]}
selectComment={this.callbackCaches.selectCommentId[comment.id]}
/>
</div>
);
}
const view = [];
let pastCursor = false;
comments.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor) {
view.push(comment);
}
});
return view;
}
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
{child}
</CellMeasurer>
);
};
render () {
const {
comments,
selectedCommentId,
commentCount,
singleView,
viewUserDetail,
activeTab,
...props
} = this.props;
@@ -159,54 +377,35 @@ class ModerationQueue extends React.Component {
);
}
const view = this.getVisibleComments();
const view = this.state.view;
return (
<div className={styles.root}>
<ViewMore
viewMore={this.viewNewComments}
viewMore={() => this.viewNewComments()}
count={comments.length - view.length}
/>
<CSSTransitionGroup
key={activeTab}
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.commentEnter,
enterActive: styles.commentEnterActive,
leave: styles.commentLeave,
leaveActive: styles.commentLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{
view
.map((comment) => {
return <Comment
data={this.props.data}
root={this.props.root}
key={comment.id}
comment={comment}
selected={comment.id === selectedCommentId}
viewUserDetail={viewUserDetail}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
currentUserId={this.props.currentUserId}
/>;
})
}
</CSSTransitionGroup>
<LoadMore
loadMore={this.props.loadMore}
showLoadMore={comments.length < commentCount}
/>
<WindowScroller onResize={this.reflowList}>
{({height, isScrolling, onChildScroll, scrollTop}) => (
<List
ref={this.handleListRef}
autoHeight
className={styles.list}
style={{
width: '100%',
}}
height={height}
width={1280}
scrollTop={scrollTop}
isScrolling={isScrolling}
onScroll={onChildScroll}
rowCount={view.length + 1}
deferredMeasurementCache={cache}
rowRenderer={this.rowRenderer}
rowHeight={cache.rowHeight}
/>
)}
</WindowScroller>
</div>
);
}
@@ -216,14 +415,19 @@ ModerationQueue.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
selectCommentId: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
comments: PropTypes.array.isRequired,
commentBelongToQueue: PropTypes.func.isRequired,
cleanUpQueue: PropTypes.func.isRequired,
commentCount: PropTypes.number.isRequired,
loadMore: PropTypes.func.isRequired,
selectedCommentId: PropTypes.string,
singleView: PropTypes.bool,
isLoadingMore: PropTypes.bool,
hasNextPage: PropTypes.bool,
comments: PropTypes.array,
activeTab: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
@@ -13,6 +13,9 @@
background-color: #2376D8;
cursor: pointer;
text-transform: capitalize;
margin: 0;
margin-top: -18px;
margin-bottom: -4px;
}
.viewMore:hover {
@@ -8,6 +8,7 @@
overflow: visible;
height: 144px;
min-height: auto;
margin-top: 16px;
z-index: 10;
@media (--tablet) {
@@ -11,7 +11,7 @@ import NotFoundAsset from '../components/NotFoundAsset';
import {isPremod, getModPath} from '../../../utils';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {handleCommentChange} from '../graphql';
import {handleCommentChange, commentBelongToQueue, cleanUpQueue} from '../graphql';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
@@ -23,7 +23,8 @@ import {
toggleStorySearch,
setSortOrder,
storySearchChange,
clearState
clearState,
selectCommentId,
} from 'actions/moderation';
import withQueueConfig from '../hoc/withQueueConfig';
import {notify} from 'coral-framework/actions/notification';
@@ -138,7 +139,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() {
@@ -168,6 +169,14 @@ class ModerationContainer extends Component {
}
}
cleanUpQueue = (queue) => {
if (!this.props.data.loading) {
this.props.data.updateQuery((query) => {
return cleanUpQueue(query, queue, this.props.moderation.sortOrder, this.props.queueConfig);
});
}
}
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
@@ -176,9 +185,13 @@ class ModerationContainer extends Component {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
commentBelongToQueue = (queue, comment) => {
return commentBelongToQueue(queue, comment, this.props.queueConfig);
}
loadMore = (tab) => {
const variables = {
limit: 10,
limit: 20,
cursor: this.props.root[tab].endCursor,
sortOrder: this.props.data.variables.sortOrder,
asset_id: this.props.data.variables.asset_id,
@@ -194,7 +207,6 @@ class ModerationContainer extends Component {
[tab]: {
nodes: {$push: comments.nodes},
hasNextPage: {$set: comments.hasNextPage},
startCursor: {$set: comments.startCursor},
endCursor: {$set: comments.endCursor},
},
});
@@ -246,6 +258,9 @@ class ModerationContainer extends Component {
activeTab={this.activeTab}
queueConfig={currentQueueConfig}
handleCommentChange={this.handleCommentChange}
selectedCommentId={this.props.selectedCommentId}
commentBelongToQueue={this.commentBelongToQueue}
cleanUpQueue={this.cleanUpQueue}
/>;
}
}
@@ -361,7 +376,8 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
asset_id: $asset_id,
sortOrder: $sortOrder
sortOrder: $sortOrder,
limit: 20,
}) {
...CoralAdmin_Moderation_CommentConnection
}
@@ -386,6 +402,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
organizationName
moderation
}
me {
id
}
...${getDefinitionName(Comment.fragments.root)}
}
${Comment.fragments.root}
@@ -423,6 +442,7 @@ const mapDispatchToProps = (dispatch) => ({
storySearchChange,
clearState,
notify,
selectCommentId,
}, dispatch),
});
@@ -16,16 +16,21 @@ function queueHasComment(root, queue, id) {
return root[queue].nodes.find((c) => c.id === id);
}
function removeCommentFromQueue(root, queue, id) {
function removeCommentFromQueue(root, queue, id, dangling = false) {
if (!queueHasComment(root, queue, id)) {
return root;
}
return update(root, {
const changes = {
[`${queue}Count`]: {$set: root[`${queue}Count`] - 1},
[queue]: {
};
if (!dangling) {
changes[queue] = {
nodes: {$apply: (nodes) => nodes.filter((c) => c.id !== id)},
},
});
};
}
return update(root, changes);
}
function shouldCommentBeAdded(root, queue, comment, sortOrder) {
@@ -40,26 +45,46 @@ function shouldCommentBeAdded(root, queue, comment, sortOrder) {
: new Date(comment.created_at) >= cursor;
}
function addCommentToQueue(root, queue, comment, sortOrder) {
function addCommentToQueue(root, queue, comment, sortOrder, cleanup) {
if (queueHasComment(root, queue, comment.id)) {
return root;
}
const sortAlgo = sortOrder === 'ASC' ? ascending : descending;
const changes = {
[`${queue}Count`]: {$set: root[`${queue}Count`] + 1},
};
if (shouldCommentBeAdded(root, queue, comment, sortOrder)) {
const nodes = root[queue].nodes.concat(comment).sort(sortAlgo);
changes[queue] = {
nodes: {$set: nodes},
startCursor: {$set: nodes[0].created_at},
endCursor: {$set: nodes[nodes.length - 1].created_at},
};
if (!shouldCommentBeAdded(root, queue, comment, sortOrder)) {
return update(root, changes);
}
return update(root, changes);
const cursor = new Date(root[queue].startCursor);
const date = new Date(comment.created_at);
let append = sortOrder === 'ASC'
? date >= cursor
: date <= cursor;
const nodes = append
? root[queue].nodes.concat(comment)
: [comment].concat(...root[queue].nodes);
changes[queue] = {
nodes: {$set: nodes},
};
const next = update(root, changes);
if (!cleanup) {
return next;
}
return cleanUpQueue(next, queue, sortOrder);
}
function sortComments(nodes, sortOrder) {
const sortAlgo = sortOrder === 'ASC' ? ascending : descending;
return nodes.sort(sortAlgo);
}
/**
@@ -68,24 +93,92 @@ function addCommentToQueue(root, queue, comment, sortOrder) {
function getCommentQueues(comment, queueConfig) {
const queues = [];
Object.keys(queueConfig).forEach((key) => {
const {action_type, statuses, tags} = queueConfig[key];
let addToQueues = true;
if (statuses && statuses.indexOf(comment.status) === -1) {
addToQueues = false;
}
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
addToQueues = false;
}
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
addToQueues = false;
}
if (addToQueues) {
if (commentBelongToQueue(key, comment, queueConfig)) {
queues.push(key);
}
});
return queues;
}
/**
* Return whether or not the comment belongs to the queue.
*/
export function commentBelongToQueue(queue, comment, queueConfig) {
const {action_type, statuses, tags} = queueConfig[queue];
let belong = true;
if (statuses && statuses.indexOf(comment.status) === -1) {
belong = false;
}
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
belong = false;
}
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
belong = false;
}
return belong;
}
function isVisible(id) {
return !!document.getElementById(`comment_${id}`);
}
function isEndOfListVisible(root, queue) {
return root[queue].nodes.length === 0 || !!document.getElementById('end-of-comment-list');
}
function applyCommentChanges(root, comment, queueConfig) {
const queues = Object.keys(queueConfig);
for (let i = 0; i < queues.length; i++) {
const queue = queues[i];
const index = root[queue].nodes.findIndex(({id}) => id === comment.id);
if (index > -1) {
return update(root, {
[queue]: {
nodes: {
[index]: {$merge: comment},
},
},
});
}
}
return root;
}
/**
* Remove dangling comments, sort and resize queues.
* If queueConfig is omitted, dangling comments are not removed.
*/
export function cleanUpQueue(root, queue, sortOrder, queueConfig) {
let nodes = root[queue].nodes;
let hasNextPage = root[queue].hasNextPage;
if (!nodes.length) {
return root;
}
if (queueConfig) {
nodes = root[queue].nodes.filter((comment) => commentBelongToQueue(queue, comment, queueConfig));
}
nodes = sortComments(
nodes,
sortOrder,
);
if (nodes.length > 100) {
nodes = nodes.slice(0, 100);
hasNextPage = true;
}
return update(root, {
[queue]: {
nodes: {$set: nodes},
endCursor: {$set: nodes[nodes.length - 1].created_at},
hasNextPage: {$set: hasNextPage},
},
});
}
/**
* Assimilate comment changes into current store.
* @param {Object} root current state of the store
@@ -113,25 +206,27 @@ export function handleCommentChange(root, comment, sortOrder, notify, queueConfi
Object.keys(queueConfig).forEach((queue) => {
if (nextQueues.indexOf(queue) >= 0) {
if (!queueHasComment(next, queue, comment.id)) {
next = addCommentToQueue(next, queue, comment, sortOrder);
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sortOrder)) {
next = addCommentToQueue(next, queue, comment, sortOrder, activeQueue !== queue);
if (notify && activeQueue === queue && isEndOfListVisible(root, queue)) {
showNotificationOnce();
}
}
} else if(queueHasComment(next, queue, comment.id)){
next = removeCommentFromQueue(next, queue, comment.id);
if (notify && activeQueue === queue) {
const dangling = activeQueue === queue && comment.status_history[comment.status_history.length - 1].assigned_by.id !== root.me.id;
next = removeCommentFromQueue(next, queue, comment.id, dangling);
if (notify && isVisible(comment.id)) {
showNotificationOnce();
}
}
if (
notify
&& queueHasComment(next, queue, comment.id)
&& activeQueue === queue
) {
if (notify && isVisible(comment.id)) {
showNotificationOnce();
}
// We need to apply every comment change, because we use
// batched subscription handler which bypasses apollo that would
// have done that for us.
next = applyCommentChanges(next, comment, queueConfig);
});
return next;
}
@@ -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);
+19 -1
View File
@@ -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: {
@@ -192,7 +193,12 @@ export default {
},
updateQueries: {
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED' || comment.status === 'SYSTEM_WITHHELD') {
if (
prev.me.roles.indexOf('ADMIN') === -1 && prev.asset.settings.moderation === 'PRE' ||
comment.status === 'PREMOD' ||
comment.status === 'REJECTED' ||
comment.status === 'SYSTEM_WITHHELD'
) {
return prev;
}
return insertCommentIntoEmbedQuery(prev, comment);
@@ -218,6 +224,18 @@ export default {
},
},
}),
UpdateAssetSettings: ({variables: {input}}) => ({
updateQueries: {
CoralEmbedStream_Embed: (prev) => {
const updated = update(prev, {
asset: {
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
},
});
return updated;
}
}
}),
},
};
+10 -20
View File
@@ -38,7 +38,7 @@ function applyToCommentsOrigin(root, callback) {
function findAndInsertComment(parent, comment) {
const isAsset = parent.__typename === 'Asset';
const [connectionField, countField, action] = isAsset
? ['comments', 'commentCount', '$unshift']
? ['comments', 'totalCommentCount', '$unshift']
: ['replies', 'replyCount', '$push'];
if (
@@ -67,19 +67,12 @@ function findAndInsertComment(parent, comment) {
}
export function insertCommentIntoEmbedQuery(root, comment) {
// Increase total comment count by one.
root = update(root, {
asset: {
totalCommentCount: {$apply: (c) => c + 1},
},
});
return applyToCommentsOrigin(root, (origin) => findAndInsertComment(origin, comment));
}
function findAndRemoveComment(parent, id) {
const [connectionField, countField] = parent.__typename === 'Asset'
? ['comments', 'commentCount']
? ['comments', 'totalCommentCount']
: ['replies', 'replyCount'];
const connection = parent[connectionField];
@@ -104,13 +97,6 @@ function findAndRemoveComment(parent, id) {
}
export function removeCommentFromEmbedQuery(root, id) {
// Decrease total comment by one.
root = update(root, {
asset: {
totalCommentCount: {$apply: (c) => c - 1},
},
});
return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id));
}
@@ -142,17 +128,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,9 +151,32 @@
}
.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 {
@@ -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';
@@ -452,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}
@@ -2,9 +2,22 @@
margin-top: 6px;
}
.viewAllButton {
.viewAllButtonContainer {
position: absolute;
right: 0px;
width: 100%;
display: flex;
justify-content: center;
margin-top: -11px;
z-index: 10;
}
.viewAllButton {
composes: buttonReset from "coral-framework/styles/reset.css";
background-color: #4D8FCC;
color: white;
padding: 4px 8px;
border-radius: 2px;
}
.tabPanel {
@@ -26,4 +39,4 @@
margin-top: 28px;
padding-bottom: 50px;
min-height: 600px;
}
}
@@ -11,15 +11,14 @@ import RestrictedMessageBox
from 'coral-framework/components/RestrictedMessageBox';
import t, {timeago} from 'coral-framework/services/i18n';
import CommentBox from 'talk-plugin-commentbox/CommentBox';
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import QuestionBox from '../../../components/QuestionBox';
import {isCommentActive} from 'coral-framework/utils';
import {Button, Tab, TabCount, TabPane} from 'coral-ui';
import {Tab, TabCount, TabPane} from 'coral-ui';
import cn from 'classnames';
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
import {getTopLevelParent, attachCommentToParent} from '../../../graphql/utils';
import AllCommentsPane from './AllCommentsPane';
import AutomaticAssetClosure from '../containers/AutomaticAssetClosure';
import StreamTabPanel from '../containers/StreamTabPanel';
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
import styles from './Stream.css';
@@ -47,7 +46,8 @@ class Stream extends React.Component {
activeReplyBox,
setActiveReplyBox,
commentClassNames,
root: {asset, asset: {comment}},
asset,
asset: {comment},
postComment,
notify,
editComment,
@@ -58,6 +58,7 @@ class Stream extends React.Component {
loadNewReplies,
auth: {user},
emit,
viewAllComments,
} = this.props;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
@@ -76,6 +77,14 @@ class Stream extends React.Component {
return (
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
<div className={cn('talk-stream-show-all-comments-button-container', styles.viewAllButtonContainer)}>
<button
className={cn('talk-stream-show-all-comments-button', styles.viewAllButton)}
onClick={viewAllComments}
>
{t('framework.show_all_comments')}
</button>
</div>
<Comment
data={data}
root={root}
@@ -106,14 +115,15 @@ class Stream extends React.Component {
);
}
renderTabPanel() {
renderExtendableTabPanel() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
commentClassNames,
root: {asset, asset: {comments, totalCommentCount}},
asset,
asset: {comments, totalCommentCount},
postComment,
notify,
editComment,
@@ -135,7 +145,7 @@ class Stream extends React.Component {
const slotProps = {data};
const slotQueryData = {root, asset};
// `key` of `StreamTabPanel` depends on sorting so that we always reset
// `key` of `ExtendableTabPanel` depends on sorting so that we always reset
// the state when changing sorting.
return (
<div className={cn('talk-stream-tab-container', styles.tabContainer)}>
@@ -148,26 +158,28 @@ class Stream extends React.Component {
{...slotProps}
/>
</div>
<StreamTabPanel
<ExtendableTabPanel
key={`${sortBy}_${sortOrder}`}
activeTab={activeStreamTab}
setActiveTab={setActiveStreamTab}
fallbackTab={'all'}
tabSlot={'streamTabs'}
tabPaneSlot={'streamTabPanes'}
fallbackTab='all'
tabSlot='streamTabs'
tabSlotPrepend='streamTabsPrepend'
tabPaneSlot='streamTabPanes'
slotProps={slotProps}
queryData={slotQueryData}
loading={loading}
appendTabs={
tabs={
<Tab tabId={'all'} key='all'>
{t('stream.all_comments')} <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
</Tab>
}
appendTabPanes={
<TabPane tabId={'all'} key='all'>
tabPanes={
<TabPane tabId='all' key='all'>
<AllCommentsPane
data={data}
root={root}
asset={asset}
comments={comments}
commentClassNames={commentClassNames}
setActiveReplyBox={setActiveReplyBox}
@@ -175,7 +187,6 @@ class Stream extends React.Component {
notify={notify}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
@@ -201,11 +212,16 @@ class Stream extends React.Component {
data,
root,
appendItemArray,
root: {asset, asset: {comment: highlightedComment}},
asset,
asset: {
comment: highlightedComment,
settings: {
questionBoxEnable,
}
},
postComment,
notify,
updateItem,
viewAllComments,
auth: {loggedIn, user},
editName,
} = this.props;
@@ -213,14 +229,13 @@ class Stream extends React.Component {
const open = !asset.isClosed;
const banned = user && user.status === 'BANNED';
const pending = user && user.status === 'PENDING';
const temporarilySuspended =
user &&
user.suspension.until &&
new Date(user.suspension.until) > new Date();
const showCommentBox = loggedIn && ((!banned && !pending & !temporarilySuspended && !highlightedComment) || keepCommentBox);
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const slotProps = {data};
const slotQueryData = {root, asset};
@@ -230,27 +245,23 @@ class Stream extends React.Component {
return (
<div id="stream" className={styles.root}>
<AutomaticAssetClosure assetId={asset.id} closedAt={asset.closedAt}/>
{highlightedComment &&
<Button
cStyle="darkGrey"
className={cn('talk-stream-show-all-comments-button', styles.viewAllButton)}
onClick={viewAllComments}
>
{t('framework.show_all_comments')}
</Button>}
{open
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<QuestionBox
content={asset.settings.questionBoxContent}
enable={asset.settings.questionBoxEnable}
icon={asset.settings.questionBoxIcon}
/>
{questionBoxEnable && (
<QuestionBox
content={asset.settings.questionBoxContent}
icon={asset.settings.questionBoxIcon}>
<Slot
fill="streamQuestionArea"
queryData={slotQueryData}
{...slotProps}
/>
</QuestionBox>
)}
{!banned &&
temporarilySuspended &&
<RestrictedMessageBox>
@@ -297,7 +308,7 @@ class Stream extends React.Component {
{highlightedComment
? this.renderHighlightedComment()
: this.renderTabPanel()
: this.renderExtendableTabPanel()
}
</div>
);
@@ -325,7 +336,7 @@ Stream.propTypes = {
loading: PropTypes.bool,
editName: PropTypes.func,
appendItemArray: PropTypes.func,
updateItem: PropTypes.func,
updateItem: PropTypes.func,
viewAllComments: PropTypes.func,
notify: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import styles from './SuspendAccount.css';
import styles from './SuspendedAccount.css';
import {Button} from 'coral-ui';
import validate from 'coral-framework/helpers/validate';
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';

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