Compare commits

...
131 Commits
Author SHA1 Message Date
Kim Gardner 1d986e7a0b Merge pull request #1003 from coralproject/fix-edited-marker
Fix styling of edited marker
2017-10-03 16:36:32 +01:00
Chi Vinh Le 79d312df50 Fix styling of edited marker 2017-10-03 22:25:37 +07:00
Kim Gardner eb969f8050 Merge pull request #999 from coralproject/subscriber-badge
Subscriber Badge Plugin and User Tags bug
2017-10-03 05:00:00 +01:00
Belen Curcio dde1f7b5be Assigned by removed 2017-10-02 16:26:21 -03:00
Belen Curcio 343d4418b0 eslint updated 2017-10-02 16:23:56 -03:00
Kim Gardner 4c28e43e62 Merge branch 'master' into subscriber-badge 2017-10-02 18:31:15 +01:00
Kim Gardner 1030ca1fa2 Merge pull request #1001 from coralproject/toxicity-detail
Revamp comment details and implement toxicity detail
2017-10-02 18:25:53 +01:00
Chi Vinh Le b1bc97c8d5 Improve styling 2017-10-02 22:34:12 +07:00
Chi Vinh Le 18b802c555 Merge branch 'master' into toxicity-detail 2017-10-02 22:30:49 +07:00
Chi Vinh Le 1c16a75f6b Merge branch 'master' into toxicity-detail
Conflicts:
	plugin-api/beta/client/components/index.js
2017-10-02 22:27:57 +07:00
Chi Vinh Le 8724cfe325 Add talk-plugin-flag-details as default plugins 2017-10-02 22:22:55 +07:00
Chi Vinh Le d3d7f909d8 Translations 2017-10-02 22:22:00 +07:00
Chi Vinh Le c2f22bbf98 Implement user flag details as slot component 2017-10-02 21:37:47 +07:00
Chi Vinh Le 9b39017f25 Export viewUserDetail in plugin-api 2017-10-02 20:54:49 +07:00
Chi Vinh Le 075d62a32a Move CommentDetails to plugin-api 2017-10-02 20:50:43 +07:00
Kim Gardner 2d92284622 Merge branch 'master' into subscriber-badge 2017-10-02 13:00:45 +01:00
Kim Gardner e99b79687f Merge pull request #1000 from coralproject/shorcuts
Admin Shortcuts working
2017-10-02 13:00:29 +01:00
Kim Gardner 9182d5b09c Merge branch 'master' into subscriber-badge 2017-10-02 11:59:21 +01:00
Kim Gardner 89b87b01df Merge branch 'master' into shorcuts 2017-10-02 11:58:07 +01:00
Kim Gardner 257f361998 Merge pull request #997 from vickychijwani/patch-1
Fix double preposition in string
2017-10-02 11:32:13 +01:00
Belen Curcio 02dff0c726 linting 2017-10-02 01:32:36 -03:00
Belen Curcio 17f5800363 Shorcuts working again! 2017-10-02 01:24:59 -03:00
Belen Curcio 7aaad6ccd9 Adds subscriber plugin 2017-10-02 01:06:08 -03:00
Belen Curcio 43cb0efe7e bug: usertags 2017-10-02 01:03:20 -03:00
Vicky Chijwani 06ced883ba Fix double preposition in French and Portuguese translations 2017-10-01 05:16:43 +05:30
Vicky Chijwani f7e6f67f4e Fix double preposition in string
This string is followed by a relative time duration with a preposition included, like "in 1 week".
2017-10-01 05:06:10 +05:30
Wyatt Johnson 307fe58cf3 Merge pull request #989 from coralproject/use-talk-linting-preset
Use eslint-config-talk preset
2017-09-29 11:21:06 -06:00
Wyatt Johnson 48f2cce893 Merge branch 'master' into use-talk-linting-preset 2017-09-29 11:02:18 -06:00
Wyatt Johnson b597f730f6 Merge pull request #996 from coralproject/new-docs
Updated docs
2017-09-29 11:02:06 -06:00
Chi Vinh Le d396f43fe3 Merge branch 'master' into use-talk-linting-preset
Conflicts:
	package.json
2017-09-29 23:55:33 +07:00
Chi Vinh Le 799a21fecd Upgrade eslint-config-talk 2017-09-29 23:52:41 +07:00
Chi Vinh Le 7d41421526 adminCommentMoreFlagDetails slot and more 2017-09-29 23:34:40 +07:00
Chi Vinh Le 9bd55b267e excludeIf now returns a new component 2017-09-29 23:33:37 +07:00
Chi Vinh Le 90470b8331 Make IfSlotIsEmpty and IfSloIsNotEmpty accept an array of slot names 2017-09-29 23:33:12 +07:00
Kim Gardner df737ad201 Merge branch 'master' into new-docs 2017-09-29 16:56:44 +01:00
Kim Gardner 09ee854302 Merge pull request #984 from ryanyun/add-class-names
introduce classes for easier CSS selection for custom styling
2017-09-29 16:56:03 +01:00
Wyatt Johnson f88bd8b485 Merge branch 'master' into new-docs 2017-09-29 08:59:36 -06:00
Ryan Yun 36ba7367d1 add talk-embed-stream- prefix to classes 2017-09-29 10:57:23 -04:00
Chi Vinh Le f9693c7154 Refactor and pluginize comment details 2017-09-29 19:42:44 +07:00
Kim Gardner 445107ef64 Merge branch 'master' into add-class-names 2017-09-29 13:36:12 +01:00
Kim Gardner 1c3be913d3 Merge pull request #981 from coralproject/bump-version-3-6-0
Bump version to 3.6.0
2017-09-29 13:35:31 +01:00
Kim Gardner 09f9782505 Merge branch 'master' into new-docs 2017-09-29 12:21:37 +01:00
Kim Gardner 1d6e9527e5 Merge branch 'master' into bump-version-3-6-0 2017-09-29 12:21:16 +01:00
Kim Gardner d0ce832319 Merge pull request #990 from coralproject/update-translation-pt
Add translation of "new" queue in Portuguese
2017-09-29 12:21:05 +01:00
Wyatt Johnson 2e7ee6bb8b Updated docs 2017-09-28 23:38:01 -06:00
Wyatt Johnson ad3958a2e0 Merge branch 'master' into update-translation-pt 2017-09-28 09:45:12 -06:00
Wyatt Johnson 58acdef043 Merge branch 'master' into bump-version-3-6-0 2017-09-28 09:44:09 -06:00
Ryan Yun 99aad0f74b Merge branch 'master' into add-class-names 2017-09-28 11:38:14 -04:00
Chi Vinh Le 5d718f6dd4 Only shw user flags in detail 2017-09-28 22:02:06 +07:00
Kim Gardner 44ce71ef14 Merge pull request #974 from coralproject/redis-cluster-support
Redis Cluster Support
2017-09-28 15:44:02 +01:00
Kim Gardner 47cde9a842 Merge branch 'master' into redis-cluster-support 2017-09-28 15:38:51 +01:00
Chi Vinh Le 219399a231 Refactor ComemntDetail and implement ToxicDetail 2017-09-27 22:50:45 +07:00
Ryan Yun 95114bc847 Merge branch 'add-class-names' of github.com:ryanyun/talk into add-class-names 2017-09-27 11:21:19 -04:00
Ryan Yun 12047d1ae1 fix typo for dynamic class names for like and respect buttons 2017-09-27 11:20:57 -04:00
Kim Gardner 0362dad14b Merge branch 'master' into add-class-names 2017-09-27 16:06:21 +01:00
Kim Gardner 97b81cc1cc Merge pull request #991 from coralproject/featured-comment-content-slot
Use commentContent slot in Featured Comments
2017-09-27 16:05:51 +01:00
Kim Gardner 8d6b9dd23c Merge branch 'master' into featured-comment-content-slot 2017-09-27 10:54:37 +01:00
Kim Gardner 9adf11e1d1 Merge branch 'master' into update-translation-pt 2017-09-27 10:53:52 +01:00
Kim Gardner ef49d9a3d2 Merge pull request #988 from coralproject/password-reset-fix
Password reset fixes
2017-09-26 17:42:06 +01:00
Wyatt Johnson 921eea59c8 Merge branch 'master' into password-reset-fix 2017-09-26 10:36:47 -06:00
Kim Gardner c5cd88f4c0 Merge pull request #986 from coralproject/cleanup-dependencies
Cleanup dependencies
2017-09-26 17:25:10 +01:00
Kim Gardner 762f4f7898 Merge branch 'master' into cleanup-dependencies 2017-09-26 17:15:13 +01:00
Kiwi e400ae37f4 Merge branch 'master' into password-reset-fix 2017-09-26 18:14:20 +02:00
Kiwi 45b0ec48de Merge branch 'master' into featured-comment-content-slot 2017-09-26 18:11:21 +02:00
Kim Gardner f3f8c6b24e Merge pull request #985 from coralproject/mod-indicator
Moderate Indicator
2017-09-26 17:03:34 +01:00
Belen Curcio baa5bddd19 Merge branch 'mod-indicator' of github.com:coralproject/talk into mod-indicator 2017-09-26 12:43:05 -03:00
Belen Curcio bea7d75db7 Polling to update the Header Indicator 2017-09-26 12:42:39 -03:00
Belén Curcio 8e5edd65c7 Merge branch 'master' into mod-indicator 2017-09-26 12:36:59 -03:00
Wyatt Johnson 9ae59ef998 added parent url to the login popup 2017-09-26 09:35:33 -06:00
Chi Vinh Le 0f382bd742 Use commentContent slot in Featured Comments 2017-09-26 22:31:54 +07:00
Kim Gardner 55b1f4831a Add translation of "new" queue in Portuguese 2017-09-26 16:30:09 +01:00
Wyatt Johnson 3d2575413d Merge branch 'master' into password-reset-fix 2017-09-26 09:07:07 -06:00
Kiwi c40393347a Merge branch 'master' into use-talk-linting-preset 2017-09-26 14:39:25 +02:00
Chi Vinh Le 8c05e3df87 Use eslint-config-talk preset 2017-09-26 19:35:24 +07:00
Kim Gardner fa4adda61e Merge branch 'master' into redis-cluster-support 2017-09-26 12:26:24 +01:00
Kim Gardner ea06b334f6 Merge pull request #987 from coralproject/fix-contains-link-styling
Fix contains links styling
2017-09-26 12:25:30 +01:00
Wyatt Johnson 203d3c7ee9 bug fixes 2017-09-25 16:12:40 -06:00
Wyatt Johnson 825e1b5e5c Fixes to password reset flow 2017-09-25 14:50:33 -06:00
Chi Vinh Le 3db406bf26 Rename Community Query 2017-09-26 03:31:04 +07:00
Belén Curcio c5e078f1e1 Merge branch 'master' into mod-indicator 2017-09-25 13:49:56 -03:00
Belen Curcio e6f18d8558 Updates 2017-09-25 13:49:26 -03:00
Ryan Yun 76f259a2aa Merge branch 'master' into add-class-names 2017-09-25 12:34:20 -04:00
Ryan Yun 3f90f72bee update classes to follow talk naming convention 2017-09-25 12:33:59 -04:00
Chi Vinh Le f211bdf34e Fix contains links styling 2017-09-25 23:20:32 +07:00
Kiwi d2e5633d91 Merge branch 'master' into cleanup-dependencies 2017-09-25 18:20:07 +02:00
Kiwi b01e919029 Merge pull request #983 from coralproject/modqueue-live-new-comments
Modqueue live new comments
2017-09-25 18:19:46 +02:00
Wyatt Johnson 62b407179f Merge branch 'master' into redis-cluster-support 2017-09-25 10:10:09 -06:00
Belén Curcio 536ef70415 Merge branch 'master' into mod-indicator 2017-09-25 12:50:29 -03:00
Chi Vinh Le 981c319810 Clean dependencies 2017-09-25 21:34:35 +07:00
Belen Curcio 19e23479ee Showing the indicators 2017-09-22 17:45:28 -03:00
Belen Curcio 86687f10d8 Working indicators 2017-09-22 17:38:46 -03:00
Belen Curcio 3b6bcba055 Indicator for Community 2017-09-22 17:25:59 -03:00
Belen Curcio acdfaeee9f Moving the initial query up 2017-09-22 16:58:06 -03:00
Chi Vinh Le 792d0b9a2d Add comments 2017-09-23 02:18:45 +07:00
Chi Vinh Le 9be2379f30 Merge branch 'modqueue-live-new-comments' of ssh://github.com/coralproject/talk into modqueue-live-new-comments 2017-09-23 02:03:49 +07:00
Chi Vinh Le 1658b2594b PropType fixes 2017-09-23 02:03:34 +07:00
Kiwi d9e2fba569 Merge branch 'master' into modqueue-live-new-comments 2017-09-22 20:59:04 +02:00
Chi Vinh Le d9cabeafe2 Add translation and style fixes 2017-09-23 01:57:20 +07:00
Chi Vinh Le 171a6d506b Add proptypes 2017-09-23 01:09:32 +07:00
Chi Vinh Le a19de7bfb4 Don't scroll smooth when switching view mode 2017-09-23 00:53:27 +07:00
Ryan Yun 6138768588 add class names to moderation link 2017-09-22 12:34:27 -04:00
Ryan Yun 4c5f8b3359 add class name to submit button on configure comment stream form 2017-09-22 12:31:55 -04:00
Ryan Yun 605c87a799 featured and all comments have same left and right action container class names 2017-09-22 12:16:31 -04:00
Chi Vinh Le 1e0451ceba More styling refactor 2017-09-22 22:58:49 +07:00
Ryan Yun d60b869937 add class name to featured tag 2017-09-22 11:48:04 -04:00
Chi Vinh Le 9f4c97e138 Improve single view, keyboard navigation and refactor styles 2017-09-22 22:42:28 +07:00
Ryan Yun 66518f50dd add class name to off topic tag 2017-09-22 11:41:54 -04:00
Ryan Yun 7328926ad7 link button needs a span tag around button label 2017-09-22 11:20:06 -04:00
Ryan Yun a736c1620e add static class names for respected and liked buttons 2017-09-22 11:16:56 -04:00
Ryan Yun d359d915fe add class name for configure container 2017-09-22 11:11:22 -04:00
Ryan Yun 235b2e1068 add class name for my profile container 2017-09-22 11:10:39 -04:00
Kim Gardner f183aeb748 Merge pull request #979 from coralproject/pluginize-timestamp
Allow replacing comment timestamp
2017-09-22 14:24:15 +01:00
Chi Vinh Le 143adc7c6f Live update modqueue on new comments 2017-09-21 23:39:04 +07:00
Kim Gardner 91714705ea Merge branch 'master' into bump-version-3-6-0 2017-09-21 14:20:38 +01:00
Kim Gardner 64cc85eb24 Merge branch 'master' into pluginize-timestamp 2017-09-21 14:20:22 +01:00
Kim Gardner 7a043dac5b Merge pull request #982 from coralproject/remove-count
Remove count from Replies button and Change copy from Load More button
2017-09-21 14:19:57 +01:00
Belen Curcio edd3e7f6fe More copuy 2017-09-21 09:20:15 -03:00
Belen Curcio 3ef9bd5e52 Removing Count from replies button 2017-09-21 09:14:02 -03:00
Kim Gardner b60196e455 Bump version 2017-09-21 12:42:45 +01:00
Kim Gardner d315458e0e Merge branch 'master' into pluginize-timestamp 2017-09-21 12:41:42 +01:00
Wyatt Johnson abdb4cbe3e Merge pull request #980 from coralproject/version-bump
Update package.json
2017-09-20 16:27:33 -06:00
Wyatt Johnson 20380cd62f Update package.json 2017-09-20 16:23:07 -06:00
Wyatt Johnson 271f0cc237 Merge branch 'master' into redis-cluster-support 2017-09-20 13:54:41 -06:00
Kiwi 864aa2be28 Merge branch 'master' into pluginize-timestamp 2017-09-20 20:51:25 +02:00
Wyatt Johnson 4c8894d6e9 Merge pull request #978 from coralproject/fix-userdetail-all-tab
Fix empty user detail all tab
2017-09-20 12:48:46 -06:00
Chi Vinh Le dd5d06aa95 Request status=null 2017-09-21 01:43:04 +07:00
Chi Vinh Le 7e536cfb31 Rename PubDate to CommentTimestamp 2017-09-21 01:37:57 +07:00
Chi Vinh Le 5fa4b4d734 Remove styling that affects the slot.. 2017-09-21 01:24:59 +07:00
Chi Vinh Le e7c78a2465 Pluginize timestamp 2017-09-20 22:53:04 +07:00
Kim Gardner 95fadbd05a Merge branch 'master' into redis-cluster-support 2017-09-20 16:28:08 +01:00
Wyatt Johnson 6d04b4add3 Redis Cluster Support
- Deprecated unused `redis` options in favour of options from `ioredis`.
- Added support for ioredis Cluster.
- Enabled more extended redis client configuration.
2017-09-19 15:28:03 -06:00
438 changed files with 4797 additions and 25483 deletions
+2
View File
@@ -26,5 +26,7 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
node_modules
+1 -73
View File
@@ -1,75 +1,3 @@
{
"env": {
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaVersion": 2017
},
"plugins": [
"json",
"promise",
"react"
],
"rules": {
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "warn",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"react/display-name": "off",
// We'll keeping using this, as we'll switch over to Preact and we don't
// need to worry about deprecation.
"react/no-find-dom-node": "off",
// TODO: Use default after fixing the errors.
"react/prop-types": "warn",
"array-callback-return": "warn",
"arrow-parens": ["warn", "always"],
"comma-spacing": ["error", {"after": true}],
"curly": "error",
"eol-last": "warn",
"eqeqeq": ["error", "smart"],
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
"lines-around-comment": ["warn", {"beforeLineComment": true}],
"newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}],
"no-console": "off",
"no-const-assign": "error",
"no-duplicate-imports": "error",
"no-eval": "error",
"no-global-assign": "error",
"no-implied-eval": "error",
"no-lonely-if": "error",
"no-multiple-empty-lines": ["error", {"max": 1}],
"no-nested-ternary": "warn",
"no-path-concat": "error",
"no-script-url": "error",
"no-tabs": "error",
"no-template-curly-in-string": "warn",
"no-throw-literal": "error",
"no-unneeded-ternary": "warn",
"no-unsafe-negation": "warn",
"no-unused-vars": ["error", {"argsIgnorePattern": "^_|next", "varsIgnorePattern": "^_"}],
"no-var": "error",
"object-curly-spacing": "warn",
"prefer-template": "warn",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"space-in-parens": ["error", "never"],
"space-infix-ops": ["error"],
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
"spaced-comment": ["warn", "always", {"line": {"exceptions": ["-", "="]}}],
"template-curly-spacing": "warn",
"yoda": "warn"
}
"extends": "@coralproject/eslint-config-talk"
}
+2
View File
@@ -43,5 +43,7 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
**/node_modules/*
@@ -1,7 +1,4 @@
---
title: Contributor Covenant Code of Conduct
permalink: /docs/development/code-of-conduct/
---
# Contributor Covenant Code of Conduct
## Our Pledge
@@ -46,7 +43,7 @@ Other inappropriate behavior:
* Impersonation of someone else
* Violating someones privacy
If you feel that someone has harassed you or otherwise treated you or someone else inappropriately, please alert the project lead at [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org).
If you feel that someone has harassed you or otherwise treated you or someone else inappropriately, please alert the project lead at [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org).
## Our Responsibilities
@@ -71,11 +68,11 @@ If youre too frustrated to have a direct conversation, there are a number of
* Contact the project lead, Andrew Losowsky, [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org), or the technical lead. We will work with you to help you figure out how to ensure that any conflict doesnt interfere with your work, in confidence if you would prefer.
* Talk to Chris Lawrence. Chris oversees the project. He can be contacted at [clawrence@mozillafoundation.org](mailto:clawrence@mozillafoundation.org).
* Talk to Chris Lawrence. Chris oversees the project. He can be contacted at [clawrence@mozillafoundation.org](mailto:clawrence@mozillafoundation.org).
If you feel you have been unfairly accused of violating this code of conduct, you should contact Chris with a concise description of your grievance.
## Conclusion
We welcome your feedback on this and every other aspect of what we do as The Coral Project, and we thank you for working with us to make it a safe, enjoyable, and friendly experience for everyone involved in the project and what we do.
Above text is licensed CC BY-SA 4.0, adapted from the SRCCON code of conduct, FreeBSDs code of conduct, Vox Medias product team code of conduct, Mediums code of conduct, as well as adapted from the Contributor Covenant.
Above text is licensed CC BY-SA 4.0, adapted from the SRCCON code of conduct, FreeBSDs code of conduct, Vox Medias product team code of conduct, Mediums code of conduct, as well as adapted from the Contributor Covenant.
@@ -1,13 +1,10 @@
---
title: Contributor's Guide
permalink: /docs/development/contributing/
---
# Contributor's Guide
Welcome! We are very excited that you are interested in contributing to Talk.
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct).
By contributing to this project you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
## What should I Contribute?
@@ -33,7 +30,7 @@ Please file issues if:
### What should I include?
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repo should start with a story or this form:
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a story or this form:
> As a [type of person] I'd like to be able to [do something] so that I can [get some result].
@@ -56,15 +53,24 @@ We are looking for _documentarians_ to:
* create new / missing sections, and
* take the lead in making sections, or the over all structure better.
Information about how to update docs can be found in our [FAQ]({{ "/docs/faq" | absolute_url }}#how-do-i-contribute-to-these-docs).
Our documentation is stored in markdown files in the [docs](docs) directory. We
use Jekyll to provide our docs. To preview:
```shell
cd docs
bundle install
bundle exec jekyll serve
```
Then visit http://127.0.0.1:4000/talk/.
If you'd like to discuss a contribution, please [file an issue](https://github.com/coralproject/talk/issues) describing the changes you would like to see.
## Contributing Translations
Talk's tranlations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
Talk's translations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repo.
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repository.
## I want to contribute but I'm not sure what to do!
@@ -84,4 +90,4 @@ Have a favorite analytics engine? Data science service? CMS? Auth platform? Depl
### Favorite Features?
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
+7 -17
View File
@@ -1,29 +1,19 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
Third party licenses are available via the `/client/3rdpartylicenses.txt`
endpoint when the server is running with built assets.
## Documentation
## Try Talk!
Developer Documentation & Setup Guides https://coralproject.github.io/talk/.
- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/ (includes Installation Guide, Quickstart, Plugin Guide, API Docs, and more)
## Roadmap and Release Schedule
- Talk Roadmap: https://www.pivotaltracker.com/n/projects/1863625
## Learn More about Coral
## Relevant Links
- Blog: https://blog.coralproject.net/
- Community Forums: https://community.coralproject.net/
- Website: https://coralproject.net
- Blog: https://blog.coralproject.net
- Community Guides for Journalism: https://guides.coralproject.net/
- Project: https://coralproject.net/
- Roadmap: https://www.pivotaltracker.com/n/projects/1863625
## License
+5 -13
View File
@@ -6,13 +6,9 @@ const merge = require('lodash/merge');
const helmet = require('helmet');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const {
BASE_URL,
BASE_PATH,
MOUNT_PATH,
STATIC_URL,
HELMET_CONFIGURATION,
} = require('./url');
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');
@@ -57,12 +53,8 @@ app.set('view engine', 'ejs');
// ROUTES
//==============================================================================
// Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will
// make them available on the templates and the routers.
app.locals.BASE_URL = BASE_URL;
app.locals.BASE_PATH = BASE_PATH;
app.locals.MOUNT_PATH = MOUNT_PATH;
app.locals.STATIC_URL = STATIC_URL;
// Add the locals to the app renderer.
applyLocals(app.locals);
debug(`mounting routes on the ${MOUNT_PATH} path`);
+1 -22
View File
@@ -1,24 +1,3 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
"extends": "@coralproject/eslint-config-talk/client"
}
+2 -2
View File
@@ -6,7 +6,7 @@ import Configure from 'routes/Configure';
import Dashboard from 'routes/Dashboard';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import {CommunityLayout, Community} from 'routes/Community';
import Community from 'routes/Community/containers/Community';
import {ModerationLayout, Moderation} from 'routes/Moderation';
import Layout from 'containers/Layout';
@@ -22,7 +22,7 @@ const routes = (
{/* Community Routes */}
<Route path='community' component={CommunityLayout}>
<Route path='community'>
<Route path='flagged' components={Community}>
<Route path=':id' components={Community} />
</Route>
+2 -3
View File
@@ -1,4 +1,4 @@
import qs from 'qs';
import queryString from 'query-string';
import {
FETCH_COMMENTERS_REQUEST,
@@ -17,9 +17,8 @@ import {
import t from 'coral-framework/services/i18n';
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
dispatch(requestFetchAccounts());
rest(`/users?${qs.stringify(query)}`)
rest(`/users?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>{
dispatch({
type: FETCH_COMMENTERS_SUCCESS,
+1 -1
View File
@@ -4,7 +4,7 @@ export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, user
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const changeUserDetailStatuses = (tab) => {
let statuses = [];
let statuses = null;
if (tab === 'rejected') {
statuses = ['REJECTED'];
}
@@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ModerationList.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
const ActionButton = ({type = '', active, ...props}) => {
const typeName = type.toLowerCase();
let text = menuActionsMap[type].text;
if (text === 'approve' && active) {
text = 'approved';
} else if (text === 'reject' && active) {
text = 'rejected';
}
return (
<Button
className={`${typeName} ${styles.actionButton} ${props.minimal ? styles.minimal : ''} ${active ? styles[`${typeName}__active`] : ''}`}
cStyle={typeName}
icon={menuActionsMap[type].icon}
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
);
};
ActionButton.propTypes = {
active: PropTypes.bool,
type: PropTypes.oneOf(['APPROVE', 'REJECT']),
minimal: PropTypes.bool,
acceptComment: PropTypes.func,
rejectComment: PropTypes.func,
};
export default ActionButton;
@@ -0,0 +1,44 @@
.root {
display: block;
color: #519954;
border: solid 2px rgba(81, 153, 84, 0.75);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
&:hover {
box-shadow: none;
color: white;
background-color: #519954;
cursor: pointer;
}
}
.active {
box-shadow: none;
color: white;
background-color: #519954;
&:hover {
cursor: not-allowed;
}
}
.minimal {
width: 45px;
min-width: 0;
}
.icon {
margin-right: 5px;
}
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick}) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
{!minimal && text}
</button>
);
};
ApproveButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
};
export default ApproveButton;
@@ -0,0 +1,18 @@
.root {
min-height: 25px;
padding: 0 16px;
}
.moreDetail {
position: absolute;
font-size: 12px;
font-weight: 500;
color: black;
right: 16px;
&:hover {
opacity: 0.9;
cursor: pointer;
}
}
@@ -0,0 +1,66 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
import Slot from 'coral-framework/components/Slot';
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
class CommentDetails extends Component {
state = {
showDetail: false
};
constructor () {
super();
this.state = {
showDetail: false
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
}));
}
render() {
const {data, root, comment} = this.props;
const {showDetail} = this.state;
const queryData = {
root,
comment,
};
return (
<div className={styles.root}>
<IfSlotIsNotEmpty
queryData={queryData}
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
>
<a onClick={this.toggleDetail} className={styles.moreDetail}>
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
</a>
</IfSlotIsNotEmpty>
<Slot
fill="adminCommentDetailArea"
data={data}
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
queryData={queryData}
/>}
</div>
);
}
}
CommentDetails.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
};
export default CommentDetails;
@@ -1,83 +0,0 @@
.flagBox {
border-top: 1px solid rgba(66, 66, 66, 0.12);
margin-top: 10px;
.container {
padding: 0 14px;
}
.detail {
padding: 0 20px 16px;
ul {
padding: 0;
list-style: none;
font-size: 12px;
font-weight: 500;
}
}
.header {
position: relative;
.moreDetail {
float: right;
font-size: 12px;
font-weight: 500;
margin-right: 10px;
margin-top: 8px;
color: black;
&:hover {
opacity: 0.9;
cursor: pointer;
}
}
i {
vertical-align: middle;
font-size: 12px;
}
ul {
vertical-align: middle;
list-style: none;
display: inline-block;
padding: 0;
margin-left: 10px;
font-size: 12px;
}
}
h3 {
vertical-align: middle;
margin: 0;
font-weight: 500;
display: inline-block;
margin-left: 7px;
font-size: 12px;
}
}
.lessDetail {
display: inline-block;
margin-right: 10px;
}
.subDetail {
font-weight: normal;
color: #888;
span {
color: black;
}
}
.username {
color: #393B44;
text-decoration: none;
cursor: pointer;
font-weight: 600;
padding: 2px 5px;
border-radius: 2px;
margin-left: -5px;
transition: background-color 200ms ease;
&:hover {
background-color: #E0E0E0;
}
}
@@ -1,97 +0,0 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Icon} from 'coral-ui';
import styles from './FlagBox.css';
import t from 'coral-framework/services/i18n';
const shortReasons = {
'This comment is offensive': t('modqueue.offensive'),
'This looks like an ad/marketing': t('modqueue.spam_ads'),
'This user is impersonating': t('modqueue.impersonating'),
'I don\'t like this username': t('modqueue.dont_like_username'),
'Other': t('modqueue.other')
};
class FlagBox extends Component {
constructor () {
super();
this.state = {
showDetail: false
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
}));
}
reasonMap = (reason) => {
const shortReason = shortReasons[reason];
// if the short reason isn't found, just return the long one.
return shortReason ? shortReason : reason;
}
render() {
const {actionSummaries, actions, viewUserDetail} = this.props;
const {showDetail} = this.state;
return (
<div className={styles.flagBox}>
<div className={styles.container}>
<div className={styles.header}>
<Icon name='flag'/><h3>{t('community.flags')} ({actionSummaries.length}):</h3>
<ul>
{actionSummaries.map((action, i) =>
<li key={i} className={styles.lessDetail}> {this.reasonMap(action.reason)} (<strong>{action.count}</strong>)</li>
)}
</ul>
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}</a>
</div>
{showDetail && (
<div className={styles.detail}>
<ul>
{actionSummaries.map((summary, i) => {
const actionList = actions.filter((a) => a.reason === summary.reason);
return (
<li key={i}>
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
<ul>
{actionList.map((action, j) =>
<li key={`${i}_${j}`} className={styles.subDetail}>
{action.user &&
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
{action.user.username}
</a>
}
{action.message}
</li>
)}
</ul>
</li>
);
})}
</ul>
</div>
)}
</div>
</div>
);
}
}
FlagBox.propTypes = {
actionSummaries: PropTypes.arrayOf(PropTypes.shape({
reason: PropTypes.string,
count: PropTypes.number
})).isRequired,
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired
};
export default FlagBox;
@@ -1,208 +0,0 @@
@custom-media --big-viewport (min-width: 780px);
.list {
padding: 8px 0;
list-style: none;
display: block;
&.singleView .listItem {
display: none;
}
&.singleView .listItem.activeItem {
display: block;
height: 100%;
font-size: 1.5em;
line-height: 1.5em;
border: none;
.actions {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
margin: 0;
}
.actionButton {
transform: scale(1.4);
}
}
}
.listItem {
border-bottom: 1px solid #e0e0e0;
font-size: 16px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
padding: 16px 14px;
position: relative;
transition: box-shadow 200ms;
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
&:last-child {
border-bottom: none;
}
.sideActions {
position: absolute;
right: 0;
height: 100%;
top: 0;
padding: 40px 18px;
box-sizing: border-box;
}
.itemHeader {
display: flex;
align-items: center;
justify-content: space-between;
.author {
min-width: 230px;
display: flex;
align-items: center;
}
}
.itemBody {
display: flex;
justify-content: space-between;
}
.avatar {
margin-right: 16px;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: #757575;
font-size: 40px;
color: #fff;
}
.created {
color: #666;
font-size: 13px;
margin-left: 40px;
}
.body {
margin-top: 20px;
flex: 1;
font-size: 0.88em;
color: black;
}
.flagged {
color: rgba(255, 0, 0, .5);
padding-top: 15px;
padding-left: 10px;
}
.flagCount{
font-size: 12px;
color: #d32f2f;
}
}
.empty {
color: #444;
margin-top: 50px;
text-align: center;
}
@media (--big-viewport) {
.listItem {
border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
&.activeItem {
border: 2px solid #333;
}
}
}
.hasLinks {
color: #f00;
text-align: right;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.banned {
color: #f00;
text-align: left;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.ban {
display: block;
text-align: center;
margin-top: 5px;
}
.banButton {
width: 114px;
letter-spacing: 1px;
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
}
}
.selected {
border-radius: 10px;
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.minimal {
width: 45px;
min-width: 0;
}
.approve__active {
box-shadow: none;
color: white;
background-color: #519954;
cursor: not-allowed;
}
.reject__active, .rejected__active {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
@@ -1,228 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ModerationList.css';
import key from 'keymaster';
import Hammer from 'hammerjs';
import Comment from './Comment';
import User from './User';
import SuspendUserModal from './SuspendUserModal';
// Each action has different meaning and configuration
const menuOptionsMap = {
'reject': {status: 'REJECTED', icon: 'close', key: 'f'},
'approve': {status: 'ACCEPTED', icon: 'done', key: 'd'},
'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
'ban': {status: 'BANNED', icon: 'not interested'}
};
// Renders a comment list and allow performing actions
export default class ModerationList extends React.Component {
static propTypes = {
isActive: PropTypes.bool,
singleView: PropTypes.bool,
commentIds: PropTypes.arrayOf(PropTypes.string),
actionIds: PropTypes.arrayOf(PropTypes.string),
comments: PropTypes.object,
users: PropTypes.object.isRequired,
actions: PropTypes.object,
userStatusUpdate: PropTypes.func.isRequired,
suspendUser: PropTypes.func.isRequired,
// list of actions (flags, etc) associated with the comments
modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
loading: PropTypes.bool,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
}
state = {active: null, suspendUserModal: null, email: null};
// remove key handlers before leaving
componentWillUnmount () {
this.unbindKeyHandlers();
}
// add key handlers and gestures
componentDidMount () {
this.bindKeyHandlers();
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
}
// If entering to singleview and no active, active is the first eleement
componentWillReceiveProps (nextProps) {
if (nextProps.singleView && !this.state.active) {
this.setState({active: nextProps.commentIds[0]});
}
}
// Add swipe to approve or reject
bindGestures () {
const {modActions} = this.props;
this._hammer = new Hammer(this.base);
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
if (modActions.indexOf('reject') !== -1) {
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
}
if (modActions.indexOf('approve') !== -1) {
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
}
}
// Add key handlers. Each action has one and added j/k for moving around
bindKeyHandlers () {
const {modActions, isActive} = this.props;
modActions.filter((action) => menuOptionsMap[action].key).forEach((action) => {
key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status));
});
key('j', 'moderationList', () => isActive && this.moveKeyHandler('down'));
key('k', 'moderationList', () => isActive && this.moveKeyHandler('up'));
key.setScope('moderationList');
}
// Perform an action using the keys only if the comment is active
actionKeyHandler (action) {
if (this.props.isActive && this.state.active) {
this.onClickAction(action, this.state.active);
}
}
// move around with j/k
moveKeyHandler (direction) {
if (!this.props.isActive) {
return;
}
const {commentIds} = this.props;
const {active} = this.state;
// check boundaries
if (active === null || !commentIds.length) {
this.setState({active: commentIds[0]});
} else if (direction === 'up' && active !== commentIds[0]) {
this.setState({active: commentIds[commentIds.indexOf(active) - 1]});
} else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) {
this.setState({active: commentIds[commentIds.indexOf(active) + 1]});
}
// scroll to the position
const index = Math.max(commentIds.indexOf(this.state.active), 0);
this.base.childNodes[index] && this.base.childNodes[index].focus();
}
unbindKeyHandlers () {
key.deleteScope('moderationList');
}
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
// TODO: In the future this can be improved and look at the actual state to
// resolve since the content of the list could change externally. For now it works as expected
onClickAction = (menuOption, id, action) => {
// activate the next comment
if (id === this.state.active) {
const moderationIds = this.getModerationIds();
if (moderationIds[moderationIds.length - 1] === this.state.active) {
this.setState({active: moderationIds[moderationIds.length - 2]});
} else {
this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]});
}
}
// Update the status right away if this is a comment
if (action.item_type === 'COMMENTS') {
this.props.updateCommentStatus(menuOption, id);
} else if (action.item_type === 'USERS') {
// If a user bio or name is rejected, bring up a dialog before suspending them.
if (menuOption === 'REJECTED') {
this.setState({suspendUserModal: action});
} else if (menuOption === 'ACCEPTED') {
this.props.userStatusUpdate('APPROVED', action.item_id);
}
}
}
onClickShowBanDialog = (userId, userName, commentId) => {
this.props.onClickShowBanDialog(userId, userName, commentId);
}
mapModItems = (itemId, index) => {
const {comments = {}, users, actions = {}, modActions, suspectWords, hideActive} = this.props;
const {active} = this.state;
// Because ids are unique, the id will either appear as an action or as a comment.
const item = comments[itemId] || actions[itemId];
let modItem;
if (item.body) {
// If the item is a comment...
const author = users[item.author_id];
modItem = <Comment
suspectWords={suspectWords}
comment={item}
author={author}
key={index}
index={index}
onClickAction={this.onClickAction}
onClickShowBanDialog={this.onClickShowBanDialog}
modActions={modActions}
menuOptionsMap={menuOptionsMap}
isActive={itemId === active}
hideActive={hideActive} />;
} else {
// If the item is an action...
const user = users[item.item_id];
modItem = user && <User
suspectWords={suspectWords}
action={item}
user={user}
key={index}
index={index}
onClickAction={this.onClickAction}
onClickShowBanDialog={this.onClickShowBanDialog}
modActions={modActions}
menuOptionsMap={menuOptionsMap}
isActive={itemId === active}
hideActive={hideActive} />;
}
return modItem;
}
getModerationIds = () => {
const {commentIds = [], actionIds = [], comments, actions} = this.props;
if (comments && actions) {
return [ ...commentIds, ...actionIds ].sort((a, b) => {
const itemA = comments[a] || actions[a];
const itemB = comments[b] || actions[b];
return itemB.updated_at - itemA.updated_at;
});
} else {
return comments ? commentIds : actionIds;
}
}
render () {
const {singleView, key, suspendUser} = this.props;
// Combine moderations and actions into a single stream and sort by most recently updated.
const moderationIds = this.getModerationIds();
return (
<ul
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
id='moderationList'>
{moderationIds.map(this.mapModItems)}
<SuspendUserModal
action = {this.state.suspendUserModal}
onClose={() => this.setState({suspendUserModal:null})}
suspendUser={suspendUser} />
</ul>
);
}
}
@@ -0,0 +1,44 @@
.root {
display: block;
color: #D03235;
border: solid 1px #D03235;
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: pointer;
}
}
.active {
color: white;
background-color: #D03235;
box-shadow: none;
&:hover {
cursor: not-allowed;
}
}
.minimal {
width: 45px;
min-width: 0;
}
.icon {
margin-right: 5px;
}
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick}) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
{!minimal && text}
</button>
);
};
RejectButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
};
export default RejectButton;
@@ -5,7 +5,6 @@ import styles from './UserDetail.css';
import {Icon, Button, Drawer, Spinner} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import {actionsMap} from '../utils/moderationQueueActionsMap';
import ClickOutside from 'coral-framework/components/ClickOutside';
import LoadMore from '../components/LoadMore';
import cn from 'classnames';
@@ -70,6 +69,7 @@ export default class UserDetail extends React.Component {
renderLoaded() {
const {
data,
root,
root: {
user,
@@ -177,15 +177,15 @@ export default class UserDetail extends React.Component {
<div>
{
nodes.map((comment) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
suspectWords={suspectWords}
bannedWords={bannedWords}
actions={actionsMap[status]}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
@@ -1,4 +1,5 @@
.root {
position: relative;
display: block;
margin: 0;
border-bottom: 1px solid #e0e0e0;
@@ -3,37 +3,41 @@ import PropTypes from 'prop-types';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import CommentAnimatedEdit from './CommentAnimatedEdit';
import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
render() {
const {
actions = [],
comment,
viewUserDetail,
suspectWords,
bannedWords,
selected,
toggleSelect,
className,
user,
...props
data,
} = this.props;
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
return (
<li
tabIndex={0}
@@ -66,7 +70,7 @@ class UserDetailComment extends React.Component {
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
<p className={styles.body}>
<div className={styles.body}>
<CommentBodyHighlighter
suspectWords={suspectWords}
bannedWords={bannedWords}
@@ -80,7 +84,7 @@ class UserDetailComment extends React.Component {
>
<Icon name="open_in_new" /> {t('comment.view_context')}
</a>
</p>
</div>
<div className={styles.sideActions}>
<IfHasLink text={comment.body}>
<span className={styles.hasLinks}>
@@ -88,41 +92,26 @@ class UserDetailComment extends React.Component {
</span>
</IfHasLink>
<div className={styles.actions}>
{actions.map((action, i) => {
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
minimal={true}
key={i}
type={action}
user={user}
status={comment.status}
active={active}
acceptComment={() =>
(comment.status === 'ACCEPTED'
? null
: props.acceptComment({commentId: comment.id}))}
rejectComment={() =>
(comment.status === 'REJECTED'
? null
: props.rejectComment({commentId: comment.id}))}
/>
);
})}
<ApproveButton
active={comment.status === 'ACCEPTED'}
onClick={this.approve}
minimal
/>
<RejectButton
active={comment.status === 'REJECTED'}
onClick={this.reject}
minimal
/>
</div>
</div>
</div>
</CommentAnimatedEdit>
</div>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
<CommentDetails
data={data}
root={root}
comment={comment}
/>
</li>
);
}
@@ -138,8 +127,9 @@ UserDetailComment.propTypes = {
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
toggleSelect: PropTypes.func,
comment: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
actions: PropTypes.array,
created_at: PropTypes.string.isRequired,
asset: PropTypes.shape({
+95 -86
View File
@@ -6,101 +6,110 @@ import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import {Logo} from './Logo';
import {can} from 'coral-framework/services/perms';
import Indicator from './Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth
}) => (
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
<IndexLink
id='dashboardNav'
className={styles.navLink}
to="/admin/dashboard"
activeClassName={styles.active}>
{t('configure.dashboard')}
</IndexLink>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<Link
id='moderateNav'
className={styles.navLink}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</Link>
)
}
<Link
id='streamsNav'
className={styles.navLink}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
<Link
id='communityNav'
className={styles.navLink}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') && (
<Link
id='configureNav'
className={styles.navLink}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
</Navigation>
:
null
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
auth,
root
}) => {
return (
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
<IndexLink
id='dashboardNav'
className={styles.navLink}
to="/admin/dashboard"
activeClassName={styles.active}>
{t('configure.dashboard')}
</IndexLink>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<Link
id='moderateNav'
className={styles.navLink}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
</Link>
)
}
<Link
id='streamsNav'
className={styles.navLink}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
<Link
id='communityNav'
className={styles.navLink}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') && (
<Link
id='configureNav'
className={styles.navLink}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
</Navigation>
:
null
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</div>
</div>
</Header>
);
</Header>
);
};
CoralHeader.propTypes = {
auth: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired
};
export default CoralHeader;
@@ -0,0 +1,10 @@
.indicator {
background-color: #E46D59;
border-radius: 10px;
position: absolute;
top: 50%;
width: 7px;
height: 7px;
margin-top: -4px;
margin-left: 7px;
}
@@ -0,0 +1,7 @@
import React from 'react';
import styles from './Indicator.css';
const Indicator = () =>
<span className={styles.indicator}></span>;
export default Indicator;
+12 -5
View File
@@ -1,22 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Layout as LayoutMDL} from 'react-mdl';
import Header from './Header';
import Header from '../../containers/Header';
import Drawer from './Drawer';
import styles from './Layout.css';
const Layout = ({
children,
handleLogout = () => {},
toggleShortcutModal,
toggleShortcutModal = () => {},
restricted = false,
...props}) => (
auth,
}) => (
<LayoutMDL className={styles.layout} fixedDrawer>
<Header
handleLogout={handleLogout}
showShortcuts={toggleShortcutModal}
{...props} />
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
auth={auth}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
/>
<div className={styles.layout}>
{children}
</div>
@@ -24,6 +29,8 @@ const Layout = ({
);
Layout.propTypes = {
children: PropTypes.node,
auth: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
@@ -1,6 +1,7 @@
import React from 'react';
import styles from './Logo.css';
import {CoralLogo} from 'coral-ui';
import PropTypes from 'prop-types';
export const Logo = ({className = ''}) => (
<div className={`${styles.logo} ${className}`}>
@@ -10,3 +11,7 @@ export const Logo = ({className = ''}) => (
</h1>
</div>
);
Logo.propTypes = {
className: PropTypes.string
};
@@ -0,0 +1,24 @@
import {gql} from 'react-apollo';
import CommentDetails from '../components/CommentDetails';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import withFragments from 'coral-framework/hocs/withFragments';
const slots = [
'adminCommentDetailArea',
'adminCommentMoreDetails',
];
export default withFragments({
root: gql`
fragment CoralAdmin_CommentDetails_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
comment: gql`
fragment CoralAdmin_CommentDetails_comment on Comment {
__typename
${getSlotFragmentSpreads(slots, 'comment')}
}
`
})(CommentDetails);
@@ -8,6 +8,12 @@ const slots = [
];
export default withFragments({
root: gql`
fragment CoralAdmin_CommentLabels_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
comment: gql`
fragment CoralAdmin_CommentLabels_comment on Comment {
hasParent
@@ -0,0 +1,24 @@
import {gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/ui/Header';
export default withQuery(gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: {
statuses: [PREMOD]
})
reportedCount: commentCount(query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
action_type: FLAG
})
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
statuses: [PENDING]
})
}
`, {
options: {
pollInterval: 5000
}
})(Header);
+38 -18
View File
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import Layout from '../components/ui/Layout';
import {fetchConfig} from '../actions/config';
import AdminLogin from '../components/AdminLogin';
@@ -10,15 +11,18 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
import PropTypes from 'prop-types';
class LayoutContainer extends Component {
class LayoutContainer extends React.Component {
componentWillMount() {
const {checkLogin, fetchConfig} = this.props;
checkLogin();
fetchConfig();
}
render() {
const {
user,
loggedIn,
@@ -29,13 +33,16 @@ class LayoutContainer extends Component {
} = this.props.auth;
const {
handleLogout,
children,
logout,
toggleShortcutModal,
TALK_RECAPTCHA_PUBLIC
TALK_RECAPTCHA_PUBLIC,
} = this.props;
if (loadingUser) {
return <FullLoading />;
}
if (!loggedIn) {
return (
<AdminLogin
@@ -48,17 +55,17 @@ class LayoutContainer extends Component {
/>
);
}
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
return (
<Layout
handleLogout={handleLogout}
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
{...this.props}
>
auth={this.props.auth} >
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{this.props.children}
{children}
</Layout>
);
} else if (loggedIn) {
@@ -72,19 +79,32 @@ class LayoutContainer extends Component {
}
}
LayoutContainer.propTypes = {
children: PropTypes.node,
requestPasswordReset: PropTypes.func,
handleLogin: PropTypes.func,
auth: PropTypes.object,
handleLogout: PropTypes.func,
logout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
checkLogin: PropTypes.func,
fetchConfig: PropTypes.func
};
const mapStateToProps = (state) => ({
auth: state.auth,
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
});
const mapDispatchToProps = (dispatch) => ({
checkLogin: () => dispatch(checkLogin()),
fetchConfig: () => dispatch(fetchConfig()),
handleLogin: (username, password, recaptchaResponse) =>
dispatch(handleLogin(username, password, recaptchaResponse)),
requestPasswordReset: (email) => dispatch(requestPasswordReset(email)),
toggleShortcutModal: (toggle) => dispatch(toggleShortcutModal(toggle)),
handleLogout: () => dispatch(logout())
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -160,8 +160,10 @@ export const withUserDetailQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
...${getDefinitionName(UserDetailComment.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
}
${UserDetailComment.fragments.root}
${commentConnectionFragment}
`, {
options: ({userId, statuses}) => {
@@ -3,10 +3,20 @@ import UserDetailComment from '../components/UserDetailComment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getDefinitionName} from 'coral-framework/utils';
import CommentLabels from './CommentLabels';
import CommentDetails from './CommentDetails';
export default withFragments({
root: gql`
fragment CoralAdmin_UserDetailComment_root on RootQuery {
__typename
...${getDefinitionName(CommentLabels.fragments.root)}
...${getDefinitionName(CommentDetails.fragments.root)}
}
${CommentLabels.fragments.root}
${CommentDetails.fragments.root}
`,
comment: gql`
fragment CoralAdmin_UserDetail_comment on Comment {
fragment CoralAdmin_UserDetailComment_comment on Comment {
id
body
created_at
@@ -17,28 +27,13 @@ export default withFragments({
title
url
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
}
}
editing {
edited
}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
})(UserDetailComment);
+2 -2
View File
@@ -4,7 +4,7 @@ export default {
mutations: {
SetUserStatus: ({variables: {status, userId}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
TalkAdmin_Community: (prev) => {
if (status !== 'APPROVED') {
return prev;
}
@@ -19,7 +19,7 @@ export default {
}),
RejectUsername: ({variables: {input: {id: userId}}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
@@ -3,7 +3,7 @@ import * as actions from '../constants/userDetail';
const initialState = {
userId: null,
activeTab: 'all',
statuses: [],
statuses: null,
selectedCommentIds: [],
};
@@ -1,4 +0,0 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
@@ -1,22 +0,0 @@
import React from 'react';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
// TODO: Needs refactoring.
const ActionButton = ({type = '', user, ...props}) => {
return (
<Button
className={`${type.toLowerCase()} ${styles.actionButton}`}
cStyle={type.toLowerCase()}
icon={menuActionsMap[type].icon}
onClick={() => {
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showRejectUsernameDialog({user: user});
}}
>{t(`modqueue.${menuActionsMap[type].text}`)}</Button>
);
};
export default ActionButton;
@@ -1,9 +0,0 @@
import React from 'react';
const CommunityLayout = (props) => (
<div>
{props.children}
</div>
);
export default CommunityLayout;
@@ -47,7 +47,6 @@ class FlaggedAccounts extends React.Component {
<FlaggedUser
user={user}
key={user.id}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
showRejectUsernameDialog={showRejectUsernameDialog}
@@ -1,13 +1,13 @@
import React from 'react';
import styles from './FlaggedUser.css';
import ActionButton from './ActionButton';
// TODO: Should not rely on plugin.
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
@@ -35,15 +35,16 @@ class User extends React.Component {
});
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
approveUser = () => this.props.approveUser({
userId: this.props.user.id,
});
render() {
const {
user,
modActionButtons,
viewUserDetail,
selected,
approveUser,
showRejectUsernameDialog,
me,
className,
} = this.props;
@@ -125,14 +126,12 @@ class User extends React.Component {
</div>
<div className={styles.sideActions}>
<div className={styles.actions}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={approveUser}
showRejectUsernameDialog={showRejectUsernameDialog}
/>
)}
<ApproveButton
onClick={this.approveUser}
/>
<RejectButton
onClick={this.showRejectUsernameDialog}
/>
</div>
</div>
</div>
@@ -51,7 +51,7 @@ class RejectUsernameDialog extends Component {
const next = () => this.setState({stage: stage + 1});
const suspend = async () => {
try {
await rejectUsername({id: user.user.id, message: this.state.email});
await rejectUsername({id: user.id, message: this.state.email});
this.props.handleClose();
} catch (err) {
@@ -1,16 +1,14 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import PropTypes from 'prop-types';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import PropTypes from 'prop-types';
import {getDefinitionName} from 'coral-framework/utils';
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
import {getDefinitionName} from 'coral-framework/utils';
import {
fetchAccounts,
updateSorting,
@@ -27,7 +25,7 @@ class CommunityContainer extends Component {
}
render() {
return <Community
return <Community
fetchAccounts={this.props.fetchAccounts}
community={this.props.community}
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
@@ -56,28 +54,6 @@ CommunityContainer.propTypes = {
root: PropTypes.object
};
const withData = withQuery(gql`
query TalkAdmin_FlaggedUsernamesCount {
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
statuses: [PENDING]
})
...${getDefinitionName(FlaggedAccounts.fragments.root)}
...${getDefinitionName(FlaggedUser.fragments.root)}
me {
...${getDefinitionName(FlaggedUser.fragments.me)}
__typename
}
}
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`, {
options: {
fetchPolicy: 'network-only',
},
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
fetchAccounts,
@@ -86,9 +62,31 @@ const mapDispatchToProps = (dispatch) =>
newPage,
}, dispatch);
const withData = withQuery(gql`
query TalkAdmin_Community {
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
statuses: [PENDING]
})
...${getDefinitionName(FlaggedAccounts.fragments.root)}
...${getDefinitionName(FlaggedUser.fragments.root)}
me {
...${getDefinitionName(FlaggedUser.fragments.me)}
__typename
}
}
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`, {
options: {
fetchPolicy: 'network-only',
},
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withSetUserStatus,
withRejectUsername,
withData,
withData
)(CommunityContainer);
@@ -1,2 +0,0 @@
export {default as Community} from './containers/Community';
export {default as CommunityLayout} from './components/CommunityLayout';
@@ -0,0 +1,181 @@
@custom-media --big-viewport (min-width: 780px);
.root {
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 650px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: all 200ms;
padding: 10px 0;
min-height: 0;
/*
Fix rendering issues in Safari by promoting this
into its own layer.
https://www.pivotaltracker.com/story/show/151142211
*/
transform: translateZ(0);
&:last-child {
border-bottom: none;
}
}
.container {
padding: 0 14px;
}
.itemHeader {
display: flex;
align-items: center;
justify-content: space-between;
}
.author {
font-weight: 300;
width: 100%;
display: flex;
align-items: center;
color: #262626;
font-size: 16px;
position: relative;
}
.sideActions {
height: 100%;
top: 0;
box-sizing: border-box;
}
.itemBody {
display: flex;
justify-content: space-between;
font-size: 14px;
line-height: 1.5;
font-weight: 300;
margin-bottom: 8px;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
font-size: 16px;
}
.created {
padding: 5px;
color: #262626;
font-size: 14px;
line-height: 1px;
font-weight: 300;
}
.moderateArticle {
font-size: 14px;
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
max-width: 500px;
a {
display: inline-block;
color: #063b9a;
text-decoration: none;
font-weight: 500;
letter-spacing: .5px;
margin-left: 10px;
font-size: 13px;
margin-left: 5px;
padding-bottom: 0px;
border-bottom: solid 1px;
line-height: 16px;
&:hover {
opacity: .9;
cursor: pointer;
}
}
}
.username {
color: #393B44;
text-decoration: none;
cursor: pointer;
font-weight: 600;
padding: 2px 5px;
border-radius: 2px;
margin-left: -5px;
transition: background-color 200ms ease;
&:hover {
background-color: #E0E0E0;
}
}
.external {
font-size: .7em;
text-decoration: none;
color: #063b9a;
cursor: pointer;
font-weight: normal;
margin-left: 10px;
white-space: nowrap;
&:hover {
text-decoration: underline;
opacity: .9;
}
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
.editedMarker {
font-style: italic;
color: #666;
font-size: 12px;
line-height: 1px;
font-weight: 300;
}
.adminCommentInfoBar {
min-width: 100px;
position: absolute;
right: 0px;
top: 0px;
text-align: right;
}
.hasLinks {
color: #f00;
display: flex;
align-items: center;
justify-content: flex-end;
margin-right: 14px;
i {
margin-right: 5px;
}
}
@media (--big-viewport) {
.root {
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
}
}
@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import FlagBox from 'coral-admin/src/components/FlagBox';
import styles from './styles.css';
import CommentDetails from 'coral-admin/src/components/CommentDetails';
import styles from './Comment.css';
import CommentLabels from 'coral-admin/src/components/CommentLabels';
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
@@ -45,11 +45,19 @@ class Comment extends React.Component {
return viewUserDetail(comment.user.id);
};
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
render() {
const {
actions = [],
comment,
viewUserDetail,
suspectWords,
bannedWords,
selected,
@@ -58,21 +66,16 @@ class Comment extends React.Component {
root,
currentUserId,
currentAsset,
acceptComment,
rejectComment,
} = this.props;
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const queryData = {root, comment, asset: comment.asset};
return (
<li
tabIndex={0}
className={cn(className, 'mdl-card', selectionStateCSS, styles.Comment, styles.listItem, {[styles.selected]: selected})}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected})}
id={`comment_${comment.id}`}
>
<div className={styles.container}>
<div className={styles.itemHeader}>
@@ -152,28 +155,14 @@ class Comment extends React.Component {
</span>
</IfHasLink>
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) => {
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
key={i}
type={action}
user={comment.user}
status={comment.status}
active={active}
acceptComment={() =>
(comment.status === 'ACCEPTED'
? null
: acceptComment({commentId: comment.id}))}
rejectComment={() =>
(comment.status === 'REJECTED'
? null
: rejectComment({commentId: comment.id}))}
/>
);
})}
<ApproveButton
active={comment.status === 'ACCEPTED'}
onClick={this.approve}
/>
<RejectButton
active={comment.status === 'REJECTED'}
onClick={this.reject}
/>
</div>
<Slot
fill="adminSideActions"
@@ -184,18 +173,11 @@ class Comment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<Slot
fill="adminCommentDetailArea"
<CommentDetails
data={data}
queryData={queryData}
root={root}
comment={comment}
/>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
);
}
@@ -213,6 +195,8 @@ Comment.propTypes = {
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
comment: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
actions: PropTypes.array,
@@ -225,8 +209,11 @@ Comment.propTypes = {
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
})
})
}),
}),
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
selected: PropTypes.bool,
};
export default Comment;
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import key from 'keymaster';
import styles from './styles.css';
import ModerationQueue from './ModerationQueue';
import ModerationMenu from './ModerationMenu';
@@ -9,12 +9,13 @@ import ModerationKeysModal from '../../../components/ModerationKeysModal';
import StorySearch from '../containers/StorySearch';
import Slot from 'coral-framework/components/Slot';
export default class Moderation extends Component {
constructor() {
super();
class Moderation extends Component {
constructor(props) {
super(props);
const comments = this.getComments(props);
this.state = {
selectedIndex: 0
selectedCommentId: comments[0] ? comments[0].id : null,
};
}
@@ -25,10 +26,10 @@ export default class Moderation extends Component {
key('s', () => singleView());
key('shift+/', () => toggleModal(true));
key('esc', () => toggleModal(false));
key('j', this.select(true));
key('k', this.select(false));
key('f', this.moderate(false));
key('d', this.moderate(true));
key('j', () => this.select(true));
key('k', () => this.select(false));
key('f', () => this.moderate(false));
key('d', () => this.moderate(true));
}
onClose = () => {
@@ -44,39 +45,102 @@ export default class Moderation extends Component {
this.props.toggleStorySearch(true);
}
moderate = (accept) => () => {
const {acceptComment, rejectComment} = this.props;
const {selectedIndex} = this.state;
const comments = this.getComments();
const comment = comments[selectedIndex];
const commentId = {commentId: comment.id};
getActiveTabCount = (props = this.props) => {
return props.root[`${props.activeTab}Count`];
}
moderate = (accept) => {
const {acceptComment, rejectComment} = this.props;
const {selectedCommentId} = this.state;
const comments = this.getComments();
const commentIdx = comments.findIndex((comment) => comment.id === selectedCommentId);
const comment = comments[commentIdx];
if (accept) {
comment.status !== 'ACCEPTED' && acceptComment(commentId);
comment.status !== 'ACCEPTED' && acceptComment({commentId: comment.id});
} else {
comment.status !== 'REJECTED' && rejectComment(commentId);
comment.status !== 'REJECTED' && rejectComment({commentId: comment.id});
}
}
getComments = () => {
const {root, activeTab} = this.props;
getComments = (props = this.props) => {
const {root, activeTab} = props;
return root[activeTab].nodes;
}
select = (next) => () => {
if (next) {
this.setState((state) => ({
...state,
selectedIndex: state.selectedIndex < this.getComments().length - 1
? state.selectedIndex + 1 : state.selectedIndex
}));
} else {
this.setState((state) => ({
...state,
selectedIndex: state.selectedIndex > 0
? state.selectedIndex - 1 : state.selectedIndex
}));
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;
try {
const result = await this.props.loadMore(this.props.activeTab);
this.isLoadingMore = false;
return result;
}
catch (e) {
this.isLoadingMore = false;
throw e;
}
}
return false;
}
componentWillUnmount() {
@@ -89,25 +153,56 @@ export default class Moderation extends Component {
key.unbind('d');
}
componentDidUpdate(_, prevState) {
componentWillReceiveProps(nextProps) {
// If paging through using keybaord shortcuts, scroll the page to keep the selected
// comment in view.
if (prevState.selectedIndex !== this.state.selectedIndex) {
if (this.props.activeTab !== nextProps.activeTab) {
// the 'smooth' flag only works in FF as of March 2017
document.querySelector(`.${styles.selected}`).scrollIntoView({behavior: 'smooth'});
// 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, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
const {root, data, moderation, settings, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
const {asset} = root;
const assetId = asset && asset.id;
const comments = root[activeTab];
const activeTabCount = root[`${activeTab}Count`];
const activeTabCount = this.getActiveTabCount();
const menuItems = Object.keys(queueConfig).map((queue) => ({
key: queue,
name: queueConfig[queue].name,
@@ -132,26 +227,26 @@ export default class Moderation extends Component {
activeTab={activeTab}
/>
<ModerationQueue
key={`${activeTab}_${this.props.moderation.sortOrder}`}
data={this.props.data}
root={this.props.root}
currentAsset={asset}
comments={comments.nodes}
activeTab={activeTab}
singleView={moderation.singleView}
selectedIndex={this.state.selectedIndex}
selectedCommentId={this.state.selectedCommentId}
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
loadMore={props.loadMore}
loadMore={this.loadMore}
assetId={assetId}
sort={this.props.moderation.sortOrder}
commentCount={activeTabCount}
currentUserId={this.props.auth.user.id}
viewUserDetail={viewUserDetail}
hideUserDetail={hideUserDetail}
/>
<ModerationKeysModal
hideShortcutsNote={props.hideShortcutsNote}
@@ -177,3 +272,28 @@ export default class Moderation extends Component {
);
}
}
Moderation.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
toggleModal: PropTypes.func.isRequired,
toggleStorySearch: PropTypes.func.isRequired,
getModPath: PropTypes.func.isRequired,
storySearchChange: PropTypes.func.isRequired,
moderation: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
queueConfig: PropTypes.object.isRequired,
handleCommentChange: PropTypes.func.isRequired,
setSortOrder: PropTypes.func.isRequired,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
loadMore: PropTypes.func.isRequired,
singleView: PropTypes.func.isRequired,
activeTab: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
};
export default Moderation;
@@ -0,0 +1,39 @@
.header {
background-color: #2c2c2c;
color: white;
margin-bottom: -1px;
}
.searchTrigger {
position: relative;
top: .2em;
}
.moderateAsset {
a {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 400;
font-size: 20px;
letter-spacing: 1px;
transition: background-color 200ms;
opacity: 1;
&:hover {
cursor: pointer;
background-color: #212121;
}
span {
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 344px;
display: inline-block;
vertical-align: top;
}
}
}
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Icon} from 'coral-ui';
import styles from './styles.css';
import styles from './ModerationHeader.css';
import t from 'coral-framework/services/i18n';
const ModerationHeader = ({asset, searchVisible, openSearch, closeSearch}) => {
@@ -30,7 +30,8 @@ ModerationHeader.propTypes = {
id: PropTypes.string
}),
openSearch: PropTypes.func.isRequired,
closeSearch: PropTypes.func.isRequired
closeSearch: PropTypes.func.isRequired,
searchVisible: PropTypes.bool,
};
export default ModerationHeader;
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
const ModerationLayout = (props) => (
<div>
@@ -6,4 +7,8 @@ const ModerationLayout = (props) => (
</div>
);
ModerationLayout.propTypes = {
children: PropTypes.node,
};
export default ModerationLayout;
@@ -0,0 +1,102 @@
@custom-media --big-viewport (min-width: 780px);
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
display: flex;
justify-content: space-between;
}
.tabBarPadding {
width: 150px;
}
.tab {
flex: 1;
color: #BDBDBD;
text-transform: capitalize;
font-weight: 100;
font-size: 14px;
letter-spacing: 1px;
transition: border-bottom 200ms;
transition: color 200ms;
padding: 0px 10px;
margin-right: 20px;
&:hover {
color: white;
/*border-bottom: solid 2px #F36451;*/
box-sizing: border-box;
}
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 4px #F36451;
font-weight: 400;
&:hover {
border-bottom: solid 4px #F36451;
font-weight: 400;
}
}
.active > span {
color: white;
}
.active:after {
background: transparent !important;
}
.selectField {
position: relative;
width: 140px;
height: 36px;
top: 5px;
margin-right: 10px;
background: #FFF;
padding: 10px 15px;
box-sizing: border-box;
border-radius: 2px;
bor
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
border-bottom: 0px;
}
label {
top: -4px;
}
&:hover {
cursor: pointer;
}
}
.tabIcon {
position: relative;
top: 3px;
font-size: 16px;
}
@media (--big-viewport) {
.tab {
flex: none;
}
}
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import CountBadge from '../../../components/CountBadge';
import styles from './styles.css';
import styles from './ModerationMenu.css';
import {SelectField, Option} from 'react-mdl-selectfield';
import {Icon} from 'coral-ui';
import {Link} from 'react-router';
@@ -49,7 +49,11 @@ ModerationMenu.propTypes = {
items: PropTypes.array.isRequired,
asset: PropTypes.shape({
id: PropTypes.string
})
}),
selectSort: PropTypes.func.isRequired,
sort: PropTypes.string.isRequired,
getModPath: PropTypes.func.isRequired,
activeTab: PropTypes.string.isRequired,
};
export default ModerationMenu;
@@ -0,0 +1,30 @@
.root {
padding: 8px 0;
list-style: none;
display: block;
margin-top: 16px;
}
.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;
}
@@ -2,42 +2,55 @@ import React from 'react';
import PropTypes from 'prop-types';
import Comment from '../containers/Comment';
import styles from './styles.css';
import styles from './ModerationQueue.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
import LoadMore from '../../../components/LoadMore';
import ViewMore from './ViewMore';
import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
if (props.comments && props.comments.length) {
const idCursors = [props.comments[0].id];
if (props.comments[1]) {
idCursors.push(props.comments[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = props.comments.findIndex((node) => node.id === idCursors[0]);
const nextInLine = props.comments[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
class ModerationQueue extends React.Component {
isLoadingMore = false;
static propTypes = {
viewUserDetail: PropTypes.func.isRequired,
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
comments: PropTypes.array.isRequired
}
loadMore = () => {
if (!this.isLoadingMore) {
this.isLoadingMore = true;
this.props.loadMore(this.props.activeTab)
.then(() => this.isLoadingMore = false)
.catch((e) => {
this.isLoadingMore = false;
throw e;
});
}
}
constructor(props) {
super(props);
this.state = {
...resetCursors(this.state, props),
};
}
componentDidUpdate (prev) {
@@ -47,14 +60,67 @@ class ModerationQueue extends React.Component {
// 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.loadMore();
this.props.loadMore();
}
}
componentWillReceiveProps(next) {
const {comments: prevComments} = this.props;
const {comments: nextComments} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
}
if (
prevComments && nextComments &&
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));
}
// 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));
}
}
}
viewNewComments = () => {
this.setState(resetCursors);
};
// getVisibileComments returns a list containing comments
// which comes after the `idCursor`.
getVisibleComments() {
const {comments} = this.props;
const idCursor = this.state.idCursors[0];
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor) {
view.push(comment);
}
});
return view;
}
render () {
const {
comments,
selectedIndex,
selectedCommentId,
commentCount,
singleView,
viewUserDetail,
@@ -62,12 +128,51 @@ class ModerationQueue extends React.Component {
...props
} = this.props;
if (comments.length === 0) {
return (
<div className={styles.root}>
<EmptyCard>{t('modqueue.empty_queue')}</EmptyCard>
</div>
);
}
if (singleView) {
const index = comments.findIndex((comment) => comment.id === selectedCommentId);
const comment = comments[index];
return (
<div className={styles.root}>
<Comment
data={this.props.data}
root={this.props.root}
key={comment.id}
comment={comment}
selected={true}
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
currentUserId={this.props.currentUserId}
/>;
</div>
);
}
const view = this.getVisibleComments();
return (
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
<div className={styles.root}>
<ViewMore
viewMore={this.viewNewComments}
count={comments.length - view.length}
/>
<CSSTransitionGroup
key={activeTab}
component={'ul'}
style={{paddingLeft: 0}}
className={styles.list}
transitionName={{
enter: styles.commentEnter,
enterActive: styles.commentEnterActive,
@@ -80,36 +185,30 @@ class ModerationQueue extends React.Component {
transitionLeaveTimeout={1000}
>
{
comments.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
data={this.props.data}
root={this.props.root}
key={comment.id}
comment={comment}
selected={i === selectedIndex}
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
currentUserId={this.props.currentUserId}
/>;
})
view
.map((comment) => {
return <Comment
data={this.props.data}
root={this.props.root}
key={comment.id}
comment={comment}
selected={comment.id === selectedCommentId}
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
currentUserId={this.props.currentUserId}
/>;
})
}
</CSSTransitionGroup>
{comments.length === 0 &&
<div className={styles.emptyCardContainer}>
<EmptyCard>{t('modqueue.empty_queue')}</EmptyCard>
</div>
}
<LoadMore
loadMore={this.loadMore}
loadMore={this.props.loadMore}
showLoadMore={comments.length < commentCount}
/>
</div>
@@ -117,4 +216,24 @@ class ModerationQueue extends React.Component {
}
}
ModerationQueue.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
comments: PropTypes.array.isRequired,
commentCount: PropTypes.number.isRequired,
loadMore: PropTypes.func.isRequired,
selectedCommentId: PropTypes.string,
singleView: PropTypes.bool,
activeTab: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
currentUserId: PropTypes.string.isRequired,
};
export default ModerationQueue;
@@ -0,0 +1,20 @@
.notFound {
position: relative;
margin: 20px auto;
text-align: center;
padding: 68px 45px;
vertical-align: middle;
min-width: 500px;
a {
color: rgb(244, 126, 107);
font-weight: 500;
}
}
.goToStreams {
position: absolute;
right: 10px;
bottom: 10px;
}
@@ -1,6 +1,7 @@
import React from 'react';
import {Link} from 'react-router';
import styles from './styles.css';
import styles from './NotFoundAsset.css';
import PropTypes from 'prop-types';
const NotFound = (props) => (
<div className={`mdl-card mdl-shadow--2dp ${styles.notFound}`}>
@@ -11,4 +12,8 @@ const NotFound = (props) => (
</div>
);
NotFound.propTypes = {
assetId: PropTypes.string.isRequired,
};
export default NotFound;
@@ -0,0 +1,49 @@
.story {
padding: 7px 50px;
border-bottom: 1px solid #ddd;
cursor: pointer;
display: block;
text-decoration: none;
min-height: 50px;
box-sizing: border-box;
transition: background-color 400ms;
&:hover {
background-color: #efefef;
}
&:last-child {
border-bottom: none;
}
}
.title, .meta {
margin: 0;
color: black;
font-size: 15px;
}
.author, .createdAt, .status {
font-size: 17px;
display: inline-block;
font-size: .8em;
}
.createdAt {
text-align: center;
}
.author {
display: inline-block;
width: 200px;
color: #aaa;
}
.createdAt {
display: inline-block;
width: 200px;
color: #aaa;
}
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './StorySearch.css';
import styles from './Story.css';
const formatDate = (date) => {
const d = new Date(date);
@@ -25,7 +25,8 @@ Story.propTypes = {
author: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
open: PropTypes.bool.isRequired
open: PropTypes.bool.isRequired,
goToStory: PropTypes.func.isRequired,
};
export default Story;
@@ -67,54 +67,6 @@
/*.storyList {
border-top: 1px solid #ddd;
}*/
.story {
padding: 7px 50px;
border-bottom: 1px solid #ddd;
cursor: pointer;
display: block;
text-decoration: none;
min-height: 50px;
box-sizing: border-box;
transition: background-color 400ms;
&:hover {
background-color: #efefef;
}
&:last-child {
border-bottom: none;
}
}
.title, .meta {
margin: 0;
color: black;
font-size: 15px;
}
.author, .createdAt, .status {
font-size: 17px;
display: inline-block;
font-size: .8em;
}
.createdAt {
text-align: center;
}
.author {
display: inline-block;
width: 200px;
color: #aaa;
}
.createdAt {
display: inline-block;
width: 200px;
color: #aaa;
}
.searchButton {
width: 90px;
height: 35px;
@@ -93,7 +93,13 @@ StorySearch.propTypes = {
clearAndCloseSearch: PropTypes.func.isRequired,
moderation: PropTypes.object.isRequired,
handleSearchChange: PropTypes.func.isRequired,
assetId: PropTypes.string
assetId: PropTypes.string,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
handleEsc: PropTypes.func.isRequired,
handleEnter: PropTypes.func.isRequired,
goToModerateAll: PropTypes.func.isRequired,
searchValue: PropTypes.string,
};
export default StorySearch;
@@ -0,0 +1,21 @@
.viewMoreContainer {
display: flex;
justify-content: center;
width: 100%;
}
.viewMore {
width: 100%;
text-align: center;
color: #FFF;
max-width: 660px;
margin-bottom: 30px;
background-color: #2376D8;
cursor: pointer;
text-transform: capitalize;
}
.viewMore:hover {
background-color: #4399FF;
}
@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import styles from './ViewMore.css';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
const ViewMore = ({viewMore, count, className, ...rest}) =>
<div {...rest} className={cn(className, styles.viewMoreContainer)}>
{
count > 0 && <Button
className={styles.viewMore}
onClick={viewMore}>
{count === 1
? t('framework.new_count', count, t('framework.comment'))
: t('framework.new_count', count, t('framework.comments'))}
</Button>
}
</div>;
ViewMore.propTypes = {
viewMore: PropTypes.func.isRequired,
count: PropTypes.number.isRequired,
className: PropTypes.string
};
export default ViewMore;
@@ -1,495 +0,0 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
@custom-media --big-viewport (min-width: 780px);
.listContainer {
max-width: 860px;
margin: 0 auto;
}
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
display: flex;
justify-content: space-between;
}
.tabBarPadding {
width: 150px;
}
.tab {
flex: 1;
color: #BDBDBD;
text-transform: capitalize;
font-weight: 100;
font-size: 14px;
letter-spacing: 1px;
transition: border-bottom 200ms;
transition: color 200ms;
padding: 0px 10px;
margin-right: 20px;
&:hover {
color: white;
/*border-bottom: solid 2px #F36451;*/
box-sizing: border-box;
}
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 4px #F36451;
font-weight: 400;
&:hover {
border-bottom: solid 4px #F36451;
font-weight: 400;
}
}
.active > span {
color: white;
}
.active:after {
background: transparent !important;
}
.showShortcuts {
position: absolute;
right: 130px;
display: flex;
align-items: center;
font-size: 13px;
span {
margin-left: 7px;
}
}
@media (--big-viewport) {
.tab {
flex: none;
}
}
.notFound {
position: relative;
margin: 20px auto;
text-align: center;
padding: 68px 45px;
vertical-align: middle;
min-width: 500px;
a {
color: rgb(244, 126, 107);
font-weight: 500;
&.goToStreams {
position: absolute;
right: 10px;
bottom: 10px;
}
}
}
.header {
background-color: #2c2c2c;
color: white;
margin-bottom: -1px;
.settingsButton {
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
font-size: 16px;
}
.moderateAsset {
a {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 400;
font-size: 20px;
letter-spacing: 1px;
transition: background-color 200ms;
opacity: 1;
&:hover {
cursor: pointer;
background-color: #212121;
}
span {
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 344px;
display: inline-block;
vertical-align: top;
}
}
}
}
@custom-media --big-viewport (min-width: 780px);
.list {
padding: 8px 0;
list-style: none;
display: block;
&.singleView .listItem {
display: none;
}
&.singleView .listItem.selected {
display: block;
height: 100%;
font-size: 1.5em;
line-height: 1.5em;
border: none;
.actions {
position: fixed;
bottom: 60px;
left: 25%;
display: flex;
justify-content: space-around;
width: 50%;
margin: 0;
}
.actionButton {
transform: scale(1.4);
}
}
}
.listItem {
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 650px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: all 200ms;
padding: 10px 0;
min-height: 0;
/*
Fix rendering issues in Safari by promoting this
into its own layer.
https://www.pivotaltracker.com/story/show/151142211
*/
transform: translateZ(0);
.container {
padding: 0 14px;
}
&:last-child {
border-bottom: none;
}
&.selected {
}
.context {
a {
color: #f36451;
text-decoration: underline;
float: right;
}
}
.sideActions {
height: 100%;
top: 0;
box-sizing: border-box;
}
.itemHeader {
display: flex;
align-items: center;
justify-content: space-between;
.author {
font-weight: 300;
width: 100%;
display: flex;
align-items: center;
color: #262626;
font-size: 16px;
position: relative;
}
}
.itemBody {
display: flex;
justify-content: space-between;
font-size: 14px;
line-height: 1.5;
font-weight: 300;
}
.avatar {
margin-right: 16px;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: #757575;
font-size: 40px;
color: #fff;
}
.created {
padding: 5px;
color: #262626;
font-size: 14px;
line-height: 1px;
font-weight: 300;
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
font-size: 16px;
}
.flagged {
color: rgba(255, 0, 0, .5);
padding-top: 15px;
padding-left: 10px;
}
.flagCount{
font-size: 12px;
color: #d32f2f;
}
}
.empty {
color: #444;
margin-top: 50px;
text-align: center;
}
@media (--big-viewport) {
.listItem {
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
&.activeItem {
border: 2px solid #333;
}
}
}
.hasLinks {
color: #f00;
text-align: right;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.banned {
color: #f00;
text-align: left;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
.ban {
display: block;
text-align: center;
margin-top: 5px;
}
.Comment {
.moderateArticle {
font-size: 14px;
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
max-width: 500px;
a {
display: inline-block;
color: #063b9a;
text-decoration: none;
font-weight: 500;
letter-spacing: .5px;
margin-left: 10px;
font-size: 13px;
margin-left: 5px;
padding-bottom: 0px;
border-bottom: solid 1px;
line-height: 16px;
&:hover {
opacity: .9;
cursor: pointer;
}
}
}
}
.selectField {
position: relative;
width: 140px;
height: 36px;
top: 5px;
margin-right: 10px;
background: #FFF;
padding: 10px 15px;
box-sizing: border-box;
border-radius: 2px;
bor
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
border-bottom: 0px;
}
label {
top: -4px;
}
&:hover {
cursor: pointer;
}
}
.tabIcon {
position: relative;
top: 3px;
font-size: 16px;
}
.username {
color: #393B44;
text-decoration: none;
cursor: pointer;
font-weight: 600;
padding: 2px 5px;
border-radius: 2px;
margin-left: -5px;
transition: background-color 200ms ease;
&:hover {
background-color: #E0E0E0;
}
}
.external {
font-size: .7em;
text-decoration: none;
color: #063b9a;
cursor: pointer;
font-weight: normal;
margin-left: 10px;
white-space: nowrap;
&:hover {
text-decoration: underline;
opacity: .9;
}
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
.emptyCardContainer {
margin-top: 16px;
}
.commentLeave {
opacity: 1.0;
}
.commentLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.commentEnter {
opacity: 0;
}
.commentEnterActive {
opacity: 1.0;
transition: opacity 800ms;
}
.editedMarker {
font-style: italic;
color: #666;
font-size: 12px;
line-height: 1px;
font-weight: 300;
}
.searchTrigger {
position: relative;
top: .2em;
}
.adminCommentInfoBar {
min-width: 100px;
position: absolute;
right: 0px;
top: 0px;
text-align: right;
}
@@ -1,6 +1,7 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import CommentLabels from '../../../containers/CommentLabels';
import CommentDetails from '../../../containers/CommentDetails';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads, getDefinitionName} from 'coral-framework/utils';
@@ -16,8 +17,12 @@ export default withFragments({
fragment CoralAdmin_ModerationComment_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
...${getDefinitionName(CommentLabels.fragments.root)}
...${getDefinitionName(CommentDetails.fragments.root)}
}
`,
${CommentLabels.fragments.root}
${CommentDetails.fragments.root}
`,
comment: gql`
fragment CoralAdmin_ModerationComment_comment on Comment {
id
@@ -34,33 +39,15 @@ export default withFragments({
title
url
}
action_summaries {
count
... on FlagActionSummary {
reason
__typename
}
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
__typename
}
__typename
}
editing {
edited
}
hasParent
${getSlotFragmentSpreads(slots, 'comment')}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
})(Comment);
@@ -82,50 +82,56 @@ class ModerationContainer extends Component {
}
subscribeToUpdates(variables = this.props.data.variables) {
const sub1 = this.props.data.subscribeToMore({
document: COMMENT_ACCEPTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
const parameters = [
{
document: COMMENT_ADDED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAdded: comment}}}) => {
return this.handleCommentChange(prev, comment);
},
},
});
const sub2 = this.props.data.subscribeToMore({
document: COMMENT_REJECTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
{
document: COMMENT_ACCEPTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
});
const sub3 = this.props.data.subscribeToMore({
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
{
document: COMMENT_REJECTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
});
const sub4 = this.props.data.subscribeToMore({
document: COMMENT_FLAGGED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
const user = comment.actions[comment.actions.length - 1].user;
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
{
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
});
{
document: COMMENT_FLAGGED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
const user = comment.actions[comment.actions.length - 1].user;
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
];
this.subscriptions.push(sub1, sub2, sub3, sub4);
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMore(param));
}
unsubscribe() {
@@ -204,12 +210,9 @@ class ModerationContainer extends Component {
// Not found.
return <NotFoundAsset assetId={assetId} />;
}
if (asset === undefined || asset.id !== assetId) {
}
// Still loading.
return <Spinner />;
}
} else if (asset !== undefined || !('premodCount' in root)) {
if(data.loading) {
// loading.
return <Spinner />;
@@ -240,6 +243,14 @@ class ModerationContainer extends Component {
/>;
}
}
const COMMENT_ADDED_SUBSCRIPTION = gql`
subscription CommentAdded($asset_id: ID){
commentAdded(asset_id: $asset_id, statuses: null){
...${getDefinitionName(Comment.fragments.comment)}
}
}
${Comment.fragments.comment}
`;
const COMMENT_EDITED_SUBSCRIPTION = gql`
subscription CommentEdited($asset_id: ID){
@@ -352,7 +363,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
organizationName
moderation
}
...${getDefinitionName(Comment.fragments.root)}
}
${Comment.fragments.root}
${commentConnectionFragment}
`, {
options: (props) => {
@@ -369,29 +382,6 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
},
});
const withQueueCountPolling = withQuery(({queueConfig}) => gql`
query CoralAdmin_ModerationCountPoll($asset_id: ID) {
${Object.keys(queueConfig).map((queue) => `
${queue}Count: commentCount(query: {
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
asset_id: $asset_id,
})
`)}
}
`, {
options: (props) => {
const id = getAssetId(props);
return {
pollInterval: 5000,
variables: {
asset_id: id
}
};
}
});
const mapStateToProps = (state) => ({
moderation: state.moderation,
settings: state.settings,
@@ -419,6 +409,5 @@ export default compose(
withQueueConfig(baseQueueConfig),
connect(mapStateToProps, mapDispatchToProps),
withSetCommentStatus,
withQueueCountPolling,
withModQueueQuery,
)(ModerationContainer);
@@ -1,14 +0,0 @@
export const actionsMap = {
PREMOD: ['APPROVE', 'REJECT'],
FLAGGED: ['APPROVE', 'REJECT'],
REJECTED: ['APPROVE', 'REJECTED']
};
export const menuActionsMap = {
'REJECT': {status: 'REJECTED', text: 'reject', icon: 'close', key: 'f'},
'REJECTED': {status: 'REJECTED', text: 'rejected', icon: 'close'},
'APPROVE': {status: 'ACCEPTED', text: 'approve', icon: 'done', key: 'd'},
'FLAGGED': {status: 'FLAGGED', text: 'flag', icon: 'flag', filter: 'Untouched'},
'BAN': {status: 'BANNED', text: 'ban_user', icon: 'not interested'},
'': {icon: 'done'}
};
@@ -1,6 +1,7 @@
import React from 'react';
import {Button, Checkbox} from 'coral-ui';
import QuestionBoxBuilder from './QuestionBoxBuilder';
import cn from 'classnames';
import styles from './ConfigureCommentStream.css';
@@ -13,7 +14,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
<h3>{t('configure.title')}</h3>
<Button
type="submit"
className={styles.apply}
className={cn(styles.apply, 'talk-embed-stream-configuration-submit-button')}
onChange={handleChange}
cStyle={changed ? 'green' : 'darkGrey'} >
{t('configure.apply')}
@@ -102,7 +102,7 @@ class ConfigureStreamContainer extends Component {
const closedTimeout = dirtySettings.closedTimeout;
return (
<div>
<div className='talk-embed-stream-configuration-container'>
<ConfigureCommentStream
handleChange={this.handleChange}
handleApply={this.handleApply}
@@ -45,16 +45,17 @@
border-left: 3px solid rgb(35,118,216);
}
.bylineSecondary {
display: flex;
color: #696969;
font-size: 12px;
}
.pendingComment {
filter: blur(2px);
pointer-events: none;
}
.bylineSecondary {
color: #696969;
font-size: 12px;
}
.editedMarker {
font-style: italic;
}
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import TagLabel from 'talk-plugin-tag-label/TagLabel';
import PubDate from 'talk-plugin-pubdate/PubDate';
import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
import {ReplyBox, ReplyButton} from 'talk-plugin-replies';
import {FlagComment} from 'talk-plugin-flags';
import {can} from 'coral-framework/services/perms';
@@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues';
import LoadMore from './LoadMore';
import {getEditableUntilDate} from './util';
import {findCommentWithId} from '../graphql/utils';
import CommentContent from './CommentContent';
import CommentContent from 'coral-framework/components/CommentContent';
import Slot from 'coral-framework/components/Slot';
import CommentTombstone from './CommentTombstone';
import InactiveCommentLabel from './InactiveCommentLabel';
@@ -465,7 +465,14 @@ export default class Comment extends React.Component {
/>
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
<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>
@@ -522,7 +529,7 @@ export default class Comment extends React.Component {
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
{isActive &&
<div className={'talk-stream-comment-actions-container'}>
<div className="commentActionsLeft comment__action-container">
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
{...slotProps}
@@ -538,7 +545,7 @@ export default class Comment extends React.Component {
/>
</ActionButton>}
</div>
<div className="commentActionsRight comment__action-container">
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
<Slot
fill="commentActions"
wrapperComponent={ActionButton}
@@ -29,7 +29,7 @@ export default class Embed extends React.Component {
};
render() {
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props;
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
const {user} = this.props.auth;
const hasHighlightedComment = !!commentId;
@@ -37,7 +37,7 @@ export default class Embed extends React.Component {
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
<IfSlotIsNotEmpty slot="login">
<Popup
href='embed/stream/login'
href={`embed/stream/login?parentUrl=${encodeURIComponent(parentUrl)}`}
title='Login'
features='menubar=0,resizable=0,width=500,height=550,top=200,left=500'
open={showSignInDialog}
@@ -9,11 +9,8 @@ class LoadMore extends React.Component {
if (!count) {
return t('framework.show_all_replies');
}
if (count === 1) {
return t('framework.show_1_more_reply');
}
return t('framework.show_x_more_replies', count);
return t('framework.show_more_replies');
}
render () {
@@ -17,7 +17,8 @@ const slots = [
'commentReactions',
'commentAvatar',
'commentAuthorName',
'commentAuthorTags'
'commentAuthorTags',
'commentTimestamp',
];
/**
+18 -5
View File
@@ -96,6 +96,11 @@ export default {
user {
id
username
tags {
tag {
name
}
}
}
action_summaries {
count
@@ -129,7 +134,19 @@ export default {
user: {
__typename: 'User',
id: auth.user.id,
username: auth.user.username
username: auth.user.username,
tags: tags.map((tag) => ({
tag: {
name: tag,
created_at: new Date().toISOString(),
__typename: 'Tag'
},
assigned_by: {
id: auth.user.id,
__typename: 'User'
},
__typename: 'TagLink'
})),
},
created_at: new Date().toISOString(),
body,
@@ -140,10 +157,6 @@ export default {
created_at: new Date().toISOString(),
__typename: 'Tag'
},
assigned_by: {
id: auth.user.id,
__typename: 'User'
},
__typename: 'TagLink'
})),
status: 'NONE',
+4 -9
View File
@@ -85,10 +85,6 @@ body {
/* Info Box Styles */
.hidden {
visibility: hidden;
display: none;
}
.talk-plugin-infobox-info {
top: 0;
@@ -235,10 +231,6 @@ button.comment__action-button[disabled],
color: #F00;
}
.talk-plugin-pubdate-text {
display: inline-block;
}
/* Flag Styles */
.talk-plugin-flags-popup-form {
@@ -367,4 +359,7 @@ button.comment__action-button[disabled],
color: white;
}
.hidden {
visibility: hidden;
display: none;
}
@@ -2,18 +2,18 @@ import React from 'react';
const CommentContent = ({comment}) => {
const textbreaks = comment.body.split('\n');
return <div className={`${name}-text`}>
return <span className={`${name}-text`}>
{
textbreaks.map((line, i) => {
return (
<span key={i} className={`${name}-line`}>
{line}
<br className={`${name}-linebreak`}/>
{i === textbreaks.length - 1 && <br className={`${name}-linebreak`}/>}
</span>
);
})
}
</div>;
</span>;
};
export default CommentContent;
@@ -0,0 +1,37 @@
.root {
}
.headerContainer {
display: flex;
justify-content: flex-start;
align-items: center;
}
.header {
vertical-align: middle;
margin: 0;
font-weight: 500;
display: inline-block;
font-size: 12px;
line-height: 12px;
margin-right: 7px;
}
.info {
font-size: 12px;
}
.details {
padding: 0 20px 16px;
font-size: 12px;
&:empty {
display: none;
}
}
.icon {
vertical-align: middle;
font-size: 12px;
margin-right: 7px;
}
@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './CommentDetail.css';
import {Icon} from 'coral-ui';
const CommentDetail = ({icon, header, info, children, className}) => {
return (
<div className={cn(className, styles.root)}>
<div className={styles.headerContainer}>
{icon && <Icon className={styles.icon} name={icon}/>}
<h3 className={styles.header}>{header}:</h3>
<div className={styles.info}>{info}</div>
</div>
{children &&
<div className={styles.details}>
{children}
</div>
}
</div>
);
};
CommentDetail.propTypes = {
className: PropTypes.string,
header: PropTypes.node,
icon: PropTypes.string,
info: PropTypes.node,
children: PropTypes.node,
};
export default CommentDetail;
@@ -0,0 +1,6 @@
.timestamp {
display: inline-block;
color: #696969;
font-size: 12px;
}
@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import {timeago} from 'coral-framework/services/i18n';
import cn from 'classnames';
import styles from './CommentTimestamp.css';
const CommentTimestamp = ({className, created_at}) => (
<div className={cn(className, styles.timestamp, 'talk-comment-timestamp')}>
{timeago(created_at)}
</div>
);
CommentTimestamp.propTypes = {
className: PropTypes.string,
created_at: PropTypes.string,
};
export default CommentTimestamp;
@@ -23,8 +23,9 @@ class IfSlotIsEmpty extends React.Component {
}
isSlotEmpty(props = this.props) {
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
const slots = Array.isArray(slot) ? slot : [slot];
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
}
render() {
@@ -34,7 +35,7 @@ class IfSlotIsEmpty extends React.Component {
}
IfSlotIsEmpty.propTypes = {
slot: PropTypes.string,
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};
const mapStateToProps = (state) => ({
@@ -23,8 +23,9 @@ class IfSlotIsNotEmpty extends React.Component {
}
isSlotEmpty(props = this.props) {
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
const slots = Array.isArray(slot) ? slot : [slot];
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
}
render() {
@@ -34,7 +35,7 @@ class IfSlotIsNotEmpty extends React.Component {
}
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.string,
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};
const mapStateToProps = (state) => ({
+19 -4
View File
@@ -1,4 +1,19 @@
export default (condition) => (BaseComponent) => {
BaseComponent.isExcluded = condition;
return BaseComponent;
};
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
/**
* ExcludeIf provides a property `emit: (eventName, value)`
* to the wrapped component.
*/
export default (condition) => hoistStatics((WrappedComponent) => {
class ExcludeIf extends React.Component {
render() {
return <WrappedComponent
{...this.props}
/>;
}
}
WrappedComponent.isExcluded = condition;
return ExcludeIf;
});
@@ -71,7 +71,7 @@ class ProfileContainer extends Component {
const emailAddress = localProfile && localProfile.id;
return (
<div>
<div className='talk-embed-stream-profile-container'>
<h2>{user.username}</h2>
{emailAddress ? <p>{emailAddress}</p> : null}
@@ -95,8 +95,9 @@ class ProfileContainer extends Component {
const slots = [
'profileSections',
// TODO: This Slot should be included in `talk-plugin-history` instead.
// TODO: These Slots should be included in `talk-plugin-history` instead.
'commentContent',
'historyCommentTimestamp',
];
const CommentFragment = gql`
-53
View File
@@ -130,59 +130,6 @@
background: #00a291;
}
.type--approve {
display: block;
color: #519954;
border: solid 2px rgba(81, 153, 84, 0.75);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
&:hover {
box-shadow: none;
color: white;
background-color: #519954;
}
}
.type--reject, .type--rejected {
display: block;
color: #D03235;
border: solid 1px #D03235;
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
}
}
.type--rejected {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
.type--ban, .type--actions {
display: block;
color: #616161;
+23 -22
View File
@@ -49,27 +49,32 @@
li {
margin-bottom: 5px;
&:nth-child(1) {
color: #5394D7;
}
&:nth-child(2) {
color: #909090;
}
i {
margin-right: 5px;
font-size: 15px;
}
a:hover {
cursor: pointer;
}
}
}
.viewLink {
color: #5394D7;
&:hover {
cursor: pointer;
}
}
.iconView {
margin-right: 5px;
font-size: 15px;
vertical-align: middle;
padding-bottom: 1px;
}
.iconDate {
margin-right: 5px;
font-size: 15px;
color: #696969;
vertical-align: middle;
padding-bottom: 1px;
}
@custom-media --mobile-viewport (max-width: 480px);
@media (--mobile-viewport) {
@@ -92,7 +97,3 @@
color: inherit;
}
.iconView, .iconDate {
vertical-align: middle;
padding-bottom: 1px;
}
+13 -7
View File
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import {Icon} from '../coral-ui';
import styles from './Comment.css';
import Slot from 'coral-framework/components/Slot';
import PubDate from '../talk-plugin-pubdate/PubDate';
import CommentContent from '../coral-embed-stream/src/components/CommentContent';
import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
import CommentContent from 'coral-framework/components/CommentContent';
import cn from 'classnames';
import {getTotalReactionsCount} from 'coral-framework/utils';
@@ -15,6 +15,7 @@ class Comment extends React.Component {
render() {
const {comment, link, data, root} = this.props;
const reactionCount = getTotalReactionsCount(comment.action_summaries);
const queryData = {root, comment, asset: comment.asset};
return (
<div className={styles.myComment}>
@@ -24,7 +25,7 @@ class Comment extends React.Component {
defaultComponent={CommentContent}
className={cn(styles.commentBody, 'my-comment-body')}
data={data}
queryData={{root, comment, asset: comment.asset}}
queryData={queryData}
/>
<div className={cn(styles.commentSummary, 'comment-summary')}>
<span className={cn(styles.commentSummaryReactions, 'comment-summary-reactions', {[styles.countZero]: reactionCount === 0})}>
@@ -37,7 +38,7 @@ class Comment extends React.Component {
<span className={cn('comment-summary-replies', {[styles.countZero]: comment.replyCount === 0})}>
<Icon name="reply" />
<span className={cn(styles.replyCount, 'comment-summary-reply-count')}>
{comment.replyCount}
{comment.replyCount}
</span>
{comment.replyCount === 1 ? t('common.reply') : t('common.replies')}
</span>
@@ -53,15 +54,20 @@ class Comment extends React.Component {
<div className={styles.sidebar}>
<ul>
<li>
<a onClick={link(`${comment.asset.url}?commentId=${comment.id}`)}>
<a onClick={link(`${comment.asset.url}?commentId=${comment.id}`)} className={styles.viewLink}>
<Icon name="open_in_new" className={styles.iconView}/>{t('view_conversation')}
</a>
</li>
<li>
<Icon name="schedule" className={styles.iconDate} />
<PubDate
className={styles.pubdate}
<Slot
fill="historyCommentTimestamp"
defaultComponent={CommentTimestamp}
className={'talk-history-comment-published-date'}
created_at={comment.created_at}
data={data}
queryData={queryData}
inline
/>
</li>
</ul>
@@ -1,13 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import {BASE_PATH} from 'coral-framework/constants/url';
const ModerationLink = (props) => props.isAdmin ? (
<div className={styles.moderationLink}>
<a href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
<div className={cn(styles.moderationLink, 'talk-embed-stream-moderation-container')}>
<a className='talk-embed-stream-moderation-link' href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
{t('moderate_this_stream')}
</a>
</div>
-10
View File
@@ -1,10 +0,0 @@
import React from 'react';
import {timeago} from 'coral-framework/services/i18n';
const name = 'talk-plugin-pubdate';
const PubDate = ({created_at}) => <div className={`${name}-text`}>
{timeago(created_at)}
</div>;
export default PubDate;
+30 -7
View File
@@ -94,14 +94,17 @@ const CONFIG = {
MONGO_URL: process.env.TALK_MONGO_URL,
REDIS_URL: process.env.TALK_REDIS_URL,
// REDIS_RECONNECTION_MAX_ATTEMPTS is the amount of attempts that a redis
// connection will attempt to reconnect before aborting with an error.
REDIS_RECONNECTION_MAX_ATTEMPTS: parseInt(process.env.TALK_REDIS_RECONNECTION_MAX_ATTEMPTS || '100'),
// REDIS_CLIENT_CONFIG is the optional configuration that is merged with the
// function config to provide deep control of the redis connection beheviour.
REDIS_CLIENT_CONFIG: process.env.TALK_REDIS_CLIENT_CONFIGURATION || '{}',
// REDIS_RECONNECTION_MAX_RETRY_TIME is the time in string format for the
// maximum amount of time that a client can be considered "connecting" before
// attempts at reconnection are aborted with an error.
REDIS_RECONNECTION_MAX_RETRY_TIME: ms(process.env.TALK_REDIS_RECONNECTION_MAX_RETRY_TIME || '1 min'),
// REDIS_CLUSTER_MODE allows configuration on the type of cluster mode enabled
// on the redis client. Can be either `NONE` or `CLUSTER`.
REDIS_CLUSTER_MODE: process.env.TALK_REDIS_CLUSTER_MODE || 'NONE',
// REDIS_CLUSTER_CONFIGURATION contains the json string for the redis cluster
// configuration.
REDIS_CLUSTER_CONFIGURATION: process.env.TALK_REDIS_CLUSTER_CONFIGURATION || '[]',
// REDIS_RECONNECTION_BACKOFF_FACTOR is the factor that will be multiplied
// against the current attempt count inbetween attempts to connect to redis.
@@ -246,6 +249,26 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) {
CONFIG.REDIS_URL = 'redis://localhost/1';
}
// REDIS_CLUSTER_CONFIGURATION should be parsed when the cluster mode !== none.
if (CONFIG.REDIS_CLUSTER_MODE === 'CLUSTER') {
try {
CONFIG.REDIS_CLUSTER_CONFIGURATION = JSON.parse(CONFIG.REDIS_CLUSTER_CONFIGURATION);
} catch (err) {
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION is not valid JSON, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
}
if (!Array.isArray(CONFIG.REDIS_CLUSTER_CONFIGURATION)) {
throw new Error('TALK_REDIS_CLUSTER_MODE is CLUSTER, but the TALK_REDIS_CLUSTER_CONFIGURATION is invalid, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
}
if (CONFIG.REDIS_CLUSTER_CONFIGURATION.length === 0) {
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION must have at least one node specified in the cluster, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
}
}
// Client config is a JSON encoded string, defaulting to `{}`.
CONFIG.REDIS_CLIENT_CONFIG = JSON.parse(CONFIG.REDIS_CLIENT_CONFIG);
//------------------------------------------------------------------------------
// Recaptcha configuration
//------------------------------------------------------------------------------
+2 -5
View File
@@ -1,6 +1,3 @@
_site/
.sass-cache/
_site
.sass-cache
.jekyll-metadata
_pdf
.idea/
.DS_Store
+22
View File
@@ -0,0 +1,22 @@
---
layout: doc
---
<style type="text/css" media="screen">
.container {
margin: 10px auto;
}
h1 {
margin: 30px 0;
font-size: 4em;
line-height: 1;
letter-spacing: -1px;
}
</style>
<div class="container">
<h1>404</h1>
<p><strong>Page not found :(</strong></p>
<p>The requested page could not be found. Try searching above for your page.</p>
</div>
Executable → Regular
+20 -7
View File
@@ -1,11 +1,24 @@
source "https://rubygems.org"
gem "github-pages", group: :jekyll_plugins
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "3.5.2"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-paginate"
gem "jekyll-sitemap"
gem "jekyll-gist"
gem "jekyll-feed"
gem "jemoji"
end
gem "jekyll-seo-tag", "~> 2.1"
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+25 -167
View File
@@ -1,176 +1,45 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.8)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
colorator (1.1.0)
ethon (0.10.1)
ffi (>= 1.3.0)
cssminify2 (2.0.1)
execjs (2.7.0)
faraday (0.12.1)
multipart-post (>= 1.2, < 3)
ffi (1.9.18)
forwardable-extended (2.6.0)
gemoji (3.0.0)
github-pages (145)
activesupport (= 4.2.8)
github-pages-health-check (= 1.3.4)
jekyll (= 3.4.5)
jekyll-avatar (= 0.4.2)
jekyll-coffeescript (= 1.0.1)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.9.2)
jekyll-gist (= 1.4.0)
jekyll-github-metadata (= 2.5.1)
jekyll-mentions (= 1.2.0)
jekyll-optional-front-matter (= 0.2.0)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.1.0)
jekyll-redirect-from (= 0.12.1)
jekyll-relative-links (= 0.4.1)
jekyll-sass-converter (= 1.5.0)
jekyll-seo-tag (= 2.2.3)
jekyll-sitemap (= 1.0.0)
jekyll-swiss (= 0.4.0)
jekyll-theme-architect (= 0.0.4)
jekyll-theme-cayman (= 0.0.4)
jekyll-theme-dinky (= 0.0.4)
jekyll-theme-hacker (= 0.0.4)
jekyll-theme-leap-day (= 0.0.4)
jekyll-theme-merlot (= 0.0.4)
jekyll-theme-midnight (= 0.0.4)
jekyll-theme-minimal (= 0.0.4)
jekyll-theme-modernist (= 0.0.4)
jekyll-theme-primer (= 0.3.1)
jekyll-theme-slate (= 0.0.4)
jekyll-theme-tactile (= 0.0.4)
jekyll-theme-time-machine (= 0.0.4)
jekyll-titles-from-headings (= 0.2.0)
jemoji (= 0.8.0)
kramdown (= 1.13.2)
liquid (= 3.0.6)
listen (= 3.0.6)
mercenary (~> 0.3)
minima (= 2.1.1)
rouge (= 1.11.1)
terminal-table (~> 1.4)
github-pages-health-check (1.3.4)
addressable (~> 2.3)
net-dns (~> 0.8)
octokit (~> 4.0)
public_suffix (~> 2.0)
typhoeus (~> 0.7)
html-pipeline (2.6.0)
activesupport (>= 2)
nokogiri (>= 1.4)
i18n (0.8.6)
jekyll (3.4.5)
htmlcompressor (0.3.1)
jekyll (3.5.2)
addressable (~> 2.4)
colorator (~> 1.0)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 1.1)
kramdown (~> 1.3)
liquid (~> 3.0)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (~> 1.7)
safe_yaml (~> 1.0)
jekyll-avatar (0.4.2)
jekyll (~> 3.0)
jekyll-coffeescript (1.0.1)
coffee-script (~> 2.2)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.9.2)
jekyll (~> 3.3)
jekyll-gist (1.4.0)
octokit (~> 4.2)
jekyll-github-metadata (2.5.1)
jekyll (~> 3.1)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.2.0)
activesupport (~> 4.0)
html-pipeline (~> 2.3)
jekyll (~> 3.0)
jekyll-optional-front-matter (0.2.0)
jekyll (~> 3.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.1.0)
jekyll (~> 3.0)
jekyll-redirect-from (0.12.1)
jekyll (~> 3.3)
jekyll-relative-links (0.4.1)
jekyll (~> 3.3)
jekyll-minifier (0.1.4)
cssminify2 (~> 2.0)
htmlcompressor (~> 0.3)
jekyll (~> 3.5)
uglifier (~> 3.2)
jekyll-sass-converter (1.5.0)
sass (~> 3.4)
jekyll-seo-tag (2.2.3)
jekyll (~> 3.3)
jekyll-sitemap (1.0.0)
jekyll (~> 3.3)
jekyll-swiss (0.4.0)
jekyll-theme-architect (0.0.4)
jekyll (~> 3.3)
jekyll-theme-cayman (0.0.4)
jekyll (~> 3.3)
jekyll-theme-dinky (0.0.4)
jekyll (~> 3.3)
jekyll-theme-hacker (0.0.4)
jekyll (~> 3.3)
jekyll-theme-leap-day (0.0.4)
jekyll (~> 3.3)
jekyll-theme-merlot (0.0.4)
jekyll (~> 3.3)
jekyll-theme-midnight (0.0.4)
jekyll (~> 3.3)
jekyll-theme-minimal (0.0.4)
jekyll (~> 3.3)
jekyll-theme-modernist (0.0.4)
jekyll (~> 3.3)
jekyll-theme-primer (0.3.1)
jekyll (~> 3.3)
jekyll-theme-slate (0.0.4)
jekyll (~> 3.3)
jekyll-theme-tactile (0.0.4)
jekyll (~> 3.3)
jekyll-theme-time-machine (0.0.4)
jekyll (~> 3.3)
jekyll-titles-from-headings (0.2.0)
jekyll-seo-tag (2.3.0)
jekyll (~> 3.3)
jekyll-watch (1.5.0)
listen (~> 3.0, < 3.1)
jemoji (0.8.0)
activesupport (~> 4.0)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0)
kramdown (1.13.2)
liquid (3.0.6)
listen (3.0.6)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9.7)
kramdown (1.15.0)
liquid (4.0.0)
listen (3.0.8)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
mercenary (0.3.6)
mini_portile2 (2.2.0)
minima (2.1.1)
jekyll (~> 3.3)
minitest (5.10.2)
multipart-post (2.0.0)
net-dns (0.8.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
octokit (4.7.0)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.14.0)
forwardable-extended (~> 2.6)
public_suffix (2.0.5)
public_suffix (3.0.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
@@ -181,28 +50,17 @@ GEM
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (0.8.0)
ethon (>= 0.8.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
unicode-display_width (1.3.0)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
PLATFORMS
ruby
DEPENDENCIES
github-pages
jekyll-feed
jekyll-gist
jekyll-paginate
jekyll-sitemap
jemoji
jekyll (= 3.5.2)
jekyll-minifier (~> 0.1.4)
jekyll-seo-tag (~> 2.1)
tzinfo-data
BUNDLED WITH
1.15.2
1.15.4
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2017 Michael Rose
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-75
View File
@@ -1,75 +0,0 @@
require "bundler/gem_tasks"
require "jekyll"
require "listen"
def listen_ignore_paths(base, options)
[
/_config\.ya?ml/,
/_site/,
/\.jekyll-metadata/
]
end
def listen_handler(base, options)
site = Jekyll::Site.new(options)
Jekyll::Command.process_site(site)
proc do |modified, added, removed|
t = Time.now
c = modified + added + removed
n = c.length
relative_paths = c.map{ |p| Pathname.new(p).relative_path_from(base).to_s }
print Jekyll.logger.message("Regenerating:", "#{relative_paths.join(", ")} changed... ")
begin
Jekyll::Command.process_site(site)
puts "regenerated in #{Time.now - t} seconds."
rescue => e
puts "error:"
Jekyll.logger.warn "Error:", e.message
Jekyll.logger.warn "Error:", "Run jekyll build --trace for more information."
end
end
end
task :preview do
base = Pathname.new('.').expand_path
options = {
"source" => base.join('test').to_s,
"destination" => base.join('test/_site').to_s,
"force_polling" => false,
"serving" => true,
"theme" => "minimal-mistakes-jekyll"
}
options = Jekyll.configuration(options)
ENV["LISTEN_GEM_DEBUGGING"] = "1"
listener = Listen.to(
base.join("_includes"),
base.join("_layouts"),
base.join("_sass"),
base.join("assets"),
options["source"],
:ignore => listen_ignore_paths(base, options),
:force_polling => options['force_polling'],
&(listen_handler(base, options))
)
begin
listener.start
Jekyll.logger.info "Auto-regeneration:", "enabled for '#{options["source"]}'"
unless options['serving']
trap("INT") do
listener.stop
puts " Halting auto-regeneration."
exit 0
end
loop { sleep 1000 }
end
rescue ThreadError
# You pressed Ctrl-C, oh my!
end
Jekyll::Commands::Serve.process(options)
end
Executable → Regular
+43 -226
View File
@@ -1,240 +1,63 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your entire site, values
# which you are expected to set up once and rarely need to edit after that.
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely edit after that. If you find
# yourself editing this file very often, consider using Jekyll's data files
# feature for the data you need to update frequently.
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# `jekyll serve`. If you change this file, please restart the server process.
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
# Site Settings
locale : "en"
title : "Coral Talk Documentation"
title_separator : "-"
name : "The Coral Project"
description : "Documentation and guides for Coral Talk."
url : https://coralproject.github.io # the base hostname & protocol for your site e.g. "https://mmistakes.github.io"
baseurl : /talk # the subpath of your site, e.g. "/blog"
repository : "coralproject/talk" # GitHub username/repo-name e.g. "mmistakes/minimal-mistakes"
teaser : # path of fallback teaser image, e.g. "/assets/images/500x300.png"
# breadcrumbs : false # true, false (default)
words_per_minute : 200
comments:
provider : # false (default), "disqus", "discourse", "facebook", "google-plus", "staticman", "custom"
disqus:
shortname : # https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname-
discourse:
server : # https://meta.discourse.org/t/embedding-discourse-comments-via-javascript/31963 , e.g.: meta.discourse.org
facebook:
# https://developers.facebook.com/docs/plugins/comments
appid :
num_posts : # 5 (default)
colorscheme : # "light" (default), "dark"
staticman:
allowedFields : ['name', 'email', 'url', 'message']
branch : "master"
commitMessage : "New comment."
filename : comment-{@timestamp}
format : "yml"
moderation : true
path : "docs/_data/comments/{options.slug}" # "/_data/comments/{options.slug}" (default)
requiredFields : ['name', 'email', 'message']
transforms:
email : "md5"
generatedFields:
date:
type : "date"
options:
format : "iso8601" # "iso8601" (default), "timestamp-seconds", "timestamp-milliseconds"
atom_feed:
path : # blank (default) uses feed.xml
# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: Talk Documentation
description: > # this means to ignore newlines until "baseurl:"
Documentation and guides for Talk from The Coral Project.
baseurl: "/talk" # the subpath of your site, e.g. /blog
url: "https://coralproject.github.io" # the base hostname & protocol for your site, e.g. http://example.com
google_analytics: UA-73335347-5
versions:
node: 8+
yarn: 1.0.1+
mongodb: 3.2+
redis: 3.2.5+
docker: 17.06.2+
docker_compose: 1.14.0+
# SEO Related
google_site_verification :
bing_site_verification :
alexa_site_verification :
yandex_site_verification :
# Social Sharing
twitter:
username :
facebook:
username :
app_id :
publisher :
og_image : # Open Graph/Twitter default site image
# For specifying social profiles
# - https://developers.google.com/structured-data/customize/social-profiles
social:
type : # Person or Organization (defaults to Person)
name : # If the user or organization name differs from the site's name
links: # An array of links to social media profiles
# Analytics
analytics:
provider : false # false (default), "google", "google-universal", "custom"
google:
tracking_id :
# Site Author
author:
name : "Your Name"
avatar : # path of avatar image, e.g. "/assets/images/bio-photo.jpg"
bio : "I am an amazing person."
location : "Somewhere"
email :
uri :
bitbucket :
codepen :
dribbble :
flickr :
facebook :
foursquare :
github :
gitlab :
google_plus :
keybase :
instagram :
lastfm :
linkedin :
pinterest :
soundcloud :
stackoverflow : # "123456/username" (the last part of your profile url, e.g. http://stackoverflow.com/users/123456/username)
steam :
tumblr :
twitter :
vine :
weibo :
xing :
youtube : # "https://youtube.com/c/MichaelRoseDesign"
# Reading Files
include:
- .htaccess
- swagger.yml
- _pages
exclude:
- "*.sublime-project"
- "*.sublime-workspace"
- vendor
- .asset-cache
- .bundle
- .jekyll-assets-cache
- .sass-cache
- assets/js/plugins
- assets/js/_main.js
- assets/js/vendor
- Capfile
- CHANGELOG
- config
- Gemfile
- Gruntfile.js
- gulpfile.js
- LICENSE
- log
- node_modules
- package.json
- Rakefile
- README.md
- tmp
keep_files:
- .git
- .svn
encoding: "utf-8"
markdown_ext: "markdown,mkdown,mkdn,mkd,md"
# Conversion
# Build settings
markdown: kramdown
highlighter: rouge
lsi: false
excerpt_separator: "\n\n"
incremental: false
# Markdown Processing
kramdown:
input: GFM
hard_wrap: false
auto_ids: true
footnote_nr: 1
entity_output: as_char
toc_levels: 1..6
smart_quotes: lsquo,rsquo,ldquo,rdquo
enable_coderay: false
syntax_highlighter_opts:
disable : true
plugins:
- jekyll-seo-tag
# Exclude from processing.
# The following items will not be processed, by default. Create a custom list
# to override the default setting.
# exclude:
# - Gemfile
# - Gemfile.lock
# - node_modules
# - vendor/bundle/
# - vendor/cache/
# - vendor/gems/
# - vendor/ruby/
# Sass/SCSS
sass:
sass_dir: _sass
style: compressed # http://sass-lang.com/documentation/file.SASS_REFERENCE.html#output_style
# Outputting
permalink: /:categories/:title/
paginate: 5 # amount of posts to show
paginate_path: /page:num/
timezone: # http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# Plugins (previously gems:)
plugins:
- jekyll-paginate
- jekyll-sitemap
- jekyll-gist
- jekyll-feed
- jemoji
# mimic GitHub Pages with --safe
whitelist:
- jekyll-paginate
- jekyll-sitemap
- jekyll-gist
- jekyll-feed
- jemoji
# Archives
# Type
# - GitHub Pages compatible archive pages built with Liquid ~> type: liquid (default)
# - Jekyll Archives plugin archive pages ~> type: jekyll-archives
# Path (examples)
# - Archive page should exist at path when using Liquid method or you can
# expect broken links (especially with breadcrumbs enabled)
# - <base_path>/tags/my-awesome-tag/index.html ~> path: /tags/
# - <base_path/categories/my-awesome-category/index.html ~> path: /categories/
# - <base_path/my-awesome-category/index.html ~> path: /
# category_archive:
# type: liquid
# path: /categories/
# tag_archive:
# type: liquid
# path: /tags/
# https://github.com/jekyll/jekyll-archives
# jekyll-archives:
# enabled:
# - categories
# - tags
# layouts:
# category: archive-taxonomy
# tag: archive-taxonomy
# permalinks:
# category: /categories/:name/
# tag: /tags/:name/
# HTML Compression
# - http://jch.penibelst.de/
compress_html:
clippings: all
ignore:
envs: development
# Collections
collections:
docs:
output: true
permalink: /:collection/:path/
permalink: /:path/
# Defaults
defaults:
@@ -243,10 +66,4 @@ defaults:
path: ""
type: docs
values:
layout: single
read_time: false
author_profile: false
share: false
comments: false
sidebar:
nav: "docs"
layout: doc
+13
View File
@@ -0,0 +1,13 @@
items:
- title: GitHub release
img: https://img.shields.io/github/release/coralproject/talk.svg
link: https://github.com/coralproject/talk/releases
- title: ImageLayers Size
img: https://img.shields.io/imagelayers/image-size/coralproject/talk/latest.svg
link: https://hub.docker.com/r/coralproject/talk/
- title: Docker Pulls
img: https://img.shields.io/docker/pulls/coralproject/talk.svg
link: https://hub.docker.com/r/coralproject/talk/
- title: CircleCI
img: https://img.shields.io/circleci/project/github/coralproject/talk.svg
link: https://circleci.com/gh/coralproject/talk

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