Compare commits

...
179 Commits
Author SHA1 Message Date
Kim Gardner 1c23f72807 Merge pull request #666 from coralproject/bump-version-1-9-1
Bump version 1.9.1
2017-06-08 11:55:08 -04:00
Kim Gardner 8c1eaf09bc Bump version 1.9.1 2017-06-08 11:51:16 -04:00
Wyatt Johnson 6e82c180f7 Merge pull request #665 from coralproject/turn-on-off-topic
Turn on off-topic plugin by default
2017-06-08 09:42:08 -06:00
Kim Gardner 81f3cc808c Turn on off-topic plugin by default 2017-06-08 11:33:06 -04:00
Kim Gardner 55928dff08 Merge pull request #664 from coralproject/fix-reply-bugs
Fix bugs related to reply box
2017-06-08 11:09:10 -04:00
Kim Gardner 9695db0678 Merge branch 'master' into fix-reply-bugs 2017-06-08 11:04:04 -04:00
Kim Gardner 655e53a35c Merge pull request #662 from coralproject/bump-version
Update version number
2017-06-08 11:01:09 -04:00
Chi Vinh Le d61ede2396 Fix bugs related to reply box 2017-06-08 18:44:07 +07:00
Kim Gardner a40dd04c1b Update version number 2017-06-07 19:10:13 -04:00
Kim Gardner 9d25a4c14a Merge pull request #661 from coralproject/comment-stream-cleanup
Refinement of design for banned and suspend user messaging on comment stream.
2017-06-07 17:12:25 -04:00
Kim Gardner 801d0a92af Merge branch 'master' into comment-stream-cleanup 2017-06-07 17:07:39 -04:00
David Erwin 8bf86f34ee Merge pull request #654 from coralproject/off-topic-features
Off Topic Features
2017-06-07 15:43:03 -04:00
Kim Gardner 14a35033da Merge branch 'master' into comment-stream-cleanup 2017-06-07 14:33:29 -04:00
Chi Vinh Le 05da69d097 Remove falsely commited css styles 2017-06-08 01:23:22 +07:00
Chi Vinh Le ffce78ef60 Move Icon and translation service to plugin-api 2017-06-08 01:18:02 +07:00
Chi Vinh Le 6561f733b7 Use correct prefix 2017-06-08 01:17:36 +07:00
Chi Vinh Le b9758ba942 Fix overlap not clickable 2017-06-08 00:59:44 +07:00
Chi Vinh Le 4e56406c37 Fix tabsize in css 2017-06-08 00:49:18 +07:00
Chi Vinh Le 06858bf071 Move Slot and withReaction to beta 2017-06-08 00:42:21 +07:00
Chi Vinh Le 66e4f02a1a Selectors, resolving merge errors, better class names, removing comment reducer 2017-06-08 00:29:05 +07:00
IAmSamHankins c3d76c6a4f messaging style updates, and update to translation files. 2017-06-07 12:55:40 -04:00
Belen Curcio c8534a91a6 Merge branch 'off-topic-features' of github.com:coralproject/talk into off-topic-features 2017-06-07 13:53:54 -03:00
Belen Curcio 87ddfea689 indexes css 2017-06-07 13:53:33 -03:00
Belén Curcio 830441c531 Merge branch 'master' into off-topic-features 2017-06-07 13:24:58 -03:00
Riley Davis 7417d51973 Merge pull request #660 from coralproject/math-is-hard
percents have to be multiplied by 100
2017-06-07 10:22:45 -06:00
Belen Curcio 847a30ff92 Adding correct classNames 2017-06-07 13:19:36 -03:00
Riley Davis 9f4df35c3a percents have to be multiplied by 100 2017-06-07 10:15:50 -06:00
Belen Curcio f6ad9c9373 Adding camelcase for plugin reducers 🎉 <3 2017-06-07 13:14:52 -03:00
Belen Curcio 8872248d63 Addiing Stable API Architecture 2017-06-07 13:10:17 -03:00
Belen Curcio d780eea357 Merge branch 'off-topic-features' of github.com:coralproject/talk into off-topic-features 2017-06-07 12:38:31 -03:00
Belen Curcio 20aba92ad2 Linting 2017-06-07 12:37:59 -03:00
Chi Vinh Le 7a2fd95eca Merge branch 'master' into off-topic-features
Conflicts:
	client/coral-embed-stream/src/components/Comment.css
	client/coral-embed-stream/src/components/Comment.js
	client/coral-embed-stream/src/components/Stream.js
	client/coral-embed-stream/src/reducers/index.js
	client/coral-framework/helpers/plugins.js
2017-06-07 22:37:06 +07:00
Belen Curcio d7dfae89a3 Global classNames, and more 2017-06-07 12:05:53 -03:00
David Erwin fa33fe2ed2 Merge pull request #657 from coralproject/better-refetch-detection
Better refetch detection
2017-06-07 09:29:19 -04:00
Belen Curcio 456eedfd31 Clearing tags and naming convention 2017-06-07 10:04:52 -03:00
Belen Curcio 4725f07757 Viewing Options state and name conventions 2017-06-07 09:53:53 -03:00
Belen Curcio e24f6ca9f1 Renaming to commentClassNames 2017-06-07 09:28:12 -03:00
Chi Vinh Le ceb99cf205 Better refetch detection 2017-06-07 17:45:33 +07:00
Kiwi e47d3b085e Merge pull request #656 from coralproject/service-fix
Resolve facebook login issue
2017-06-07 16:57:19 +07:00
Belén Curcio 5e1fcc48e1 Merge branch 'master' into service-fix 2017-06-06 20:45:06 -03:00
Belen Curcio b5c9e92cc6 Adding streamBox slot and coral-plugin-viewing-options 🎉 2017-06-06 20:42:42 -03:00
Wyatt Johnson 71ff1c31f8 moved plugin to lazy load 2017-06-06 16:49:51 -06:00
IAmSamHankins cc76517ba8 minor copy changes in Configure tab on stream, and style updates to banned user message on stream 2017-06-06 17:37:14 -04:00
Belen Curcio 6f909c3451 Containers and Components structure 2017-06-06 18:21:03 -03:00
Belen Curcio 2202c8ae5b Fully working filter 2017-06-06 18:08:13 -03:00
Kim Gardner 6d8133b1b5 Merge pull request #655 from coralproject/move-config
move edit comment config
2017-06-06 16:57:48 -04:00
Riley Davis 2384ae4baa move edit comment config 2017-06-06 14:49:07 -06:00
Belen Curcio 1c7ad9a8ac Plugin reducers working again with reducer name 2017-06-06 17:29:56 -03:00
Belen Curcio c3360aa020 Plugin reducers working again 2017-06-06 17:23:35 -03:00
Belen Curcio 4aa7900769 Not connecting comment Component to the Store 2017-06-06 17:09:13 -03:00
Riley Davis 0dc61f33f5 Merge pull request #646 from coralproject/bulk-actions
bulk operations on User Detail comments
2017-06-06 13:26:28 -06:00
Riley Davis 8af0d929c2 Merge branch 'master' into bulk-actions 2017-06-06 13:15:04 -06:00
Riley Davis 4e19d5eecf don't have functions re-created every render 2017-06-06 13:11:31 -06:00
Riley Davis 5465563856 reload and clear selections 2017-06-06 12:52:26 -06:00
Belen Curcio b5a64bd2f9 Filter State 2017-06-06 15:45:56 -03:00
Kim Gardner b02782d532 Merge pull request #653 from coralproject/support-wss
Use wss when connection is secure
2017-06-06 14:39:35 -04:00
Chi Vinh Le 7cb05cebef Use wss when connection is secure 2017-06-07 01:32:37 +07:00
Kim Gardner 80d4ffa5bf Merge branch 'master' into bulk-actions 2017-06-06 14:13:47 -04:00
Riley Davis 9963f6d337 Merge pull request #651 from ebirving/active-tab-classnames
add semantic classname to active tab
2017-06-06 12:11:44 -06:00
Kim Gardner eff923424c Merge branch 'master' into bulk-actions 2017-06-06 14:11:16 -04:00
Erica Irving c3e32f37dc add coral- prefix as requested 2017-06-06 13:43:45 -04:00
Riley Davis 18f11a84cf Merge branch 'master' into active-tab-classnames 2017-06-06 11:14:47 -06:00
Riley Davis b0bdfff785 Merge pull request #649 from ebirving/respect-plugin-styles
add classnames to RespectButton component
2017-06-06 11:14:32 -06:00
Riley Davis dc55febac8 Merge branch 'master' into respect-plugin-styles 2017-06-06 11:09:36 -06:00
David Erwin 4d6e02228c Merge pull request #643 from coralproject/subscribe-to-comments
Subscribe to comment updates
2017-06-06 13:00:58 -04:00
Chi Vinh Le c50fe9f91d Merge branch 'master' into subscribe-to-comments
Conflicts:
	client/coral-embed-stream/src/components/Comment.js
2017-06-06 23:44:27 +07:00
Riley Davis 7d5a73bc95 Merge branch 'master' into respect-plugin-styles 2017-06-06 10:40:33 -06:00
Riley Davis 5f3220dba9 Merge pull request #652 from coralproject/css-override-classes
Css override classes + tab bugfix
2017-06-06 10:40:21 -06:00
Kim Gardner 700ab38cb0 Merge branch 'master' into css-override-classes 2017-06-06 12:34:13 -04:00
Riley Davis 6fcd003e76 remove console.log 2017-06-06 10:32:14 -06:00
Kiwi 0069d17672 Merge branch 'master' into subscribe-to-comments 2017-06-06 23:19:22 +07:00
Chi Vinh Le afdcdff51e Handle ignored user case 2017-06-06 23:18:37 +07:00
Erica Irving 3d8478dfb2 add semantic classname to active tab 2017-06-06 12:17:32 -04:00
Kim Gardner 71d2f44d38 Merge pull request #650 from coralproject/copy-updates
Copy changes
2017-06-06 12:07:40 -04:00
Kim Gardner 124ea19b52 Update en.yml 2017-06-06 12:03:12 -04:00
Erica Irving 512a463589 add classnames to RespectButton component 2017-06-06 11:28:28 -04:00
Belen Curcio e9cb61a534 Arrow for filtering 2017-06-06 12:13:26 -03:00
Belen Curcio a6db60096c Fully working, needs docs 2017-06-06 11:58:51 -03:00
Belen Curcio 9364272ffc Ádd classnames based on tags 2017-06-06 11:53:19 -03:00
Chi Vinh Le 1ceb72ca5c Fix error with tombstones 2017-06-06 21:26:43 +07:00
Belen Curcio 9e001f5ce9 addClassNames, removeClassNames, reducers and actions for Comment Component 2017-06-06 11:15:35 -03:00
Chi Vinh Le 0fcdd09f9f Lazily create apollo client 2017-06-06 20:50:07 +07:00
Belen Curcio 3ad3b3af1c Adding filters 2017-06-06 09:43:34 -03:00
Chi Vinh Le d3f7e6210c Simplify transition code 2017-06-06 19:06:42 +07:00
Chi Vinh Le 447ec7e524 Deeply pass commentIsIgnored 2017-06-06 19:05:18 +07:00
Chi Vinh Le f8ca14d3d3 Reset stream after refetching 2017-06-06 18:45:45 +07:00
Belen Curcio 5faa92e8b3 Label style, top level comments, no offtopic replies 2017-06-06 08:09:34 -03:00
Kim Gardner 17783c91c4 Merge branch 'master' into bulk-actions 2017-06-05 23:06:03 -04:00
Riley Davis ad4c4a0a5b fix for active tab not showing. user box classNames 2017-06-05 16:46:58 -06:00
Wyatt Johnson 7956c193c5 Merge branch 'master' into subscribe-to-comments 2017-06-05 16:38:28 -06:00
Wyatt Johnson 2aa3b7a5a0 Merge pull request #641 from coralproject/safari-cookie-time
Use same lifespan for cookies as for tokens
2017-06-05 16:38:13 -06:00
Wyatt Johnson 2f77842072 Merge branch 'master' into safari-cookie-time 2017-06-05 16:32:29 -06:00
Wyatt Johnson a595e94394 Removed parse-duration in favour of ms 2017-06-05 16:31:06 -06:00
riley d03647067d Merge branch 'master' into css-override-classes 2017-06-05 16:19:46 -06:00
Riley Davis fb537c9990 Merge pull request #648 from coralproject/StephanieDClark-master
Stephanie's changes to add classNames to Tab and TabBar
2017-06-05 16:18:19 -06:00
Riley Davis 8ea7ba9b69 Merge branch 'master' into css-override-classes 2017-06-05 16:13:45 -06:00
David Erwin 15297cd674 Update css naming convention 2017-06-05 18:12:38 -04:00
Riley Davis e46802910c Merge branch 'master' into StephanieDClark-master 2017-06-05 16:09:12 -06:00
riley 19b90910ba better tab classNames 2017-06-05 16:08:53 -06:00
David Erwin 291cee3af4 Merge pull request #647 from coralproject/auth-token-hooks
Added new tokenUserNotFound plugin hook
2017-06-05 18:05:56 -04:00
Wyatt Johnson 069c4c5e50 Merge branch 'master' into auth-token-hooks 2017-06-05 16:00:23 -06:00
Riley Davis 7d54fb60a8 use existing className attr instead of custom property 2017-06-05 15:57:34 -06:00
Wyatt Johnson e8143aea14 Added new tokenUserNotFound plugin hook 2017-06-05 15:52:32 -06:00
StephanieDClark 9e1d010db3 Adjusting tab classes 2017-06-05 14:48:50 -07:00
David Erwin 728d523b9e Begin adding css override classes 2017-06-05 17:25:27 -04:00
StephanieDClark 5838f5d486 fix lint errors 2017-06-05 14:00:21 -07:00
Riley Davis 1c8e1cc46c Merge branch 'master' into bulk-actions 2017-06-05 14:42:07 -06:00
Riley Davis c4a41af33b clear selected ids when panel is closed 2017-06-05 14:40:03 -06:00
StephanieDClark 5e463c00fd Merge branch 'master' into master 2017-06-05 13:35:36 -07:00
Kim Gardner 933a9371f3 Merge branch 'master' into safari-cookie-time 2017-06-05 16:35:23 -04:00
Kim Gardner 0be77fa5fd Merge pull request #645 from coralproject/fix-typos
Fix typos
2017-06-05 16:28:08 -04:00
Kim Gardner 49eecaa65b Fix typos 2017-06-05 16:22:17 -04:00
Riley Davis 39d8a5d01b bulk operations on User Detail comments 2017-06-05 14:20:34 -06:00
StephanieDClark 414e5d78bd Fix yarn lint error 2017-06-05 12:58:04 -07:00
Kiwi e9a6c3dfe3 Merge branch 'master' into subscribe-to-comments 2017-06-06 02:42:35 +07:00
StephanieDClark 3b7603ee31 add supplied classes to tabbar 2017-06-05 12:39:35 -07:00
StephanieDClark 8c89296007 add supplied classes to tab 2017-06-05 12:38:36 -07:00
StephanieDClark 32fe291847 Add classes to tabbar, tab 2017-06-05 12:37:13 -07:00
Chi Vinh Le 056ddc7010 Meaningful comment 2017-06-06 02:25:42 +07:00
Chi Vinh Le 2d0eec15c1 Readd falsely deleted part 2017-06-06 02:25:32 +07:00
Chi Vinh Le 2be73d1df2 Unsubscribe on unmount 2017-06-06 02:10:56 +07:00
Chi Vinh Le 3ad17f008f Refactor fetchMore updaters 2017-06-06 01:44:17 +07:00
David Erwin 547547d04c Merge pull request #638 from coralproject/auth-token-via-config
Use config auth_token if present on http headers
2017-06-05 14:22:48 -04:00
David Erwin 90d7e3706c Improve auth token logic. 2017-06-05 13:58:13 -04:00
Chi Vinh Le 9993350b7e Animate flash for new comments 2017-06-06 00:54:12 +07:00
David Erwin 7c66d846c2 Add auth token comment 2017-06-05 13:38:06 -04:00
Chi Vinh Le 6b0cdae183 Subscribe to comments 2017-06-05 20:51:08 +07:00
Chi Vinh Le c20df65ebc Use same lifespan for cookies as for tokens 2017-06-05 18:21:40 +07:00
Kiwi 7efdccb683 Merge branch 'master' into auth-token-via-config 2017-06-05 17:57:13 +07:00
Chi Vinh Le a9796d47cd Check login after loading config 2017-06-05 17:51:06 +07:00
Chi Vinh Le 7c145bbb3c Refactor import order and framework initialization 2017-06-05 17:49:43 +07:00
Chi Vinh Le ac9e70bc67 Add same getAuthToken logic to apollo transport 2017-06-05 17:48:09 +07:00
Chi Vinh Le defd28b20f Remove double login checks 2017-06-05 17:46:57 +07:00
Chi Vinh Le b05b54a991 Add auth_token default config 2017-06-05 17:46:30 +07:00
Kim Gardner 3c96b7eb1e Merge pull request #640 from coralproject/user-comment-history
user detail design pass
2017-06-02 17:27:13 -04:00
Kim Gardner 6e483842f1 Merge branch 'master' into user-comment-history 2017-06-02 17:22:48 -04:00
Kim Gardner 0276c00828 Merge pull request #639 from coralproject/key-trans-missing
Key translation missing
2017-06-02 17:08:48 -04:00
gaba c6efc586ae Merge branch 'master' into key-trans-missing 2017-06-02 14:03:33 -07:00
gaba 6de9c69668 Missing key translation. 2017-06-02 14:02:45 -07:00
Kim Gardner 396abaae22 Merge pull request #637 from coralproject/i18n-emails
I18n emails
2017-06-02 16:59:32 -04:00
gaba e98b9ff9bc Some translations missing and a folder that should not be there. 2017-06-02 13:50:48 -07:00
Gabriela Rodríguez Berón cbf83206b8 Merge branch 'master' into i18n-emails 2017-06-02 13:35:09 -07:00
Kim Gardner 664ace170b Merge pull request #618 from coralproject/user-comment-history
show comments on the UserDetail drawer
2017-06-02 16:10:22 -04:00
Kim Gardner bd60eca6f9 Merge branch 'master' into user-comment-history 2017-06-02 15:58:54 -04:00
Riley Davis e097b8eb21 design pass 2017-06-02 13:50:00 -06:00
Kim Gardner 058cab970b Merge pull request #636 from coralproject/story-146187883
UX Updates
2017-06-02 15:25:40 -04:00
Riley Davis 22c2843ae5 remove moderate link per designs 2017-06-02 13:12:59 -06:00
Riley Davis d8cb560755 add a tab for All/Rejected user detail comments 2017-06-02 13:02:22 -06:00
David Erwin ea3d8e141d Use config auth_token if present on http headers 2017-06-02 14:22:06 -04:00
gaba cf50a0e85c Translations missing. 2017-06-02 08:30:36 -07:00
gaba b9b62e5764 Use accepts library. 2017-06-02 08:24:21 -07:00
gaba eadbb03c15 Fix typo. 2017-06-02 06:54:01 -07:00
gaba b6dbb5975f Adds missing translations. 2017-06-01 17:26:49 -07:00
gaba fd1cf8121e Move start to the left. 2017-06-01 17:21:55 -07:00
gaba dcbd0fc167 Change signed in for logged in. 2017-06-01 17:13:55 -07:00
gaba 44d82f8b13 Merge branch 'master' into i18n-emails 2017-06-01 16:26:44 -07:00
gaba 27e6372fe2 Get language from the header. 2017-06-01 16:24:15 -07:00
gaba 686dc96c5f An attempt to guess language. In process. 2017-06-01 15:44:44 -07:00
Riley Davis 8d25b766ff Merge branch 'master' into user-comment-history 2017-06-01 15:47:54 -06:00
Riley Davis 555d54425a implement kiwi's pagination changes 2017-06-01 15:34:05 -06:00
Kim Gardner f3f7fad68f Merge pull request #635 from StephanieDClark/master
Add isReply to commentInputDetailArea api
2017-06-01 14:38:36 -04:00
Riley Davis 9483922781 Merge branch 'master' into master 2017-06-01 12:31:48 -06:00
Kim Gardner 91d415b434 Merge pull request #632 from coralproject/losowsky-patch-2
Updated description
2017-06-01 14:25:55 -04:00
Kim Gardner 1943b38544 Merge branch 'master' into losowsky-patch-2 2017-06-01 14:20:45 -04:00
Kim Gardner 9344490514 Merge pull request #631 from coralproject/losowsky-patch-1
Added iPhone 7
2017-06-01 14:20:28 -04:00
StephanieDClark 1bf2053cb8 Add isReply to commentInputDetailArea api 2017-06-01 11:01:57 -07:00
Andrew Losowsky 9fe7b961ca Updated description
Added mention of Plugins
2017-06-01 13:43:30 -04:00
Andrew Losowsky ac0db69c27 Added iPhone 7
What about the Android devices? It says Galaxy S5 but that's up to S7, same with Nexus.
2017-06-01 13:41:08 -04:00
Riley Davis d981b5a85b Merge branch 'master' into user-comment-history 2017-06-01 10:52:24 -06:00
gaba 276b2c7591 Merge branch 'master' into i18n-emails 2017-05-31 15:31:14 -07:00
gaba a126c94aa4 Adds service for translations in the backend. Clean yaml. 2017-05-31 13:55:27 -07:00
Riley Davis 576cbed8e9 no more errors 2017-05-31 13:44:38 -06:00
Kim Gardner bf15354aa2 Merge branch 'master' into user-comment-history 2017-05-31 15:17:00 -04:00
riley 6e458e0149 resolve missing files 2017-05-31 10:54:34 -06:00
Riley Davis 259ce9c9fb merge master 2017-05-31 10:36:01 -06:00
Riley Davis 6599ed2b4c Merge branch 'master' into user-comment-history 2017-05-30 15:53:36 -06:00
Riley Davis 29753a958b show comments on the UserDetail drawer 2017-05-30 15:50:56 -06:00
gaba 77bfd55af3 Merge branch 'i18n-refactor' into i18n-emails 2017-05-25 14:35:49 -04:00
gaba 09bd0a6735 Move templates to a folder for translations. 2017-05-25 10:43:34 -04:00
125 changed files with 2052 additions and 814 deletions
+1
View File
@@ -10,4 +10,5 @@ plugins/*
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
!plugins/coral-plugin-viewing-options
node_modules
+1
View File
@@ -23,5 +23,6 @@ plugins/*
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
!plugins/coral-plugin-viewing-options
**/node_modules/*
+35
View File
@@ -280,6 +280,41 @@ send data to the client. If the type in question contains args, clients may subs
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
#### Field: `tokenUserNotFound`
```js
tokenUserNotFound: async ({jwt, token}) => {
let profile = await someExternalService(token);
if (!profile) {
return null;
}
let user = await UserModel.findOneAndUpdate({
id: profile.id
}, {
id: profile.id,
username: profile.username,
lowercaseUsername: profile.username.toLowerCase(),
roles: [],
profiles: []
}, {
setDefaultsOnInsert: true,
new: true,
upsert: true
});
return user;
}
```
The `tokenUserNotFound` hook allows auth integrations to hook into the event
when a valid token is provided but a user can't be found in the database that
matches the provided id.
The function is async, and should return the user object that was created in the
database, or null if the user wasn't found. The `jwt` paramenter of the object
is the unpacked token, while `token` is the original jwt token string.
#### Field: `router`
```js
+3
View File
@@ -6,6 +6,7 @@ const helmet = require('helmet');
const authentication = require('./middleware/authentication');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const i18n = require('./services/i18n');
const enabled = require('debug').enabled;
const errors = require('./errors');
const {createGraphOptions} = require('./graph');
@@ -157,6 +158,8 @@ app.use('/', (err, req, res, next) => {
console.error(err);
}
i18n.init(req);
if (err instanceof errors.APIError) {
res.status(err.status);
res.render('error', {
+1 -1
View File
@@ -5,7 +5,7 @@
*/
const program = require('./commander');
const parseDuration = require('parse-duration');
const parseDuration = require('ms');
const Table = require('cli-table');
const AssetModel = require('../models/asset');
const mongoose = require('../services/mongoose');
+5 -6
View File
@@ -4,21 +4,20 @@
"es6": true,
"mocha": true
},
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -33,3 +33,21 @@ export const setSortOrder = (order) => ({
order
});
export const changeUserDetailStatuses = (tab) => {
let statuses;
if (tab === 'all') {
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
} else if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
export const toggleSelectCommentInUserDetail = (id, active) => {
return {
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
id
};
};
@@ -17,11 +17,11 @@ const ActionButton = ({type = '', active, ...props}) => {
return (
<Button
className={`${typeName} ${styles.actionButton} ${active ? styles[`${typeName}__active`] : ''}`}
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}
>{t(`modqueue.${text}`)}</Button>
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
);
};
@@ -189,6 +189,12 @@
width: 140px;
}
.minimal {
width: 45px;
min-width: 0;
float: left;
}
.approve__active {
box-shadow: none;
color: white;
@@ -8,3 +8,7 @@ export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG';
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
+2 -1
View File
@@ -9,9 +9,10 @@ import App from './components/App';
import 'react-mdl/extra/material.js';
import './graphql';
import {loadPluginsTranslations} from 'coral-framework/helpers/plugins';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
loadPluginsTranslations();
injectPluginsReducers();
render(
<ApolloProvider client={client} store={store}>
+1 -1
View File
@@ -13,5 +13,5 @@ export default {
community,
moderation,
install,
config
config,
};
+17 -2
View File
@@ -1,4 +1,4 @@
import {fromJS, Map} from 'immutable';
import {fromJS, Map, Set} from 'immutable';
import * as actions from '../constants/moderation';
const initialState = fromJS({
@@ -8,6 +8,9 @@ const initialState = fromJS({
commentId: null,
commentStatus: null,
userDetailId: null,
userDetailActiveTab: 'all',
userDetailStatuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
userDetailSelectedIds: new Set(),
banDialog: false,
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
sortOrder: 'REVERSE_CHRONOLOGICAL',
@@ -64,7 +67,19 @@ export default function moderation (state = initialState, action) {
case actions.VIEW_USER_DETAIL:
return state.set('userDetailId', action.userId);
case actions.HIDE_USER_DETAIL:
return state.set('userDetailId', null);
return state
.set('userDetailId', null)
.update('userDetailSelectedIds', (set) => set.clear());
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return state.update('userDetailSelectedIds', (set) => set.clear());
case actions.CHANGE_USER_DETAIL_STATUSES:
return state
.set('userDetailActiveTab', action.tab)
.set('userDetailStatuses', action.statuses);
case actions.SELECT_USER_DETAIL_COMMENT:
return state.update('userDetailSelectedIds', (set) => set.add(action.id));
case actions.UNSELECT_USER_DETAIL_COMMENT:
return state.update('userDetailSelectedIds', (set) => set.delete(action.id));
case actions.SET_SORT_ORDER:
return state.set('sortOrder', action.order);
default :
@@ -99,7 +99,7 @@
.inlineTextfield {
border-color: #ccc;
border-style: solid;
border-width: 0px 0px 1px 0px;
border-width: 0px 0px 1px 0px;
text-align: center;
font-size: inherit;
}
@@ -108,7 +108,7 @@
outline: none;
}
.charCountTexfield {
.charCountTexfield, .editCommentTimeframeTextfield {
width: 4em;
padding: 0px;
}
@@ -25,12 +25,6 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
const on = styles.enabledSetting;
const off = styles.disabledSetting;
const onChangeEditCommentWindowLength = (e) => {
const value = e.target.value;
const valueAsNumber = parseFloat(value);
const milliseconds = (!isNaN(valueAsNumber)) && (valueAsNumber * 1000);
updateSettings({editCommentWindowLength: milliseconds || value});
};
return (
<div className={styles.Configure}>
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
@@ -76,27 +70,6 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
onChangeWordlist={onChangeWordlist} />
{/* Edit Comment Timeframe */}
<Card className={styles.configSetting}>
<div className={styles.settingsHeader}>{t('configure.edit_comment_timeframe_heading')}</div>
<p>
{t('configure.edit_comment_timeframe_text_pre')}
&nbsp;
<input
style={{width: '3em'}}
className={styles.inlineTextfield}
type="number"
min="0"
onChange={onChangeEditCommentWindowLength}
placeholder="30"
defaultValue={(settings.editCommentWindowLength / 1000) /* saved as ms, rendered as seconds */}
pattern='[0-9]+([\.][0-9]*)?'
/>
&nbsp;
{t('configure.edit_comment_timeframe_text_post')}
</p>
</Card>
</div>
);
};
@@ -61,6 +61,13 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
}
};
const updateEditCommentWindowLength = (updateSettings) => (e) => {
const value = e.target.value;
const valueAsNumber = parseFloat(value);
const milliseconds = (!isNaN(valueAsNumber)) && (valueAsNumber * 1000);
updateSettings({editCommentWindowLength: milliseconds || value});
};
const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
// just putting this here for shorthand below
@@ -132,6 +139,25 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
</div>
</div>
</Card>
{/* Edit Comment Timeframe */}
<Card className={styles.configSetting}>
<div className={styles.settingsHeader}>{t('configure.edit_comment_timeframe_heading')}</div>
<p>
{t('configure.edit_comment_timeframe_text_pre')}
&nbsp;
<input
className={`${styles.inlineTextfield} ${styles.editCommentTimeframeTextfield}`}
type="number"
min="0"
onChange={updateEditCommentWindowLength(updateSettings)}
placeholder="30"
defaultValue={(settings.editCommentWindowLength / 1000) /* saved as ms, rendered as seconds */}
pattern='[0-9]+([\.][0-9]*)?'
/>
&nbsp;
{t('configure.edit_comment_timeframe_text_post')}
</p>
</Card>
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
<div className={styles.action}>
<Checkbox
@@ -23,6 +23,9 @@ const Comment = ({
viewUserDetail,
suspectWords,
bannedWords,
minimal,
selected,
toggleSelect,
...props
}) => {
const links = linkify.getMatches(comment.body);
@@ -47,17 +50,38 @@ const Comment = ({
})
.concat(linkText);
let selectionStateCSS;
if (minimal) {
selectionStateCSS = selected ? styles.minimalSelection : '';
} else {
selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
}
return (
<li
tabIndex={props.index}
className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}
className={`mdl-card ${selectionStateCSS} ${styles.Comment} ${styles.listItem} ${minimal ? styles.minimal : ''}`}
>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span className={styles.username} onClick={() => viewUserDetail(comment.user.id)}>
{comment.user.name}
</span>
{
!minimal && (
<span className={styles.username} onClick={() => viewUserDetail(comment.user.id)}>
{comment.user.name}
</span>
)
}
{
minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && (
<input
className={styles.bulkSelectInput}
type='checkbox'
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
)
}
<span className={styles.created}>
{timeago(comment.created_at || Date.now() - props.index * 60 * 1000)}
</span>
@@ -128,6 +152,7 @@ const Comment = ({
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
minimal={minimal}
key={i}
type={action}
user={comment.user}
@@ -154,14 +179,12 @@ const Comment = ({
</div>
</div>
</div>
<div>
<Slot
data={props.data}
root={props.root}
fill="adminCommentDetailArea"
comment={comment}
/>
</div>
<Slot
data={props.data}
root={props.root}
fill="adminCommentDetailArea"
comment={comment}
/>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
@@ -173,6 +196,7 @@ const Comment = ({
};
Comment.propTypes = {
minimal: PropTypes.bool,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
@@ -182,6 +206,7 @@ Comment.propTypes = {
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
toggleSelect: PropTypes.func,
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
@@ -187,10 +187,15 @@ export default class Moderation extends Component {
{moderation.userDetailId && (
<UserDetail
id={moderation.userDetailId}
hideUserDetail={hideUserDetail} />
hideUserDetail={hideUserDetail}
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment} />
)}
</div>
);
}
}
@@ -36,6 +36,47 @@
background-color: transparent;
font-size: 16px;
position: absolute;
width: 100%;
width: 90%;
outline: none;
}
.commentStatuses {
padding: 10px 0 0 0;
margin: 0;
height: 52px;
list-style: none;
box-sizing: border-box;
li {
display: inline-block;
margin: 0 10px;
cursor: pointer;
padding: 0 10px;
}
}
.active {
font-weight: bold;
border-bottom: 3px solid #F36451;
}
.bulkActionGroup {
height: 52px;
background-color: #efefef;
i {
margin-right: 0;
}
.bulkAction {
display: inline-block;
width: 48px;
height: 48px;
transform: scale(.7);
min-width: 0;
}
.bulkAction:last-child {
margin-left: -10px;
}
}
@@ -2,12 +2,24 @@ import React, {PropTypes} from 'react';
import {Button, Drawer} from 'coral-ui';
import styles from './UserDetail.css';
import Slot from 'coral-framework/components/Slot';
import Comment from './Comment';
import {actionsMap} from '../helpers/moderationQueueActionsMap';
export default class UserDetail extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
bannedWords: PropTypes.array.isRequired,
suspectWords: PropTypes.array.isRequired,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeStatus: PropTypes.func.isRequired,
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
}
copyPermalink = () => {
@@ -20,15 +32,55 @@ export default class UserDetail extends React.Component {
}
}
rejectThenReload = (info) => {
this.props.rejectComment(info).then(() => {
this.props.data.refetch();
});
}
acceptThenReload = (info) => {
this.props.acceptComment(info).then(() => {
this.props.data.refetch();
});
}
showAll = () => {
this.props.changeStatus('all');
}
showRejected = () => {
this.props.changeStatus('rejected');
}
render () {
const {root: {user, totalComments, rejectedComments}, hideUserDetail} = this.props;
const {
root: {
user,
totalComments,
rejectedComments,
comments: {nodes}
},
moderation: {
userDetailActiveTab: tab,
userDetailSelectedIds: selectedIds
},
bannedWords,
suspectWords,
toggleSelect,
bulkAccept,
bulkReject,
showBanUserDialog,
showSuspendUserDialog,
hideUserDetail
} = this.props;
const localProfile = user.profiles.find((p) => p.provider === 'local');
let profile;
if (localProfile) {
profile = localProfile.id;
}
let rejectedPercent = rejectedComments / totalComments;
let rejectedPercent = (rejectedComments / totalComments) * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
// if totalComments is 0, you're dividing by zero, which is naughty
@@ -62,8 +114,60 @@ export default class UserDetail extends React.Component {
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
</div>
</div>
{
selectedIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<Button
onClick={bulkAccept}
className={styles.bulkAction}
cStyle='approve'
icon='done'>
</Button>
<Button
onClick={bulkReject}
className={styles.bulkAction}
cStyle='reject'
icon='close'>
</Button>
{`${selectedIds.length} comments selected`}
</div>
)
}
<div>
{
nodes.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
const selected = selectedIds.indexOf(comment.id) !== -1;
return <Comment
key={i}
index={i}
comment={comment}
selected={false}
suspectWords={suspectWords}
bannedWords={bannedWords}
viewUserDetail={() => {}}
actions={actionsMap[status]}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
toggleSelect={toggleSelect}
currentAsset={null}
currentUserId={this.props.id}
minimal={true} />;
})
}
</div>
</Drawer>
);
}
}
@@ -178,16 +178,11 @@ span {
margin: 0 auto;
position: relative;
transition: all 200ms;
padding: 10px 0 0;
min-height: 220px;
padding: 10px 0;
min-height: 0;
.container {
padding: 0 14px;
min-height: 180px;
}
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
&:last-child {
@@ -247,6 +242,7 @@ span {
}
.created {
padding: 5px;
color: #262626;
font-size: 14px;
margin-left: 15px;
@@ -291,7 +287,6 @@ span {
@media (--big-viewport) {
.listItem {
border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
@@ -433,7 +428,6 @@ span {
.username {
color: blue;
text-decoration: underline;
padding: 5px;
cursor: pointer;
&:hover {
@@ -461,3 +455,15 @@ span {
position: relative;
}
}
.minimal {
margin: 0;
}
.minimalSelection {
background-color: #ecf4ff;
}
.bulkSelectInput {
cursor: pointer;
}
@@ -1,8 +1,30 @@
import React, {PropTypes} from 'react';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import {getDefinitionName} from 'coral-framework/utils';
import {
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail
} from 'coral-admin/src/actions/moderation';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import Comment from './Comment';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
}
hasNextPage
startCursor
endCursor
}
${Comment.fragments.comment}
`;
const pluginFragments = getSlotsFragments([
'userProfile',
@@ -14,17 +36,42 @@ class UserDetailContainer extends React.Component {
hideUserDetail: PropTypes.func.isRequired
}
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = (status) => {
const changes = this.props.moderation.userDetailSelectedIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
});
Promise.all(changes).then(() => {
this.props.data.refetch(); // some comments may have moved out of this tab
this.props.clearUserDetailSelections(); // un-select everything
});
}
bulkReject = () => {
this.bulkSetCommentStatus('REJECTED');
}
bulkAccept = () => {
this.bulkSetCommentStatus('ACCEPTED');
}
render () {
if (!('user' in this.props.root)) {
return null;
}
return <UserDetail {...this.props}/>;
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeStatus={this.props.changeUserDetailStatuses}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
{...this.props} />;
}
}
export const withUserDetailQuery = withQuery(gql`
query CoralAdmin_UserDetail($author_id: ID!) {
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
id
username
@@ -37,18 +84,40 @@ export const withUserDetailQuery = withQuery(gql`
}
totalComments: commentCount(query: {author_id: $author_id})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
comments: comments(query: {
author_id: $author_id,
statuses: $statuses
}) {
...CoralAdmin_Moderation_CommentConnection
}
${pluginFragments.spreads('root')}
}
${Comment.fragments.comment}
${pluginFragments.definitions('user')}
${pluginFragments.definitions('root')}
${commentConnectionFragment}
`, {
options: ({id}) => {
options: ({id, moderation: {userDetailStatuses: statuses}}) => {
return {
variables: {author_id: id}
variables: {author_id: id, statuses}
};
}
});
const mapStateToProps = (state) => ({
moderation: state.moderation.toJS()
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail
}, dispatch)
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUserDetailQuery,
withSetCommentStatus,
)(UserDetailContainer);
+8 -4
View File
@@ -14,14 +14,18 @@ if (window.devToolsExtension) {
middlewares.push(window.devToolsExtension());
}
const coralReducers = {
...mainReducer,
apollo: client.reducer()
};
const store = createStore(
combineReducers({
...mainReducer,
apollo: client.reducer()
}),
combineReducers(coralReducers),
{},
compose(...middlewares)
);
store.coralReducers = coralReducers;
window.coralStore = store;
export default store;
@@ -2,7 +2,6 @@ import {pym} from 'coral-framework';
import * as actions from '../constants/stream';
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
export const setCommentCountCache = (amount) => ({type: actions.SET_COMMENT_COUNT_CACHE, amount});
function removeParam(key, sourceURL) {
let rtn = sourceURL.split('?')[0];
@@ -38,3 +37,13 @@ export const viewAllComments = () => {
return {type: actions.VIEW_ALL_COMMENTS};
};
export const addCommentClassName = (className) => ({
type: actions.ADD_COMMENT_CLASSNAME,
className
});
export const removeCommentClassName = (idx) => ({
type: actions.REMOVE_COMMENT_CLASSNAME,
idx
});
@@ -97,3 +97,17 @@
.Wizard .textAlignRight {
text-align: right;
}
.commentInfoBar {
float: right;
}
@keyframes enter {
0% {background-color: rgba(0, 0, 0, 0);}
50% {background-color: rgba(255,255,0, 0.2);}
100% {background-color: rgba(0, 0, 0, 0);}
}
.enter {
animation: enter 1000ms;
}
@@ -1,13 +1,15 @@
import React, {PropTypes} from 'react';
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import TagLabel from 'coral-plugin-tag-label/TagLabel';
import Content from 'coral-plugin-commentcontent/CommentContent';
import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment';
import {TransitionGroup} from 'react-transition-group';
import cn from 'classnames';
import {
BestButton,
IfUserCanModifyBest,
@@ -17,15 +19,50 @@ import {
} from 'coral-plugin-best/BestButton';
import Slot from 'coral-framework/components/Slot';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu';
import classnames from 'classnames';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {EditableCommentContent} from './EditableCommentContent';
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
import {getEditableUntilDate} from './util';
import styles from './Comment.css';
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF');
const hasTag = (tags, lookupTag) => !!tags.filter((tag) => tag.name === lookupTag).length;
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second newest comment in
// the current reply list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
const replies = props.comment.replies;
if (replies && replies.nodes.length) {
const idCursors = [replies.nodes[replies.nodes.length - 1].id];
if (replies.nodes.length >= 2) {
idCursors.push(replies.nodes[replies.nodes.length - 2].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 replies = props.comment.replies;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = replies.nodes.findIndex((node) => node.id === idCursors[0]);
const prevInLine = replies.nodes[index - 1];
if (prevInLine) {
idCursors.push(prevInLine.id);
}
}
return {idCursors};
}
// hold actions links (e.g. Reply) along the comment footer
const ActionButton = ({children}) => {
@@ -36,7 +73,7 @@ const ActionButton = ({children}) => {
);
};
class Comment extends React.Component {
export default class Comment extends React.Component {
constructor(props) {
super(props);
@@ -49,9 +86,47 @@ class Comment extends React.Component {
// Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment)
isEditing: false,
replyBoxVisible: false,
animateEnter: false,
...resetCursors({}, props),
};
}
componentWillReceiveProps(next) {
const {comment: {replies: prevReplies}} = this.props;
const {comment: {replies: nextReplies}} = next;
if (
prevReplies && nextReplies &&
nextReplies.nodes.length < prevReplies.nodes.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextReplies.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextReplies.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
}
}
}
componentWillEnter(callback) {
callback();
const userId = this.props.currentUser ? this.props.currentUser.id : null;
if (this.props.comment.id.indexOf('pending') >= 0) {
return;
}
if (userId && this.props.comment.user.id === userId) {
// This comment was just added by currentUser.
if (Date.now() - Number(new Date(this.props.comment.created_at)) < 30 * 1000) {
return;
}
}
this.setState({animateEnter: true});
}
static propTypes = {
reactKey: PropTypes.string.isRequired,
@@ -67,6 +142,7 @@ class Comment extends React.Component {
addNotification: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
liveUpdates: PropTypes.bool.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
@@ -127,6 +203,50 @@ class Comment extends React.Component {
}
}
hasIgnoredReplies() {
return this.props.comment.replies &&
this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply));
}
loadNewReplies = () => {
const {replies, replyCount, id} = this.props.comment;
if (replyCount > replies.nodes.length) {
this.props.loadMore(id).then(() => {
this.setState(resetCursors(this.state, this.props));
});
return;
}
this.setState(resetCursors);
};
// getVisibileReplies returns a list containing comments
// which were authored by current user or comes before the `idCursor`.
getVisibileReplies() {
const {comment: {replies}, currentUser, liveUpdates} = this.props;
const idCursor = this.state.idCursors[0];
const userId = currentUser ? currentUser.id : null;
if (!replies) {
return [];
}
if (liveUpdates) {
return replies.nodes;
}
const view = [];
let pastCursor = false;
replies.nodes.forEach((comment) => {
if (idCursor && !pastCursor || comment.user.id === userId) {
view.push(comment);
}
if (comment.id === idCursor) {
pastCursor = true;
}
});
return view;
}
componentDidMount() {
this._isMounted = true;
if (this.editWindowExpiryTimeout) {
@@ -151,30 +271,35 @@ class Comment extends React.Component {
}
render () {
const {
comment,
parentId,
currentUser,
asset,
depth,
postComment,
addNotification,
showSignInDialog,
highlighted,
comment,
postFlag,
parentId,
ignoreUser,
highlighted,
postComment,
currentUser,
postDontAgree,
loadMore,
setActiveReplyBox,
activeReplyBox,
deleteAction,
addCommentTag,
removeCommentTag,
ignoreUser,
disableReply,
commentIsIgnored,
maxCharCount,
charCountEnable
addCommentTag,
addNotification,
charCountEnable,
showSignInDialog,
removeCommentTag,
liveUpdates,
commentIsIgnored,
commentClassNames = []
} = this.props;
const view = this.getVisibileReplies();
const hasMoreComments = comment.replies && (comment.replies.hasNextPage || comment.replies.nodes.length > view.length);
const replyCount = this.hasIgnoredReplies() ? '' : comment.replyCount;
const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary(
'DontAgreeActionSummary',
@@ -224,9 +349,39 @@ class Comment extends React.Component {
() => 'Failed to remove best comment tag'
);
/**
* classNamesToAdd
* adds classNames based on condition
* classnames is an array of objects with key as classnames and value as conditions
* i.e:
* {
* 'myClassName': { tags: ['STAFF']}
* }
*
* This will add myClassName to comments tagged with STAFF TAG.
* **/
const classNamesToAdd = commentClassNames.reduce((acc, className) => {
let res = [];
// Adding classNames based on tags
Object.keys(className).forEach((cn) => {
const condition = className[cn];
condition.tags.forEach((tag) => {
if (hasTag(comment.tags, tag)) {
res = [...res, cn];
}
});
});
// TODO: Compare rest of the comment obj to the condition if needed
return [...acc, ...res];
}, []);
return (
<div
className={commentClass}
className={cn(commentClass, 'talk-stream-comment-wrapper', classNamesToAdd, {[styles.enter]: this.state.animateEnter})}
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}
>
@@ -234,15 +389,15 @@ class Comment extends React.Component {
<div
className={highlighted === comment.id ? 'highlighted-comment' : ''}
>
<AuthorName author={comment.user} />
<AuthorName author={comment.user} className={'talk-stream-comment-user-name'} />
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
{commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel>
: null }
<span className={styles.bylineSecondary}>
<PubDate created_at={comment.created_at} />
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>(Edited)</span></span>
@@ -251,11 +406,13 @@ class Comment extends React.Component {
</span>
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
data={this.props.data}
root={this.props.root}
depth={depth}
comment={comment}
commentId={comment.id}
data={this.props.data}
root={this.props.root}
inline
/>
@@ -263,17 +420,17 @@ class Comment extends React.Component {
(comment.user.id === currentUser.id))
/* User can edit/delete their own comment for a short window after posting */
? <span className={classnames(styles.topRight)}>
? <span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={classnames(styles.link, {[styles.active]: this.state.isEditing})}
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
/* TopRightMenu allows currentUser to ignore other users' comments */
: <span className={classnames(styles.topRight, styles.topRightMenu)}>
: <span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
@@ -308,15 +465,6 @@ class Comment extends React.Component {
commentId={comment.id}
inline
/>
{!disableReply &&
<ActionButton>
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false}
/>
</ActionButton>}
<ActionButton>
<IfUserCanModifyBest user={currentUser}>
<BestButton
@@ -326,6 +474,15 @@ class Comment extends React.Component {
/>
</IfUserCanModifyBest>
</ActionButton>
{!disableReply &&
<ActionButton>
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false}
/>
</ActionButton>}
<Slot
fill="commentActions"
data={this.props.data}
@@ -369,53 +526,54 @@ class Comment extends React.Component {
assetId={asset.id}
/>
: null}
{comment.replies &&
comment.replies.nodes.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postComment={postComment}
editComment={this.props.editComment}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply}
/>;
})}
{comment.replies &&
<div className="coral-load-more-replies">
<LoadMore
topLevel={false}
replyCount={comment.replyCount}
moreComments={comment.replyCount > comment.replies.nodes.length}
loadMore={() => loadMore(comment.id)}
/>
</div>}
<TransitionGroup>
{view.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postComment={postComment}
editComment={this.props.editComment}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
showSignInDialog={showSignInDialog}
commentIsIgnored={commentIsIgnored}
liveUpdates={liveUpdates}
reactKey={reply.id}
key={reply.id}
comment={reply}
/>;
})}
</TransitionGroup>
<div className="coral-load-more-replies">
<LoadMore
topLevel={false}
replyCount={replyCount}
moreComments={hasMoreComments}
loadMore={this.loadNewReplies}
/>
</div>
</div>
);
}
}
export default Comment;
// return whether the comment is editable
function commentIsStillEditable (comment) {
const editing = comment && comment.editing;
@@ -41,10 +41,10 @@ export default class Embed extends React.Component {
return (
<div>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={totalCommentCount} /></Tab>
<Tab>{t('framework.my_profile')}</Tab>
<Tab restricted={!can(user, 'UPDATE_CONFIG')}>{t('framework.configure_stream')}</Tab>
<TabBar onChange={this.changeTab} activeTab={activeTab} className='talk-stream-tabbar'>
<Tab className={'talk-stream-comment-count-tab'} id='stream'><Count count={totalCommentCount}/></Tab>
<Tab className={'talk-stream-profile-tab'} id='profile'>{t('framework.my_profile')}</Tab>
<Tab className={'talk-stream-configuration-tab'} id='config' restricted={!can(user, 'UPDATE_CONFIG')}>{t('framework.configure_stream')}</Tab>
</TabBar>
{commentId &&
<Button
@@ -3,18 +3,22 @@ import React from 'react';
import t from 'coral-framework/services/i18n';
// Render in place of a Comment when the author of the comment is ignored
const IgnoredCommentTombstone = () => (
<div>
<hr aria-hidden={true} />
<p style={{
backgroundColor: '#F0F0F0',
textAlign: 'center',
padding: '1em',
color: '#3E4F71',
}}>
{t('framework.comment_is_ignored')}
</p>
</div>
);
class IgnoredCommentTombstone extends React.Component {
render() {
return (
<div>
<hr aria-hidden={true} />
<p style={{
backgroundColor: '#F0F0F0',
textAlign: 'center',
padding: '1em',
color: '#3E4F71',
}}>
{t('framework.comment_is_ignored')}
</p>
</div>
);
}
}
export default IgnoredCommentTombstone;
@@ -9,6 +9,9 @@ class LoadMore extends React.Component {
}
replyCountFormat = (count) => {
if (!count) {
return t('framework.view_all_replies_unknown_number');
}
if (count === 1) {
return t('framework.view_reply');
}
@@ -2,22 +2,14 @@ import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
const onLoadMoreClick = ({loadMore, commentCount, setCommentCountCache}) => (e) => {
e.preventDefault();
setCommentCountCache(commentCount);
loadMore();
};
const NewCount = (props) => {
const newComments = props.commentCount - props.commentCountCache;
const NewCount = ({count, loadMore}) => {
return <div className='coral-new-comments coral-load-more'>
{
props.commentCountCache && newComments > 0 ?
<button onClick={onLoadMoreClick(props)}>
{newComments === 1
? t('framework.new_count', newComments, t('framework.comment'))
: t('framework.new_count', newComments, t('framework.comments'))}
count ?
<button onClick={loadMore}>
{count === 1
? t('framework.new_count', count, t('framework.comment'))
: t('framework.new_count', count, t('framework.comments'))}
</button>
: null
}
@@ -25,8 +17,7 @@ const NewCount = (props) => {
};
NewCount.propTypes = {
commentCount: PropTypes.number.isRequired,
commentCountCache: PropTypes.number,
count: PropTypes.number.isRequired,
loadMore: PropTypes.func.isRequired,
};
@@ -1,21 +1,92 @@
import React, {PropTypes} from 'react';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
import Comment from '../containers/Comment';
import Comment from '../components/Comment';
import SuspendedAccount from './SuspendedAccount';
import RestrictedMessageBox
from 'coral-framework/components/RestrictedMessageBox';
import Slot from 'coral-framework/components/Slot';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import {can} from 'coral-framework/services/perms';
import {ModerationLink} from 'coral-plugin-moderation';
import RestrictedMessageBox
from 'coral-framework/components/RestrictedMessageBox';
import t, {timeago} from 'coral-framework/services/i18n';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import t, {timeago} from 'coral-framework/services/i18n';
import NewCount from './NewCount';
import {TransitionGroup} 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) {
const comments = props.root.asset.comments;
if (comments && comments.nodes.length) {
const idCursors = [comments.nodes[0].id];
if (comments.nodes[1]) {
idCursors.push(comments.nodes[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const comments = props.root.asset.comments;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
const nextInLine = comments.nodes[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
class Stream extends React.Component {
constructor(props) {
super(props);
this.state = resetCursors(this.state, props);
}
componentWillReceiveProps(next) {
const {root: {asset: {comments: prevComments}}} = this.props;
const {root: {asset: {comments: nextComments}}} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
}
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
}
}
}
viewNewComments = () => {
this.setState(resetCursors);
};
setActiveReplyBox = (reactKey) => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
@@ -24,8 +95,33 @@ class Stream extends React.Component {
}
};
// getVisibileComments returns a list containing comments
// which were authored by current user or comes after the `idCursor`.
getVisibleComments() {
const {root: {asset: {comments}}, auth: {user}} = this.props;
const idCursor = this.state.idCursors[0];
const userId = user ? user.id : null;
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.nodes.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor || comment.user.id === userId) {
view.push(comment);
}
});
return view;
}
render() {
const {
commentClassNames,
root: {asset, asset: {comments}, comment, me},
postComment,
addNotification,
@@ -34,13 +130,13 @@ class Stream extends React.Component {
deleteAction,
showSignInDialog,
addCommentTag,
removeCommentTag,
pluginProps,
ignoreUser,
auth: {loggedIn, user},
commentCountCache,
removeCommentTag,
pluginProps,
editName
} = this.props;
const view = this.getVisibleComments();
const open = asset.closedAt === null;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
@@ -97,8 +193,6 @@ class Stream extends React.Component {
postComment={this.props.postComment}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
setCommentCountCache={this.props.setCommentCountCache}
commentCountCache={commentCountCache}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
@@ -108,11 +202,17 @@ class Stream extends React.Component {
/>}
</div>
: <p>{asset.settings.closedMessage}</p>}
{loggedIn &&
{loggedIn && (
<ModerationLink
assetId={asset.id}
isAdmin={can(user, 'MODERATE_COMMENTS')}
/>}
/>
)}
<div className="talk-stream-wrapper-box">
<Slot fill="streamBox" />
</div>
{/* the highlightedComment is isolated after the user followed a permalink */}
{highlightedComment
@@ -139,19 +239,19 @@ class Stream extends React.Component {
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
liveUpdates={true}
/>
: <div>
: <div className="talk-stream-comments-container">
<NewCount
commentCount={asset.commentCount}
commentCountCache={commentCountCache}
setCommentCountCache={this.props.setCommentCountCache}
loadMore={this.props.loadNewComments}
count={comments.nodes.length - view.length}
loadMore={this.viewNewComments}
/>
<div className="embed__stream">
{comments && comments.nodes.map((comment) => {
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
commentClassNames={commentClassNames}
data={this.props.data}
root={this.props.root}
disableReply={!open}
@@ -178,9 +278,10 @@ class Stream extends React.Component {
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
liveUpdates={false}
/>;
})}
</div>
</TransitionGroup>
<LoadMore
topLevel={true}
moreComments={asset.comments.hasNextPage}
@@ -41,7 +41,10 @@ class SuspendedAccount extends Component {
<span>{
canEditName ?
t('framework.edit_name.msg')
: t('framework.banned_account_msg')
:
<span>
<b>{t('framework.banned_account_header')}</b><br/> {t('framework.banned_account_body')}
</span>
}</span>
{
canEditName ?
@@ -1,5 +1,5 @@
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const SET_COMMENT_COUNT_CACHE = 'SET_COMMENT_COUNT_CACHE';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
@@ -14,7 +14,7 @@ import Embed from '../components/Embed';
import Stream from './Stream';
import {setActiveTab} from '../actions/embed';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
import {viewAllComments} from '../actions/stream';
const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
@@ -33,18 +33,11 @@ class EmbedContainer extends React.Component {
// TODO: remove asset data from redux store.
fetchAssetSuccess(nextProps.root.asset);
const {setCommentCountCache, commentCountCache} = this.props;
const {asset} = nextProps.root;
if (commentCountCache === -1) {
setCommentCountCache(asset.commentCount);
}
}
}
componentDidUpdate(prevProps) {
if (!isEqual(prevProps.root.comment, this.props.root.comment)) {
if (!prevProps.root.comment && this.props.root.comment) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
@@ -87,7 +80,6 @@ export const withEmbedQuery = withQuery(EMBED_QUERY, {
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
@@ -103,7 +95,6 @@ const mapDispatchToProps = (dispatch) =>
setActiveTab,
viewAllComments,
fetchAssetSuccess,
setCommentCountCache
},
dispatch
);
+115 -148
View File
@@ -2,34 +2,94 @@ import React from 'react';
import {gql, compose} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {NEW_COMMENT_COUNT_POLL_INTERVAL, ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
} from 'coral-framework/graphql/mutations';
import update from 'immutability-helper';
import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user';
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
import {setActiveReplyBox} from '../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import {withFragments} from 'coral-framework/hocs';
import {Spinner} from 'coral-ui';
import {getDefinitionName} from 'coral-framework/utils';
import {
findCommentInEmbedQuery,
insertCommentIntoEmbedQuery,
removeCommentFromEmbedQuery,
insertFetchedCommentsIntoEmbedQuery,
} from '../graphql/utils';
const {showSignInDialog} = authActions;
const {addNotification} = notificationActions;
class StreamContainer extends React.Component {
getCounts = (variables) => {
return this.props.data.fetchMore({
query: LOAD_COMMENT_COUNTS_QUERY,
variables,
subscriptions = [];
// Apollo requires this, even though we don't use it...
updateQuery: (data) => data,
subscribeToUpdates() {
const sub1 = this.props.data.subscribeToMore({
document: COMMENTS_EDITED_SUBSCRIPTION,
variables: {
assetId: this.props.root.asset.id,
},
updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (this.props.auth.user && commentEdited.user.id === this.props.auth.user.id) {
return prev;
}
// Exit when comment is not in the query.
if (!findCommentInEmbedQuery(prev, commentEdited.id)) {
return prev;
}
if (['PREMOD', 'REJECTED'].includes(commentEdited.status)) {
return removeCommentFromEmbedQuery(prev, commentEdited.id);
}
},
});
};
const sub2 = this.props.data.subscribeToMore({
document: COMMENTS_ADDED_SUBSCRIPTION,
variables: {
assetId: this.props.root.asset.id,
},
updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (this.props.auth.user && commentAdded.user.id === this.props.auth.user.id) {
return prev;
}
// Exit if author is ignored.
if (
this.props.root.me &&
this.props.root.me.ignoredUsers.some(({id}) => id === commentAdded.user.id)) {
return prev;
}
// Exit when comment is already in the query.
if (findCommentInEmbedQuery(prev, commentAdded.id)) {
return prev;
}
return insertCommentIntoEmbedQuery(prev, commentAdded);
}
});
this.subscriptions.push(sub1, sub2);
}
unsubscribe() {
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions = [];
}
loadNewReplies = (parent_id) => {
const comment = this.props.root.comment
@@ -47,86 +107,11 @@ class StreamContainer extends React.Component {
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
if (!comments.nodes.length) {
return prev;
}
const updateNode = (node) =>
update(node, {
replies: {
endCursor: {$set: comments.endCursor},
nodes: {$apply: (nodes) => nodes
.concat(comments.nodes.filter(
(comment) => !nodes.some((node) => node.id === comment.id)
))
.sort(ascending)
},
},
});
// highlighted comment.
if (prev.comment) {
if (prev.comment.parent) {
return update(prev, {
comment: {
parent: {$apply: (comment) => updateNode(comment)},
}
});
}
return update(prev, {
comment: {$apply: (comment) => updateNode(comment)},
});
}
return update(prev, {
asset: {
comments: {
nodes: {
$apply: (nodes) => nodes.map(
(node) => node.id !== parent_id
? node
: updateNode(node)
)
},
},
},
});
return insertFetchedCommentsIntoEmbedQuery(prev, comments, parent_id);
},
});
}
loadNewComments = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: ADDTL_COMMENTS_ON_LOAD_MORE,
cursor: this.props.root.asset.comments.startCursor,
parent_id: null,
asset_id: this.props.root.asset.id,
sort: 'CHRONOLOGICAL',
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
if (!comments.nodes.length) {
return prev;
}
return update(prev, {
asset: {
comments: {
startCursor: {$set: comments.endCursor},
nodes: {$apply: (nodes) => comments.nodes.filter(
(comment) => !nodes.some((node) => node.id === comment.id)
)
.concat(nodes)
.sort(descending)
},
},
},
});
},
});
};
loadMoreComments = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
@@ -139,78 +124,77 @@ class StreamContainer extends React.Component {
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
if (!comments.nodes.length) {
return prev;
}
return update(prev, {
asset: {
comments: {
hasNextPage: {$set: comments.hasNextPage},
endCursor: {$set: comments.endCursor},
nodes: {$push: comments.nodes},
},
},
});
return insertFetchedCommentsIntoEmbedQuery(prev, comments);
},
});
};
componentDidMount() {
if (this.props.previousTab) {
this.props.data.refetch()
.then(({data: {asset: {commentCount}}}) => {
return this.props.setCommentCountCache(commentCount);
});
this.props.data.refetch();
}
this.countPoll = setInterval(() => {
this.getCounts(this.props.data.variables);
}, NEW_COMMENT_COUNT_POLL_INTERVAL);
this.subscribeToUpdates();
}
componentWillUnmount() {
this.unsubscribe();
clearInterval(this.countPoll);
}
render() {
if (this.props.refetching) {
return <Spinner />;
}
return <Stream
{...this.props}
loadMore={this.loadMore}
loadMoreComments={this.loadMoreComments}
loadNewComments={this.loadNewComments}
loadNewReplies={this.loadNewReplies}
/>;
}
}
const ascending = (a, b) => {
const dateA = new Date(a.created_at);
const dateB = new Date(b.created_at);
if (dateA < dateB) { return -1; }
if (dateA > dateB) { return 1; }
return 0;
};
const commentFragment = gql`
fragment CoralEmbedStream_Stream_comment on Comment {
id
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies(excludeIgnored: $excludeIgnored) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
}
hasNextPage
startCursor
endCursor
}
}
${Comment.fragments.comment}
`;
const descending = (a, b) => ascending(a, b) * -1;
const LOAD_COMMENT_COUNTS_QUERY = gql`
query CoralEmbedStream_LoadCommentCounts($assetUrl: String, , $commentId: ID!, $assetId: ID, $hasComment: Boolean!, $excludeIgnored: Boolean) {
comment(id: $commentId) @include(if: $hasComment) {
id
const COMMENTS_ADDED_SUBSCRIPTION = gql`
subscription onCommentAdded($assetId: ID!, $excludeIgnored: Boolean){
commentAdded(asset_id: $assetId){
parent {
id
replyCount(excludeIgnored: $excludeIgnored)
}
replyCount(excludeIgnored: $excludeIgnored)
...CoralEmbedStream_Stream_comment
}
asset(id: $assetId, url: $assetUrl) {
}
${commentFragment}
`;
const COMMENTS_EDITED_SUBSCRIPTION = gql`
subscription onCommentEdited($assetId: ID!){
commentEdited(asset_id: $assetId){
id
commentCount(excludeIgnored: $excludeIgnored)
comments(limit: 10) @skip(if: $hasComment) {
nodes {
id
replyCount(excludeIgnored: $excludeIgnored)
}
body
status
editing {
edited
}
user {
id
}
}
}
@@ -241,24 +225,6 @@ const LOAD_MORE_QUERY = gql`
${Comment.fragments.comment}
`;
const commentFragment = gql`
fragment CoralEmbedStream_Stream_comment on Comment {
id
...${getDefinitionName(Comment.fragments.comment)}
replyCount(excludeIgnored: $excludeIgnored)
replies {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
}
hasNextPage
startCursor
endCursor
}
}
${Comment.fragments.comment}
`;
const fragments = {
root: gql`
fragment CoralEmbedStream_Stream_root on RootQuery {
@@ -316,6 +282,7 @@ const fragments = {
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
refetching: state.embed.refetching,
commentCountCache: state.stream.commentCountCache,
activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId,
@@ -323,6 +290,7 @@ const mapStateToProps = (state) => ({
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
previousTab: state.embed.previousTab,
commentClassNames: state.stream.commentClassNames
});
const mapDispatchToProps = (dispatch) =>
@@ -331,7 +299,6 @@ const mapDispatchToProps = (dispatch) =>
addNotification,
setActiveReplyBox,
editName,
setCommentCountCache,
}, dispatch);
export default compose(
@@ -91,6 +91,9 @@ const extension = {
edited
editableUntil
}
parent {
id
}
}
`,
},
@@ -141,9 +144,12 @@ const extension = {
parent_id,
asset_id,
action_summaries: [],
tags,
tags: tags.map((t) => ({name: t, __typename: 'Tag'})),
status: null,
replyCount: 0,
parent: parent_id
? {id: parent_id}
: null,
replies: {
__typename: 'CommentConnection',
nodes: [],
@@ -165,7 +171,7 @@ const extension = {
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return prev;
}
return insertCommentIntoEmbedQuery(prev, parent_id, comment);
return insertCommentIntoEmbedQuery(prev, comment);
},
}
}),
+108 -41
View File
@@ -1,11 +1,33 @@
import update from 'immutability-helper';
function findAndInsertComment(parent, id, comment) {
const [connectionField, countField, action] = parent.comments
function applyToCommentsOrigin(root, callback) {
if (root.comment) {
if (root.comment.parent) {
return update(root, {
comment: {
parent: {
$apply: (node) => callback(node),
},
},
});
}
return update(root, {
comment: {
$apply: (node) => callback(node),
},
});
}
return update(root, {
asset: {$apply: (asset) => callback(asset)},
});
}
function findAndInsertComment(parent, comment) {
const [connectionField, countField, action] = parent.__typename === 'Asset'
? ['comments', 'commentCount', '$unshift']
: ['replies', 'replyCount', '$push'];
if (!id || parent.id === id) {
if (!comment.parent || parent.id === comment.parent.id) {
return update(parent, {
[connectionField]: {
nodes: {[action]: [comment]},
@@ -21,13 +43,13 @@ function findAndInsertComment(parent, id, comment) {
[connectionField]: {
nodes: {
$apply: (nodes) =>
nodes.map((node) => findAndInsertComment(node, id, comment))
nodes.map((node) => findAndInsertComment(node, comment))
},
},
});
}
export function insertCommentIntoEmbedQuery(root, id, comment) {
export function insertCommentIntoEmbedQuery(root, comment) {
// Increase total comment count by one.
root = update(root, {
@@ -35,30 +57,11 @@ export function insertCommentIntoEmbedQuery(root, id, comment) {
totalCommentCount: {$apply: (c) => c + 1},
},
});
if (root.comment) {
if (root.comment.parent) {
return update(root, {
comment: {
parent: {
$apply: (node) => findAndInsertComment(node, id, comment),
},
},
});
}
return update(root, {
comment: {
$apply: (node) => findAndInsertComment(node, id, comment),
},
});
}
return update(root, {
asset: {$apply: (asset) => findAndInsertComment(asset, id, comment)},
});
return applyToCommentsOrigin(root, (origin) => findAndInsertComment(origin, comment));
}
function findAndRemoveComment(parent, id) {
const [connectionField, countField] = parent.comments
const [connectionField, countField] = parent.__typename === 'Asset'
? ['comments', 'commentCount']
: ['replies', 'replyCount'];
@@ -78,7 +81,7 @@ function findAndRemoveComment(parent, id) {
};
if (parent[countField] && next.length !== connection.nodes.length) {
changes[countField] = {$set: changes[countField] - 1};
changes[countField] = {$set: parent[countField] - 1};
}
return update(parent, changes);
}
@@ -91,24 +94,88 @@ export function removeCommentFromEmbedQuery(root, id) {
totalCommentCount: {$apply: (c) => c - 1},
},
});
return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id));
}
if (root.comment) {
if (root.comment.parent) {
return update(root, {
comment: {
parent: {
$apply: (node) => findAndRemoveComment(node, id),
},
},
});
function findComment(nodes, callback) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (callback(node)) {
return node;
}
return update(root, {
comment: {
$apply: (node) => findAndRemoveComment(node, id),
if (node.replies) {
const find = findComment(node.replies.nodes, callback);
if (find){
return find;
}
}
}
return false;
}
export function findCommentInEmbedQuery(root, callbackOrId) {
let callback = callbackOrId;
if (typeof callbackOrId === 'string') {
callback = (node) => node.id === callbackOrId;
}
if (root.comment) {
if (callback(root.comment)) {
return root.comment;
}
if (root.comment.parent && callback(root.comment.parent)) {
return root.comment.parent;
}
}
if (!root.asset.comments) {
return false;
}
return findComment(root.asset.comments.nodes, callback);
}
const ascending = (a, b) => {
const dateA = new Date(a.created_at);
const dateB = new Date(b.created_at);
if (dateA < dateB) { return -1; }
if (dateA > dateB) { return 1; }
return 0;
};
function findAndInsertFetchedComments(parent, comments, parent_id) {
const isAsset = parent.__typename === 'Asset';
const connectionField = isAsset ? 'comments' : 'replies';
if (!parent_id && connectionField === 'comments' || parent.id === parent_id) {
return update(parent, {
[connectionField]: {
hasNextPage: {$set: comments.hasNextPage},
endCursor: {$set: comments.endCursor},
nodes: {$apply: (nodes) => {
if (isAsset) {
return nodes.concat(comments.nodes);
}
return nodes
.concat(comments.nodes.filter(
(comment) => !nodes.some((node) => node.id === comment.id)
))
.sort(ascending);
}},
},
});
}
return update(root, {
asset: {$apply: (asset) => findAndRemoveComment(asset, id)},
const connection = parent[connectionField];
if (!connection) {
return parent;
}
return update(parent, {
[connectionField]: {
nodes: {
$apply: (nodes) =>
nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id))
},
},
});
}
export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) {
return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id));
}
+6 -6
View File
@@ -2,30 +2,30 @@ import React from 'react';
import {render} from 'react-dom';
import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/services/client';
import {checkLogin} from 'coral-framework/actions/auth';
import './graphql';
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
import reducers from './reducers';
import {getStore, injectReducers} from 'coral-framework/services/store';
import {getClient} from 'coral-framework/services/client';
import AppRouter from './AppRouter';
import {pym} from 'coral-framework';
import {loadPluginsTranslations} from 'coral-framework/helpers/plugins';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
import reducers from './reducers';
const store = getStore();
const client = getClient();
loadPluginsTranslations();
injectPluginsReducers();
injectReducers(reducers);
// Don't run this in the popup.
if (!window.opener) {
store.dispatch(checkLogin());
pym.sendMessage('getConfig');
pym.onMessage('config', (config) => {
store.dispatch(addExternalConfig(JSON.parse(config)));
store.dispatch(checkLogin());
});
}
@@ -3,6 +3,8 @@ import * as actions from '../constants/embed';
const initialState = {
activeTab: 'stream',
previousTab: '',
refetching: false,
refetchRequestId: 0,
};
export default function stream(state = initialState, action) {
@@ -13,6 +15,23 @@ export default function stream(state = initialState, action) {
activeTab: action.tab,
previousTab: state.activeTab,
};
case 'APOLLO_QUERY_INIT':
if (action.queryString.indexOf('query CoralEmbedStream_Embed(') >= 0) {
return {
...state,
refetching: action.isRefetch ? true : state.refetching,
refetchRequestId: action.isRefetch ? action.requestId : state.refetchRequestId,
};
}
return state;
case 'APOLLO_QUERY_RESULT':
if (action.operationName === 'CoralEmbedStream_Embed') {
return {
...state,
refetching: action.requestId === state.refetchRequestId ? false : state.refetching,
};
}
return state;
default:
return state;
}
@@ -4,6 +4,6 @@ import stream from './stream';
export default {
embed,
config,
stream,
config
};
@@ -1,4 +1,5 @@
import * as actions from '../constants/stream';
import * as authActions from 'coral-framework/constants/auth';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
@@ -16,29 +17,42 @@ function getQueryVariable(variable) {
const initialState = {
activeReplyBox: '',
commentCountCache: -1,
assetId: getQueryVariable('asset_id'),
assetUrl: getQueryVariable('asset_url'),
commentId: getQueryVariable('comment_id'),
commentClassNames: []
};
export default function stream(state = initialState, action) {
switch (action.type) {
case authActions.LOGOUT:
return {
...state,
activeReplyBox: '',
};
case actions.SET_ACTIVE_REPLY_BOX:
return {
...state,
activeReplyBox: action.id,
};
case actions.SET_COMMENT_COUNT_CACHE:
return {
...state,
commentCountCache: action.amount,
};
case actions.VIEW_ALL_COMMENTS:
return {
...state,
commentId: '',
};
case actions.ADD_COMMENT_CLASSNAME :
return {
...state,
commentClassNames: [...state.commentClassNames, action.className]
};
case actions.REMOVE_COMMENT_CLASSNAME :
return {
...state,
commentClassNames: [
...state.commentClassNames.slice(0, action.idx),
...state.commentClassNames.slice(action.idx + 1)
]
};
default:
return state;
}
+17 -9
View File
@@ -119,7 +119,7 @@ hr {
.coral-plugin-questionbox-info {
top: 0;
border: 0;
background: rgb(213,213,213);
background: #F0F0F0;
color: black;
width: 100%;
text-align: left;
@@ -134,26 +134,25 @@ hr {
.coral-plugin-questionbox-icon.bubble{
position: absolute;
top: 11px;
top: 8px;
left: 10px;
color: #949393;
font-size: 20px;
color: #9E9E9E;
font-size: 24px;
z-index: 0;
}
.coral-plugin-questionbox-icon.person{
z-index: 2;
top: 20px;
left: 15px;
top: 12px;
left: 12px;
position: absolute;
font-size: 24px;
color: white;
font-size: 33px;
color: #262626;
}
.coral-plugin-questionbox-box {
position: relative;
border: 0;
background: black;
color: white;
padding: 20px;
margin-left: 0px !important;
@@ -178,6 +177,15 @@ hr {
display: none;
}
.talk-stream-wrapper-box {
padding: 10px 0;
}
.talk-stream-comments-container {
position: relative;
z-index: 0;
}
.commentStream {
/* prevent absolutely positioned final permalink popover from being clipped */
padding-bottom: 50px;
+1
View File
@@ -147,6 +147,7 @@ function configurePymParent(pymParent, opts) {
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
* @param {String} [opts.asset_url] - Asset URL
* @param {String} [opts.asset_id] - Asset ID
* @param {String} [opts.auth_token] - (optional) A jwt representing the session
*/
Talk.render = function(el, opts) {
if (!el) {
@@ -1,6 +1,8 @@
.message {
background: #D8D8D8;
padding: 25px;
margin-bottom: 8px;
background-color: white;
border: 1px #F0F0F0 solid;
text-align: center;
font-weight: 100;
}
+2 -2
View File
@@ -4,9 +4,9 @@ import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
function Slot ({fill, inline = false, plugin_config: config, ...rest}) {
function Slot ({fill, inline = false, className, plugin_config: config, ...rest}) {
return (
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug})}>
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug}, className)}>
{getSlotElements(fill, {...rest, config})}
</div>
);
@@ -0,0 +1 @@
export {default as Slot} from './Slot';
@@ -0,0 +1,16 @@
const regExp = /[-\s]+(.)?/g;
/**
* Convert dash separated strings to camel case.
*
* @param {String} str
* @return {String}
*/
export default function camelize(str) {
return str.replace(regExp, toUpper);
}
function toUpper(match, c) {
return c ? c.toUpperCase() : '';
}
+14 -9
View File
@@ -1,18 +1,14 @@
import React from 'react';
import merge from 'lodash/merge';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import uniq from 'lodash/uniq';
import pick from 'lodash/pick';
import merge from 'lodash/merge';
import plugins from 'pluginsConfig';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
import {loadTranslations} from 'coral-framework/services/i18n';
export const pluginReducers = merge(
...plugins
.filter((o) => o.module.reducer)
.map((o) => ({...o.module.reducer}))
);
import {injectReducers} from 'coral-framework/services/store';
import camelize from './camelize';
/**
* Returns React Elements for given slot.
@@ -98,3 +94,12 @@ function getTranslations() {
export function loadPluginsTranslations() {
getTranslations().forEach((t) => loadTranslations(t));
}
export function injectPluginsReducers() {
const reducers = merge(
...plugins
.filter((o) => o.module.reducer)
.map((o) => ({[camelize(o.plugin)] : o.module.reducer}))
);
injectReducers(reducers);
}
+28 -6
View File
@@ -1,6 +1,30 @@
import bowser from 'bowser';
import * as Storage from './storage';
import merge from 'lodash/merge';
import {getStore} from 'coral-framework/services/store';
/**
* getAuthToken returns the active auth token or null
* Note: this method does not have access to the cookie based token used by
* browsers that don't allow us to use cross domain iframe local storage.
* @return {string|null}
*/
export const getAuthToken = () => {
let state = getStore().getState();
if (state.config.auth_token) {
// if an auth_token exists in config, use it.
return state.config.auth_token;
} else if (!bowser.safari && !bowser.ios) {
// Use local storage auth tokens where there's a stable api.
return Storage.getItem('token');
}
return null;
};
const buildOptions = (inputOptions = {}) => {
const defaultOptions = {
@@ -14,12 +38,10 @@ const buildOptions = (inputOptions = {}) => {
let options = merge({}, defaultOptions, inputOptions);
if (!bowser.safari && !bowser.ios) {
let authorization = Storage.getItem('token');
if (authorization) {
options.headers.Authorization = `Bearer ${authorization}`;
}
// Apply authToken header
let authToken = getAuthToken();
if (authToken !== null) {
options.headers.Authorization = `Bearer ${authToken}`;
}
if (options.method.toLowerCase() !== 'get') {
-2
View File
@@ -2,12 +2,10 @@ import auth from './auth';
import user from './user';
import asset from './asset';
import {reducer as commentBox} from '../../coral-plugin-commentbox';
import {pluginReducers} from '../helpers/plugins';
export default {
auth,
user,
asset,
commentBox,
...pluginReducers
};
+30 -22
View File
@@ -1,28 +1,36 @@
import ApolloClient, {addTypename} from 'apollo-client';
import {networkInterface} from './transport';
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
let client;
// TODO: replace absolute reference with something loaded from the store/page.
// const wsClient = new SubscriptionClient('ws://localhost:3000/api/v1/live', {
// reconnect: true
// });
// const networkInterface = addGraphQLSubscriptions(
// getNetworkInterface(),
// wsClient,
// );
export const client = new ApolloClient({
connectToDevTools: true,
addTypename: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
}
return null;
},
networkInterface
});
export function getClient() {
if (client) {
return client;
}
export default client;
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
const wsClient = new SubscriptionClient(`${protocol}://${location.host}/api/v1/live`, {
reconnect: true
});
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient,
);
client = new ApolloClient({
connectToDevTools: true,
addTypename: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
}
return null;
},
networkInterface: networkInterfaceWithSubscriptions,
});
return client;
}
+1 -1
View File
@@ -7,7 +7,7 @@ import esTA from '../../../node_modules/timeago.js/locales/es';
import en from '../../../locales/en.yml';
import es from '../../../locales/es.yml';
// Translations are happening at https://www.transifex.com/the-coral-project/talk-1/dashboard/.
// Translations are happening at https://translate.lingohub.com/the-coral-project/dashboard
const defaultLanguage = 'en';
const translations = {...en, ...es};
+23 -23
View File
@@ -1,28 +1,7 @@
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from '../reducers';
import {client} from './client';
const apolloErrorReporter = () => (next) => (action) => {
if (action.type === 'APOLLO_QUERY_ERROR') {
console.error(action.error);
}
return next(action);
};
const middlewares = [
applyMiddleware(
client.middleware(),
thunk,
apolloErrorReporter,
),
];
if (window.devToolsExtension) {
// we can't have the last argument of compose() be undefined
middlewares.push(window.devToolsExtension());
}
import {getClient} from './client';
export function injectReducers(reducers) {
const store = getStore();
@@ -35,9 +14,30 @@ export function getStore() {
return window.coralStore;
}
const apolloErrorReporter = () => (next) => (action) => {
if (action.type === 'APOLLO_QUERY_ERROR') {
console.error(action.error);
}
return next(action);
};
const middlewares = [
applyMiddleware(
getClient().middleware(),
thunk,
apolloErrorReporter,
),
];
if (window.devToolsExtension) {
// we can't have the last argument of compose() be undefined
middlewares.push(window.devToolsExtension());
}
const coralReducers = {
...mainReducer,
apollo: client.reducer()
apollo: getClient().reducer()
};
window.coralStore = createStore(
@@ -1,16 +0,0 @@
import {print} from 'graphql-tag/printer';
// quick way to add the subscribe and unsubscribe functions to the network interface
const addGraphQLSubscriptions = (networkInterface, wsClient) => {
return Object.assign(networkInterface, {
subscribe: (request, handler) => wsClient.subscribe({
query: print(request.query),
variables: request.variables,
}, handler),
unsubscribe: (id) => {
wsClient.unsubscribe(id);
},
});
};
export default addGraphQLSubscriptions;
+4 -4
View File
@@ -1,6 +1,5 @@
import {createNetworkInterface} from 'apollo-client';
import * as Storage from '../helpers/storage';
import bowser from 'bowser';
import {getAuthToken} from '../helpers/request';
//==============================================================================
// NETWORK INTERFACE
@@ -23,8 +22,9 @@ networkInterface.use([{
req.options.headers = {}; // Create the header object if needed.
}
if (!bowser.safari && !bowser.ios) {
req.options.headers['authorization'] = `Bearer ${Storage.getItem('token')}`;
let authToken = getAuthToken();
if (authToken) {
req.options.headers['authorization'] = `Bearer ${authToken}`;
}
next();
-1
View File
@@ -1 +0,0 @@
export {withReaction} from '../coral-framework/hocs';
+1 -18
View File
@@ -36,18 +36,10 @@ class CommentBox extends React.Component {
}
};
}
static get defaultProps() {
return {
setCommentCountCache: () => {}
};
}
postComment = ({body}) => {
const {
commentPostedHandler,
postComment,
setCommentCountCache,
commentCountCache,
isReply,
assetId,
parentId,
addNotification,
@@ -60,8 +52,6 @@ class CommentBox extends React.Component {
...this.props.commentBox
};
!isReply && setCommentCountCache(commentCountCache + 1);
// Execute preSubmit Hooks
this.state.hooks.preSubmit.forEach((hook) => hook());
@@ -74,19 +64,12 @@ class CommentBox extends React.Component {
notifyForNewCommentStatus(addNotification, postedComment.status);
if (postedComment.status === 'REJECTED') {
!isReply && setCommentCountCache(commentCountCache);
} else if (postedComment.status === 'PREMOD') {
!isReply && setCommentCountCache(commentCountCache);
}
if (commentPostedHandler) {
commentPostedHandler();
}
})
.catch((err) => {
console.error(err);
!isReply && setCommentCountCache(commentCountCache);
});
this.setState({postedCount: this.state.postedCount + 1});
@@ -167,6 +150,7 @@ class CommentBox extends React.Component {
fill="commentInputDetailArea"
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
isReply={isReply}
inline
/>}
cancelButtonClicked={cancelButtonClicked}
@@ -189,7 +173,6 @@ CommentBox.propTypes = {
authorId: PropTypes.string.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
setCommentCountCache: PropTypes.func,
};
const mapStateToProps = ({commentBox}) => ({commentBox});
@@ -1,4 +1,6 @@
.moderationLink {
display: inline;
a {
color: #679af3;
text-decoration: none;
+5 -2
View File
@@ -9,6 +9,10 @@ class ReplyBox extends Component {
document.getElementById('replyText').focus();
}
cancelReply = () => {
this.props.setActiveReplyBox('');
};
render() {
const {
styles,
@@ -18,7 +22,6 @@ class ReplyBox extends Component {
addNotification,
parentId,
commentPostedHandler,
setActiveReplyBox,
maxCharCount,
charCountEnable
} = this.props;
@@ -28,7 +31,7 @@ class ReplyBox extends Component {
charCountEnable={charCountEnable}
commentPostedHandler={commentPostedHandler}
parentId={parentId}
cancelButtonClicked={setActiveReplyBox}
cancelButtonClicked={this.cancelReply}
addNotification={addNotification}
authorId={authorId}
assetId={assetId}
@@ -1,5 +1,5 @@
import React, {Component, PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
import styles from './IgnoredUsers.css';
export class IgnoredUsers extends Component {
@@ -18,7 +18,7 @@ export class IgnoredUsers extends Component {
<div>
{
users.length
? <p>Because you ignored these, you do not see their comments.</p>
? <p>{t('framework.because_you_ignored')}</p>
: null
}
<dl className={styles.ignoredUserList}>
@@ -29,7 +29,7 @@ export class IgnoredUsers extends Component {
<dd className={styles.stopListening}>
<a
onClick={() => stopIgnoring({id})}
className={styles.link}>Stop ignoring</a>
className={styles.link}>{t('framework.stop_ignoring')}</a>
</dd>
</span>
))
+13 -3
View File
@@ -1,15 +1,25 @@
.drawer {
max-width: 700px;
min-width: 400px;
padding: 20px;
min-width: 550px;
position: fixed;
top: 0;
right: 0;
right: -17px;
bottom: 0;
background-color: white;
transition: transform 500ms ease-in-out;
box-shadow: -3px 0px 4px 0px rgba(0,0,0,0.15);
z-index: 10000;
.content {
position: absolute;
padding: 20px;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow-y: scroll;
overflow-x: hidden;
}
}
.closeButton {
+3 -1
View File
@@ -6,7 +6,9 @@ const Drawer = ({children, handleClickOutside}) => {
return (
<div className={styles.drawer}>
<div className={styles.closeButton} onClick={handleClickOutside}>×</div>
{children}
<div className={styles.content}>
{children}
</div>
</div>
);
};
+2 -3
View File
@@ -1,13 +1,12 @@
import React from 'react';
import styles from './Tab.css';
export default ({children, tabId, active, onTabClick, cStyle = 'base'}) => (
export default ({children, tabId, active, onTabClick, cStyle = 'base', ...props}) => (
<li
key={tabId}
className={active ? styles[`${cStyle}--active`] : ''}
className={`${active ? `${styles[`${cStyle}--active`]} coral-tab-active` : ''} coral-tab ${props.className}`}
onClick={() => onTabClick(tabId)}
>
{children}
</li>
);
+2 -2
View File
@@ -17,13 +17,13 @@ class TabBar extends React.Component {
const {children, activeTab, cStyle = 'base'} = this.props;
return (
<div>
<ul className={`${styles.base} ${cStyle ? styles[cStyle] : ''}`}>
<ul className={`${styles.base} ${cStyle ? styles[cStyle] : ''} ${this.props.className}`}>
{React.Children.toArray(children)
.filter((child) => !child.props.restricted)
.map((child, tabId) =>
React.cloneElement(child, {
tabId,
active: tabId === activeTab,
active: child.props.id === activeTab,
onTabClick: this.handleClickTab,
cStyle
})
+4 -2
View File
@@ -8,7 +8,7 @@ summary:
[![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
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. Mozilla's open-source, plugin-based tool Talk rethinks how moderation, comment display, and conversation function online, creating the opportunity for better, smarter discussions designed to fit your needs. [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.
@@ -30,6 +30,8 @@ See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CO
- iPad
- iPad Pro
- iPhone 7 Plus
- iPhone 7
- iPhone 6 Plus
- iPhone 6
- iPhone 5
@@ -42,7 +44,7 @@ See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CO
### Android Devices
- Galaxy S5
- Galaxy S5x
- Nexus 5X
- Nexus 6P
+6
View File
@@ -340,6 +340,12 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
// Execute the edit.
const comment = await CommentsService.edit(id, context.user.id, {body, status});
if (context.pubsub) {
// Publish the edited comment via the subscription.
context.pubsub.publish('commentEdited', comment);
}
return comment;
};
+1 -1
View File
@@ -49,7 +49,7 @@ const Comment = {
},
async editing(comment, _, {loaders: {Settings}}) {
const settings = await Settings.load();
const editableUntil = new Date(Number(comment.created_at) + settings.editCommentWindowLength);
const editableUntil = new Date(Number(new Date(comment.created_at)) + settings.editCommentWindowLength);
return {
edited: comment.edited,
editableUntil: editableUntil
+3
View File
@@ -1,6 +1,9 @@
const Subscription = {
commentAdded(comment) {
return comment;
},
commentEdited(comment) {
return comment;
}
};
+5
View File
@@ -25,6 +25,11 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
filter: (comment) => comment.asset_id === args.asset_id
},
}),
commentEdited: (options, args) => ({
commentEdited: {
filter: (comment) => comment.asset_id === args.asset_id
},
}),
});
/**
+4
View File
@@ -148,6 +148,9 @@ enum ACTION_TYPE {
# CommentsQuery allows the ability to query comments by a specific methods.
input CommentsQuery {
# Author of the comments
author_id: ID
# Current status of a comment. Requires the `ADMIN` role.
statuses: [COMMENT_STATUS!]
@@ -880,6 +883,7 @@ type RootMutation {
type Subscription {
commentAdded(asset_id: ID!): Comment
commentEdited(asset_id: ID!): Comment
}
################################################################################
+41 -13
View File
@@ -3,17 +3,17 @@ en:
are_you_sure: "Are you sure you would like to ban {0}?"
ban_user: "Ban User?"
banned_user: "Banned User"
cancel: Cancel
cancel: "Cancel"
note: "Note: Banning this user will also place this comment in the Rejected queue."
yes_ban_user: "Yes, Ban User"
bio_offensive: "This bio is offensive"
cancel: Cancel
cancel: "Cancel"
characters_remaining: "characters remaining"
comment:
anon: Anonymous
anon: "Anonymous"
ban_user: "Ban User"
comment: "Post a comment"
flagged: flagged
flagged: "flagged"
view_context: "View context"
comment_box:
post: "Post"
@@ -87,7 +87,7 @@ en:
custom_css_url_desc: "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external."
dashboard: Dashboard
days: Days
description: "As an admin you may customize the settings for the comment stream for this asset"
description: "As an admin, you can customize the settings for the comment stream for this story:"
domain_list_text: "Enter the domains you would like to permit for Talk e.g. your local staging and production environments (ex. localhost:3000 staging.domain.com domain.com)."
domain_list_title: "Permitted Domains"
edit_comment_timeframe_heading: "Edit Comment Timeframe"
@@ -101,13 +101,13 @@ en:
enable_premod: "Enable Premoderation"
enable_premod_description: "Moderators must approve any comment before its published."
enable_premod_links_description: "Moderators must approve any comment containing a link before its published."
enable_questionbox: "Ask readers a question"
enable_questionbox: "Ask Readers a Question"
enable_questionbox_description: "This question will appear at the top of this comment stream. Ask readers about a certain issue in the article or pose discussion questions etc."
hours: Hours
include_comment_stream: "Include Comment Stream Description for Readers"
include_comment_stream_desc: "Write a message to be added to the top of your comment stream. Pose a topic include community guidelines etc."
include_text: "Include your text here."
include_question_here: "Write your question here."
include_question_here: "Write your question here:"
moderate: Moderate
moderation_settings: "Moderation Settings"
open: "Open"
@@ -126,7 +126,20 @@ en:
title: "Configure Comment Stream"
weeks: Weeks
wordlist: "Banned Words"
continue: Continue
continue: "Continue"
createdisplay:
check_the_form: "Invalid Form. Please check the fields"
continue: "Continue with the same Facebook username"
error_create: "Error when changing username"
fake_comment_body: "This is an example comment. Readers can share their thoughts and opinions with newsrooms in the comments section."
fake_comment_date: "1 minute ago"
if_you_dont_change_your_name: "If you don't change your username at this step your Facebook display name will appear alongside of all your comments."
required_field: "Required field"
save: Save
special_characters: "Usernames can contain letters numbers and _ only"
username: Username
write_your_username: "Edit your username"
your_username: "Your username appears on every comment you post."
dashboard:
auto_update: "Data automatically updates every five minutes or when you Reload."
comment_count: comments
@@ -147,6 +160,17 @@ en:
second: "second"
seconds_plural: "seconds"
unexpected_error: "Unexpected error while saving changes. Sorry!"
email:
confirm:
has_been_requested: "A email confirmation has been requested for the following account:"
to_confirm: "To confirm the account, please visit the following link:"
confirm_email: "Confirm Email"
if_you_did_not: "If you did not request this, you can safely ignore this email."
subject: "Email Confirmation"
password_reset:
we_received_a_request: "We received a request to reset your password. If you did not request this change, you can ignore this email."
if_you_did: "If you did,"
please_click: "please click here to reset password"
embedlink:
copy: "Copy to Clipboard"
error:
@@ -178,11 +202,13 @@ en:
flag_reason: "Reason for reporting (Optional)"
flag_username: "Report username"
framework:
banned_account_msg: "Your account is currently suspended. This means that you cannot Like Report or write comments. Please contact us if you have any questions."
banned_account_header: "Your account is currently banned."
banned_account_body: "This means that you cannot Like, Report, or write comments."
because_you_ignored: "Because you ignored the following commenters, their comments are hidden."
comment: comment
comment_is_ignored: "This comment is hidden because you ignored this user."
comments: comments
configure_stream: "Configure Stream"
configure_stream: "Configure"
content_not_available: "This content is not available"
edit_name:
button: Submit
@@ -195,9 +221,11 @@ en:
new_count: "View {0} new {1}"
profile: Profile
show_all_comments: "Show all comments"
stop_ignoring: "Stop ignoring"
success_bio_update: "Your biography has been updated"
success_name_update: "Your username has been updated"
success_update_settings: "The changes you have made have been applied to the comment stream on this article"
view_all_replies_unknown_number: "view all replies"
view_all_replies: "view {0} replies"
view_all_replies_initial: "view all {0} replies"
view_more_comments: "view more comments"
@@ -238,8 +266,8 @@ en:
reject: "Reject"
rejected: "Rejected"
select_stream: "Select Stream"
shift_key:
shortcuts: Shortcuts
shift_key: "⇧"
shortcuts: "Shortcuts"
show_shortcuts: "Show Shortcuts"
singleview: "Toggle single comment edit view"
spam_ads: Spam/Ads
@@ -296,7 +324,7 @@ en:
bio: bio
cancel: "Cancel"
days: "{0} days"
description_0: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}."
description_0: "Would you like to temporarily suspend this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}."
description_1: "Suspending this user will temporarily disable their account and hide all of their comments on the site."
description_notify: "Suspending this user will temporarily disable their account and hide all of their comments on the site."
description_reject: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}."
+156 -166
View File
@@ -4,8 +4,7 @@ es:
ban_user: "¿Quieres suspender el Usuario?"
banned_user: "Usuario Suspendido"
cancel: Cancelar
note: "Nota: Suspender a este usuario también va a colocar este comentario en\
\ la cola de Rechazados."
note: "Nota: Suspender a este usuario también va a colocar este comentario en la cola de Rechazados."
yes_ban_user: "Si, Suspender al usuario"
bio_offensive: "Esta biografia es ofensiva"
cancel: Cancelar
@@ -23,22 +22,16 @@ es:
comment: "Publicar un comentario"
name: "Nombre"
comment_post_notif: "Tu comentario ha sido publicado."
comment_post_notif_premod: "Gracias por el comentario. Nuestro equipo de moderació\
n va a revisarlo muy pronto."
comment_post_banned_word: "Tu comentario contiene una o más palabras que no está\
n permitidas en nuestro espacio, por lo que no será publicado. Si crees que\
\ es un error, por favor contacta a nuestro equipo de moderación."
comment_post_notif_premod: "Gracias por el comentario. Nuestro equipo de moderación va a revisarlo muy pronto."
comment_post_banned_word: "Tu comentario contiene una o más palabras que no están permitidas en nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación."
characters_remaining: "carácteres restantes"
comment_is_best: "Este comentario es uno de los mejores"
comment_offensive: "Este comentario es ofensivo"
comment_singular: Comentario
comment_plural: Comentarios
comment_post_banned_word: "Tu comentario contiene una o más palabras que no está\
n permitidas en nuestro espacio, por lo que no será publicado. Si crees que es\
\ un error, por favor contacta a nuestro equipo de moderación."
comment_post_banned_word: "Tu comentario contiene una o más palabras que no están permitidas en nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación."
comment_post_notif: "Tu comentario ha sido publicado."
comment_post_notif_premod: "Gracias por el comentario. Nuestro equipo de moderació\
n va a revisarlo muy pronto."
comment_post_notif_premod: "Gracias por el comentario. Nuestro equipo de moderación va a revisarlo muy pronto."
community:
account_creation_date: "Fecha de creación de la cuenta"
active: Activa
@@ -57,8 +50,7 @@ es:
newsroom_role: "Rol en la redacción"
no_flagged_accounts: "No hay ninguna cuenta reportada en este momento."
no_results: "No se encontraron usuarios con ese nombre o correo."
note: "Nota: Suspender a este usuario no le va a permitir (al usuario) borrar\
\ ni editar ni comentar."
note: "Nota: Suspender a este usuario no le va a permitir (al usuario) borrar ni editar ni comentar."
offensive: "Este comentario es ofensivo"
other: Otro
people: Gente
@@ -71,21 +63,15 @@ es:
yes_ban_user: "Si, Suspendan el usuario"
configure:
apply: Aplicar
banned_word_text: "Comentarios que contengan estas palabras o frases, en mayusculas\
\ o minúsculas, serán automáticamente eliminados del hilo de comentario. Escribir\
\ una palabra y apretar Enter o Tabulador para agregarla. O pueden pegar una\
\ lista de palabras separadas por coma."
banned_word_text: "Comentarios que contengan estas palabras o frases, en mayusculas o minúsculas, serán automáticamente eliminados del hilo de comentario. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pueden pegar una lista de palabras separadas por coma."
banned_words_title: "Lista de palabras prohibidas"
close: "Cerrar"
close_after: "Cerrar comentarios luego de"
close_stream: "Cerrar comentarios"
close_stream_configuration: "Este hilo de comentario está en este momento cerrado.\
\ Al abrirlo, nuevos comentarios serán publicados y mostrados."
closed_comments_desc: "Escribe un mensaje que será mostrado cuando los comentarios\
\ estén cerrados y no se acepten más comentarios."
close_stream_configuration: "Este hilo de comentario está en este momento cerrado. Al abrirlo, nuevos comentarios serán publicados y mostrados."
closed_comments_desc: "Escribe un mensaje que será mostrado cuando los comentarios estén cerrados y no se acepten más comentarios."
closed_comments_label: "Escribe un mensaje..."
closed_stream_settings: "Mensaje que se muestra cuando no se aceptan más comentarios\
\ en el articulo"
closed_stream_settings: "Mensaje que se muestra cuando no se aceptan más comentarios en el articulo"
comment_count_error: "Por favor escribir un número válido"
comment_count_header: "Limitar el largo del comentario"
comment_count_text_post: carácteres
@@ -95,155 +81,155 @@ es:
comment_stream_will_close: "El hilo de comentarios se cerrara"
community: Comunidad
configure: Configurar
copy_and_paste: "Copiar y pegar el código de más abajo en tu CMS para colocar\
\ la caja de comentarios en tus artículos"
copy_and_paste: "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus artículos"
custom_css_url: "URL CSS a medida"
custom_css_url_desc: "URL de una hoja de estilo que va a sobrescribir los estilos\
\ por defecto del hilo de comentarios. Puede ser interna o externa."
custom_css_url_desc: "URL de una hoja de estilo que va a sobrescribir los estilos por defecto del hilo de comentarios. Puede ser interna o externa."
dashboard: Panel
days: Días
description: "Como Administrador/a puedes modificar la configuración de los comentarios\
\ en este artículo"
domain_list_text: "Agrega dominios permitidos a Talk, por ejemplo tu localhost,\
\ staging y ambientes de producción (ej. localhost:3000, staging.domain.com,\
\ domain.com)."
description: "Como Administrador/a puedes modificar la configuración de los comentarios en este artículo"
domain_list_text: "Agrega dominios permitidos a Talk, por ejemplo tu localhost, staging y ambientes de producción (ej. localhost:3000, staging.domain.com, domain.com)."
domain_list_title: "Dominios Permitidos"
edit_comment_timeframe_heading: "Periodo de Tiempo para Edición del Comentario"
edit_comment_timeframe_text_pre: "Los comentaristas tendrán"
edit_comment_timeframe_text_post: "segundos para editar sus comentarios."
embed_comment_stream: "Colocar Hilo de Comentarios"
enable_premod_links_text: "Los moderadores deben aprobar todo comentario que contenga\
\ un enlace antes de que sea publicado."
enable_premod_links_text: "Los moderadores deben aprobar todo comentario que contenga un enlace antes de que sea publicado."
enable_pre_moderation: "Permitir pre-moderación"
enable_pre_moderation_text: "Los moderadores deben aprobar todo comentario antes\
\ que sea publicado."
enable_pre_moderation_text: "Los moderadores deben aprobar todo comentario antes que sea publicado."
enable_premod_links: "Pre-Moderar Comentarios que contienen Enlaces"
enable_premod: "Activar Pre Moderación"
enable_premod_description: "Los y las moderadoras deben aprobar cualquier comentario\
\ antes de su publicación"
enable_premod_links_description: "Los y las moderadoras deben aprobar cualquier\
\ comentario que contengan enlaces antes de su publicación."
enable_premod_description: "Los y las moderadoras deben aprobar cualquier comentario antes de su publicación"
enable_premod_links_description: "Los y las moderadoras deben aprobar cualquier comentario que contengan enlaces antes de su publicación."
enable_questionbox: "Hacer una pregunta a los y las lectoras."
enable_questionbox_description: "Esta pregunta aparecerá en la parte de arriba\
\ del hilo de comentarios."
enable_questionbox_description: "Esta pregunta aparecerá en la parte de arriba del hilo de comentarios."
hours: Horas
include_comment_stream: "Incluir Descripción de Hilo de Comentarios para Lectores"
include_comment_stream_desc: "Escribir un mensaje para ser incluido al principio\
\ del hilo de comentarios. Un tema planteado podría ser la guía de comunidad,\
\ etc."
include_comment_stream_desc: "Escribir un mensaje para ser incluido al principio del hilo de comentarios. Un tema planteado podría ser la guía de comunidad, etc."
include_text: "Agregar tu texto aquí."
include_question_here: "Escribir la pregunta aquí."
moderate: Moderar
moderation_settings: "Configuración de la Moderación"
open: "Abrir"
open_stream: "Abrir Hilo de Comentarios"
open_stream_configuration: "Este hilo de comentarios está abierto. Al cerrarlo\
\ no se podrán publicar nuevos comentarios pero todos los comentarios anteriores\
\ aún serán mostrados."
open_stream_configuration: "Este hilo de comentarios está abierto. Al cerrarlo no se podrán publicar nuevos comentarios pero todos los comentarios anteriores aún serán mostrados."
require_email_verification: "Necesita confirmación su correo"
require_email_verification_text: "Nuevos usuarios deben confirmar sus correos\
\ antes de comentar"
require_email_verification_text: "Nuevos usuarios deben confirmar sus correos antes de comentar"
save_changes: "Guardar Cambios"
shortcuts: Atajos
sign_out: "Desconectar"
stories: Artículos
stream_settings: "Configuración de Comentarios"
suspect_word_title: "Lista de palabras sospechosas"
suspect_word_text: "Comentarios que contengan estas palabras o frases, considerando\
\ mayusculas y minúsculas, serán automáticamente destacadas en los comentarios\
\ publicados. Escribir una palabra y apretar Enter o Tabulador para agregarla.\
\ O pegar una lista de palabras separadas por coma."
suspect_word_text: "Comentarios que contengan estas palabras o frases, considerando mayusculas y minúsculas, serán automáticamente destacadas en los comentarios publicados. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pegar una lista de palabras separadas por coma."
tech_settings: "Configuración Técnica"
title: "Configurar los comentarios"
weeks: Semanas
wordlist: "Palabras Suspendidas"
continue: Continuar
continue: "Continuar"
createdisplay:
check_the_form: "Formulario Inválido. Por favor verifica los campos"
continue: "Continuar con el mismo nombre que aparece en Facebook"
error_create: "Hubo un error al cambiar el nombre de usuario"
fake_comment_body: "Este es un comentario de ejemplo. Las lectoras pueden compartir sus ideas y opiniones con los periodistas en la sección de comentarios."
fake_comment_date: "hace un minuto"
if_you_dont_change_your_name: "Si no modificas tu nombre de usuario en este paso tu nombre de Facebook aparecerá al lado de cada comentario que publiques."
required_field: "Campo requerido"
save: "Guardar"
special_characters: "Los nombres sólo pueden contener letras números y _"
username: "Nombre"
write_your_username: "Edita tu nombre"
your_username: "Tu nombre aparece en cada comentario que publiques."
dashboard:
auto_update: "Los datos se actualizan automáticamente cada cinco minutos o cuando\
\ refresca el navegador."
comment_count: comentarios
flags: Reportes
auto_update: "Los datos se actualizan automáticamente cada cinco minutos o cuando refresca el navegador."
comment_count: "comentarios"
flags: "Reportes"
most_flags: "Artículos con la mayor cantidad de reportes"
most_conversations: "Artículos con las mayores conversaciones"
next_update: "{0} minutos hasta la próxima actualización."
no_activity: "¡No han habido comentarios en ningún lado en los últimos 5 minutos."
no_flags: "¡No ha habido ningún reporte en los últimos 5 minutos! Bravo!"
no_likes: "¡No ha habido ningún 'me gusta' en los últimos 5 minutos. Todo tranquilo."
done: Hecho
done: "Hecho"
edit_comment:
body_input_label: "Editar este comentario"
save_button: "Guardar cambios"
edit_window_expired: "No se puede editar este comentario. El periodo de edición\
\ ya ha concluido. Podrías publicar uno nuevo :-)"
edit_window_expired: "No se puede editar este comentario. El periodo de edición ya ha concluido. Podrías publicar uno nuevo :-)"
edit_window_expired_close: "Cerrar"
edit_window_timer_prefix: "Ventana Edición:"
second: "segundo"
seconds_plural: "segundos"
unexpected_error: "Lo siento. Ha habido un error no previsto al guardar los cambios."
email:
confirm:
has_been_requested: "Un correo de confirmación ha sido pedido para la siguiente cuenta:"
to_confirm: "Para confirmar la cuenta, por favor visitar el siguiente enlace:"
confirm_email: "Confirmar Correo"
if_you_did_not: "Si no has pedido esto, puedes ignorar el correo."
subject: "Confirmación de Correo"
password_reset:
we_received_a_request: "Recibimos un pedido para resetear su contraseña. Si no ha pedido ese cambio, por favor ignorar el correo. "
if_you_did: "Si lo hizo,"
please_click: "por favor cliquea aqui para resetear la contraseña."
embedlink:
copy: "Copiar al portapapeles"
error:
COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito"
NOT_AUTHORIZED: "Acción no autorizada."
NO_SPECIAL_CHARACTERS: "Los nombres pueden contener letras números y _"
PASSWORD_LENGTH: "Contraseña es muy corta"
PROFANITY_ERROR: "Los nombres no pueden contener blasfemias. Por favor contacte\
\ al o la administradora si cree que esto es un error"
USERNAME_IN_USE: "Este nombre ya está siendo usado."
USERNAME_REQUIRED: "Debe ingresar un nombre"
EDIT_WINDOW_ENDED: "No puedes editar este comentario. El tiempo de edición ha\
\ expirado."
EDIT_USERNAME_NOT_AUTHORIZED: "No tiene permiso para editar el nombre de usuario."
EMAIL_IN_USE: "Este correo se encuentra en uso"
EMAIL_REQUIRED: "Se requiere un correo"
LOGIN_MAXIMUM_EXCEEDED: "Ha realizado demasiados intentos fallidos de usar la\
\ contraseña. Por favor espere."
PASSWORD_REQUIRED: "Debe ingresar la contraseña"
COMMENTING_CLOSED: "Los comentarios ya estan cerrados"
NOT_FOUND: "Recurso no encontrado"
INVALID_ASSET_URL: "La URL del articulo no es valida"
email: "No es un correo válido"
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
network_error: "Error al conectar con el servidor. Compruebe su conexión a Internet\
\ y vuelva a intentarlo."
edit_window_ended: "No puedes editar este comentario. El tiempo de edición ha expirado."
edit_username_not_authorized: "No tiene permiso para editar el nombre de usuario."
email: "No es un correo válido"
email_in_use: "Este correo se encuentra en uso"
email_not_verified: "Correo {0} no confirmado."
email_password: "Correo y/o contraseña incorrecta."
email_required: "Se requiere un correo"
email_username_in_use: "Correo o nombre en uso."
INVALID_ASSET_URL: "La URL del articulo no es valida"
login_maximum_exceeded: "Ha realizado demasiados intentos fallidos de usar la contraseña. Por favor espere."
network_error: "Error al conectar con el servidor. Compruebe su conexión a Internet y vuelva a intentarlo."
NOT_AUTHORIZED: "Acción no autorizada."
NOT_FOUND: "Recurso no encontrado"
NO_SPECIAL_CHARACTERS: "Los nombres pueden contener letras números y _"
organization_name: "El nombre de la organización debe contener letras y/o números."
password: "La contraseña debe tener por lo menos 8 caracteres"
PASSWORD_LENGTH: "Contraseña es muy corta"
PASSWORD_REQUIRED: "Debe ingresar la contraseña"
PROFANITY_ERROR: "Los nombres no pueden contener blasfemias. Por favor contacte al o la administradora si cree que esto es un error"
username: "Los nombres pueden contener letras números y _"
USERNAME_IN_USE: "Este nombre ya está siendo usado."
USERNAME_REQUIRED: "Debe ingresar un nombre"
flag_comment: "Reportar este comentario"
flag_reason: "Razón por la que hacer este reporte (Opcional)"
flag_username: "Reportar el nombre de usuario"
framework:
banned_account_msg: "Tu cuenta se encuentra suspendida. Esto significa que no\
\ puedes gustar, marcar o escribir comentarios."
comment: comentario
banned_account_header: "Tu cuenta se encuentra suspendida."
banned_account_body: "Esto significa que no puedes gustar, marcar o escribir comentarios."
comment: "comentario"
comment_is_ignored: "Este comentario está escondido porque has ignorado al usuario."
comments: comentarios
comments: "comentarios"
configure_stream: "Configurar Hilo de Comentarios"
content_not_available: "Este contenido no se encuentra disponible"
edit_name:
button: Enviar
button: "Enviar"
error: "Nombres de usuarios pueden solamente incluir letras, números y _"
label: "Nuevo Nombre"
msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado\
\ no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar\
\ un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta."
msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta."
ignored_users: "Usuarios ignorados"
my_comments: "Mis Comentarios"
my_profile: "Mi perfil"
new_count: "Ver {0} {1} nuevo"
profile: Perfil
show_all_comments: "Mostrar todos los comentarios"
stop_ignoring: "No ignorar más"
success_bio_update: "Tu biografia fue actualizada"
success_name_update: "Tu nombre de usuario ha sido actualizado"
success_update_settings: "La configuración de este articulo fue actualizada"
view_all_replies_unknown_number: "ver todas las respuestas"
view_all_replies: "ver {0} respuestas"
view_all_replies_initial: "ver todas las {0} respuestas"
view_more_comments: "Ver más comentarios"
view_reply: "ver respuesta"
from_settings_page: "Desde la página de configuración puedes ver tu historia de\
\ comentarios."
from_settings_page: "Desde la página de configuración puedes ver tu historia de comentarios."
like: Me gusta
loading_results: "Cargando Resultados"
marketing: "Esto parece una propaganda"
@@ -300,78 +286,88 @@ es:
profile_settings: "Configuración del Perfil"
reply: Responder
report: Reportar
report_notif: "Gracias por reportar este comentario. Nuestro equipo de moderació\
n ha sido notificado y muy pronto lo va a revisar."
report_notif: "Gracias por reportar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar."
report_notif_remove: "Tu reporte ha sido eliminado."
reported: Reportado
reported: "Reportado"
set_best: "Etiquetar como el mejor"
settings:
all_comments: "Todos los comentarios"
from_settings_page: "Desde la página de configuración puedes ver tu historial\
\ de comentarios."
from_settings_page: "Desde la página de configuración puedes ver tu historial de comentarios."
my_comment_history: "Mi historial de comentarios"
profile: Perfil
profile_settings: "Configuración del perfil"
sign_in: "Entrar"
to_access: "para acceder al perfil"
user_no_comment: "No has dejado aún ningún comentario. ¡Únete a la conversació\
n!"
user_no_comment: "No has dejado aún ningún comentario. ¡Únete a la conversación!"
sign_in:
email_verify_cta: "Por favor confirme su correo."
request_new_verify_email: "Enviar otro correo:"
verify_email: "¡Gracias por crear una cuenta! Le enviamos un correo a la dirección que dio para confirmar su cuenta."
verify_email2: "Debe confirmarla antes de poder involucrarse en la comunidad."
not_you: "¿No eres tu?"
logged_in_as: "Entraste como"
facebook_sign_in: "Entrar con Facebook"
facebook_sign_up: "Registrarse con Facebook"
logout: "Salir"
sign_in: "Entrar"
sign_in_to_join: "Entrar para unirte a la conversación"
or: "O"
email: "Dirección de Correo"
password: "Contraseña"
forgot_your_pass: "¿Has olvidado tu contraseña?"
need_an_account: "¿Necesitas una cuenta?"
register: "Registrar"
sign_up: "Registro"
confirm_password: "Confirmar Contraseña"
username: "Nombre"
already_have_an_account: "¿Ya tienes una cuenta?"
recover_password: "Recuperar la contraseña"
email_in_use: "Este correo se encuentra en uso"
email_or_username_in_use: "Este correo ó nombre de usuario se encuentran en uso"
required_field: "Este campo es requerido"
passwords_dont_match: "Las contraseñas no coinciden."
special_characters: "Los nombres pueden contener letras, números y _"
sign_in_to_comment: "Entrar para comentar"
temporarily_suspended: "De acuerdo con la guía de la comunidad de {0}, su cuenta ha sido temporalmente suspendida. Por favor unirse a la conversación {1}."
check_the_form: "Formulario Inválido. Por favor, completa los campos"
step_1_header: "Reportar un problema"
step_2_header: "Ayúdanos a comprender"
step_3_header: "Gracias por tu participación"
stream:
temporarily_suspended: "De acuerdo con la guía de la comunidad de {0}, su cuenta\
\ ha sido temporalmente suspendida. Por favor unirse a la conversación {1}."
temporarily_suspended: "De acuerdo con la guía de la comunidad de {0}, su cuenta ha sido temporalmente suspendida. Por favor unirse a la conversación {1}."
streams:
all: Todos
article: Artículo
closed: Cerrado
empty_result: "Ningún articulo coincide con esta búsqueda. Intente expandir su\
\ búsqueda."
all: "Todos"
article: "Artículo"
closed: "Cerrado"
empty_result: "Ningún articulo coincide con esta búsqueda. Intente expandir su búsqueda."
filter_streams: "Filtrar Hilos"
newest: Más nuevo
oldest: Más antiguo
open: Abierto
newest: "Más nuevo"
oldest: "Más antiguo"
open: "Abierto"
pubdate: "Fecha Publicación"
search: Buscar
search: "Buscar"
sort_by: "Ordenar por"
status: "Estado del Hilo"
stream_status: "Estado del Hilo"
suspenduser:
bio: bio
bio: "bio"
cancel: "Cancelar"
days: "{0} días"
description_0: "¿Quiere suspender temporalmente a este usuario por su {0}? Hacerlo\
\ va a ocultar temporalmente todos sus comentarios hasta que edite su {0}."
description_1: "Suspender a este usuario va a deshabilitar temporalmente su cuenta\
\ y ocultar todos sus comentarios en el sitio."
description_notify: "Suspender a este usuario va a deshabilitar temporalmente\
\ su cuenta y ocultar todos sus comentarios en el sitio."
description_reject: "¿Quiere suspender temporalmente a este usuario por su {0}?\
\ Hacerlo va a ocultar temporalmente sus comentarios hasta que edite su {0}."
description_suspend: "Esta suspendiendo a {0}. Este comentario va a ir a la fila\
\ de Rechazados, y a {0} no se le va a permitir gustar, marcar, responder o\
\ publicar hasta que el tiempo de suspendido halla concluido."
email: "Otro miembro de la comunidad recientemente marco su nombre de usuario\
\ para revisarlo. Por su contenido su nombre de usuario ha sido rechazado. Esto\
\ significa que no puede comentar o marcar contenido hasta que edite su nombre\
\ de usuario. Envíenos un correo si tiene preguntas o comentarios."
description_0: "¿Quiere suspender temporalmente a este usuario por su {0}? Hacerlo va a ocultar temporalmente todos sus comentarios hasta que edite su {0}."
description_1: "Suspender a este usuario va a deshabilitar temporalmente su cuenta y ocultar todos sus comentarios en el sitio."
description_notify: "Suspender a este usuario va a deshabilitar temporalmente su cuenta y ocultar todos sus comentarios en el sitio."
description_reject: "¿Quiere suspender temporalmente a este usuario por su {0}? Hacerlo va a ocultar temporalmente sus comentarios hasta que edite su {0}."
description_suspend: "Esta suspendiendo a {0}. Este comentario va a ir a la fila de Rechazados, y a {0} no se le va a permitir gustar, marcar, responder o publicar hasta que el tiempo de suspendido halla concluido."
email: "Otro miembro de la comunidad recientemente marco su nombre de usuario para revisarlo. Por su contenido su nombre de usuario ha sido rechazado. Esto significa que no puede comentar o marcar contenido hasta que edite su nombre de usuario. Envíenos un correo si tiene preguntas o comentarios."
email_subject: "Su cuenta ha sido suspendida"
email_message_reject: "Otro miembro de la comunidad hace poco ha marcado su nombre\
\ de usuario para revisar. Y este ha sido rechazado por su contenido. Esto significa\
\ que no puede comentar, gustar o marcar contenido hasta que edite su nombre\
\ de usuario. Envíenos un correo si tiene alguna preocupación o pregunta."
email_message_suspend: "Querida {0},\n\nDe acuerdo a la guía comunitaria de {1},\
\ su cuenta ha sido temporalmente suspendida. Durante este tiempo, no podrá\
\ comentar, marcar o involucrarse con otros comentaristas. Por favor unirse\
\ a la conversación {2}."
email_message_reject: "Otro miembro de la comunidad hace poco ha marcado su nombre de usuario para revisar. Y este ha sido rechazado por su contenido. Esto significa que no puede comentar, gustar o marcar contenido hasta que edite su nombre de usuario. Envíenos un correo si tiene alguna preocupación o pregunta."
email_message_suspend: "Querida {0}, De acuerdo a la guía comunitaria de {1}, su cuenta ha sido temporalmente suspendida. Durante este tiempo, no podrá comentar, marcar o involucrarse con otros comentaristas. Por favor unirse a la conversación {2}."
error_email_message_empty: "Debe especificar un mensaje de correo."
hours: "{0} horas"
no_cancel: "No cancelar"
notify_suspend_until: "Usuario {0} ha sido temporalmente suspendido. Esta suspension\
\ va a terminar automáticamente en {1}."
notify_suspend_until: "Usuario {0} ha sido temporalmente suspendido. Esta suspension va a terminar automáticamente en {1}."
one_hour: "1 hora"
send: Enviar
send: "Enviar"
select_duration: "Seleccionar la duración de la suspensión"
suspend_user: "Suspender Usuario"
title: "Suspender un usuario"
@@ -383,8 +379,7 @@ es:
username: nombre de usuario
write_message: "Escribir un mensaje"
yes_suspend: "Si, suspender"
thank_you: "Valoramos tanto su seguridad en este espacio como sus comentarios. Un\
\ o una moderadora va a leer su reporte."
thank_you: "Valoramos tanto su seguridad en este espacio como sus comentarios. Un o una moderadora va a leer su reporte."
unset_best: "Des-etiquetar como el mejor"
user:
bio_flags: "reportes para este bio"
@@ -395,29 +390,24 @@ es:
username_offensive: "Este nombre de usuario es ofensivo"
view_conversation: "Ver Conversación"
install:
initial:
description: "Vamos a crear su comunidad Talk en unos pocos pasos."
submit: "Empecemos"
add_organization:
description: "Por favor díganos el nombre de su organización. Esta aparecerá\
\ en correos cuando se inviten a nuevos miembros del equipo."
label: "Nombre de la Organización"
save: "Guardar"
create:
email: "Dirección de correo"
username: "Nombre de Usuario"
password: "Contraseña"
confirm_password: "Confirmar Contraseña"
save: "Guardar"
permitted_domains:
title: "Dominios permitidos"
description: "Ingresar los dominios en donde estará Talk, por ejemplo sus ambientes\
\ locales, de staging y producción (ej. localhost:3000, staging.domain.com,\
\ domain.com)."
submit: "Terminar la Instalación"
final:
description: "¡Gracias por instalar Talk! Enviamos un correo para verificar\
\ su dirección de correo. Mientras esta terminando de configurar su cuenta,\
\ ya puede comenzar a involucrarse con sus lectores."
launch: "Iniciar Talk"
close: "Cerrar este instalador"
INITIAL:
DESCRIPTION: "Vamos a crear su comunidad Talk en unos pocos pasos."
SUBMIT: "Empecemos"
ADD_ORGANIZATION:
DESCRIPTION: "Por favor díganos el nombre de su organización. Esta aparecerá en correos cuando se inviten a nuevos miembros del equipo."
LABEL: "Nombre de la Organización"
SAVE: "Guardar"
CREATE:
EMAIL: "Dirección de correo"
USERNAME: "Nombre de Usuario"
PASSWORD: "Contraseña"
CONFIRM_PASSWORD: "Confirmar Contraseña"
SAVE: "Guardar"
PERMITTED_DOMAINS:
TITLE: "Dominios permitidos"
DESCRIPTION: "Ingresar los dominios en donde estará Talk, por ejemplo sus ambientes locales, de staging y producción (ej. localhost:3000, staging.domain.com, domain.com)."
SUBMIT: "Terminar la Instalación"
FINAL:
DESCRIPTION: "¡Gracias por instalar Talk! Enviamos un correo para verificar su dirección de correo. Mientras esta terminando de configurar su cuenta, ya puede comenzar a involucrarse con sus lectores."
LAUNCH: "Iniciar Talk"
CLOSE: "Cerrar este instalador"
+4 -1
View File
@@ -104,7 +104,10 @@ const CommentSchema = new Schema({
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
}
},
toJSON: {
virtuals: true,
},
});
CommentSchema.virtual('edited').get(function() {
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "1.8.0",
"version": "1.9.1",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"scripts": {
@@ -92,11 +92,11 @@
"minimist": "^1.2.0",
"mongoose": "^4.9.8",
"morgan": "^1.8.1",
"ms": "^2.0.0",
"natural": "^0.5.0",
"node-emoji": "^1.5.1",
"node-fetch": "^1.6.3",
"nodemailer": "^2.6.4",
"parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1",
"passport-local": "^1.0.0",
@@ -104,6 +104,7 @@
"react-apollo": "^1.1.0",
"react-recaptcha": "^2.2.6",
"react-toastify": "^1.5.0",
"react-transition-group": "^1.1.3",
"recompose": "^0.23.1",
"redis": "^2.7.1",
"resolve": "^1.3.2",
@@ -112,7 +113,8 @@
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"timekeeper": "^1.0.0",
"uuid": "^3.0.1",
"yaml-loader": "^0.4.0"
"yaml-loader": "^0.4.0",
"yamljs": "^0.2.10"
},
"devDependencies": {
"apollo-client": "^1.0.4",
+14
View File
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
+2
View File
@@ -0,0 +1,2 @@
export {addTag, removeTag} from 'coral-plugin-commentbox/actions';
export {addCommentClassName, removeCommentClassName} from 'coral-embed-stream/src/actions/stream';
@@ -0,0 +1,2 @@
export const commentBoxTagsSelector = (state) => state.commentBox.tags;
export const commentClassNamesSelector = (state) => state.stream.commentClassNames;
@@ -0,0 +1,2 @@
export {Slot} from 'coral-framework/components';
export {Icon} from 'coral-ui';
+1
View File
@@ -0,0 +1 @@
export {withReaction} from 'coral-framework/hocs';
+1
View File
@@ -0,0 +1 @@
export {t} from 'coral-framework/services/i18n';
View File
View File
+5 -2
View File
@@ -3,11 +3,14 @@
"coral-plugin-respect",
"coral-plugin-like",
"coral-plugin-facebook-auth",
"coral-plugin-auth"
"coral-plugin-auth",
"coral-plugin-offtopic"
],
"client": [
"coral-plugin-respect",
"coral-plugin-like",
"coral-plugin-auth"
"coral-plugin-auth",
"coral-plugin-offtopic",
"coral-plugin-viewing-options"
]
}
@@ -3,12 +3,13 @@ import {Button} from 'coral-ui';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {showSignInDialog} from 'coral-framework/actions/auth';
import t from 'coral-framework/services/i18n';
const SignInButton = ({loggedIn, showSignInDialog}) => (
<div>
{!loggedIn
? <Button id="coralSignInButton" onClick={showSignInDialog} full>
Sign in to comment
{t('sign_in.sign_in_to_comment')}
</Button>
: null}
</div>
@@ -18,7 +18,6 @@ import {
facebookCallback,
invalidForm,
validForm,
checkLogin
} from 'coral-framework/actions/auth';
class SignInContainer extends React.Component {
@@ -38,10 +37,6 @@ class SignInContainer extends React.Component {
};
}
componentWillMount() {
this.props.checkLogin();
}
componentDidMount() {
window.addEventListener('storage', this.handleAuth);
@@ -187,7 +182,6 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
checkLogin,
facebookCallback,
fetchSignUp,
fetchSignIn,
@@ -9,10 +9,10 @@ const UserBox = ({loggedIn, user, logout, onShowProfile}) => (
<div>
{
loggedIn ? (
<div className={styles.userBox}>
{t('sign_in.logged_in_as')}
<div className={`${styles.userBox} talk-stream-auth-userbox`}>
<span className={styles.userBoxLoggedIn}>{t('sign_in.logged_in_as')}</span>
<a onClick={onShowProfile}>{user.username}</a>. {t('sign_in.not_you')}
<a className={styles.logout} onClick={() => logout()}>
<a className={`${styles.logout} talk-stream-userbox-logout`} onClick={() => logout()}>
{t('sign_in.logout')}
</a>
</div>
@@ -70,6 +70,10 @@ input.error{
letter-spacing: 0.1px;
}
.userBoxLoggedIn {
font-weight: bold;
}
.userBox a {
color: black;
font-weight: bold;
@@ -5,10 +5,10 @@ en:
verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account."
verify_email2: "You must verify your account before engaging with the community."
not_you: "Not you?"
logged_in_as: "Logged in as"
logged_in_as: "Signed in as"
facebook_sign_in: "Sign in with Facebook"
facebook_sign_up: "Sign up with Facebook"
logout: "Logout"
logout: "Sign out"
sign_in: "Sign in"
sign_in_to_join: "Sign in to join the conversation"
or: "Or"
@@ -2,7 +2,7 @@ import React from 'react';
import {Icon} from 'coral-ui';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
import {withReaction} from 'coral-plugin-api';
import {withReaction} from 'plugin-api/beta/client/hocs';
class LoveButton extends React.Component {
handleClick = () => {
@@ -0,0 +1,5 @@
import {OFFTOPIC_TOGGLE_CHECKBOX} from './constants';
export const toggleCheckbox = () => ({
type: OFFTOPIC_TOGGLE_CHECKBOX
});
@@ -1,39 +1,45 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addTag, removeTag} from 'coral-plugin-commentbox/actions';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
import {t} from 'plugin-api/beta/client/services';
class OffTopicCheckbox extends React.Component {
export default class OffTopicCheckbox extends React.Component {
label = 'OFF_TOPIC';
handleChange = (e) => {
if (e.target.checked) {
this.props.addTag(this.label);
} else {
const idx = this.props.commentBox.tags.indexOf(this.label);
componentDidMount() {
this.clearTagsHook = this.props.registerHook('postSubmit', () => {
const idx = this.props.tags.indexOf(this.label);
this.props.removeTag(idx);
});
}
componentWillUnmount() {
this.props.unregisterHook(this.clearTagsHook);
}
handleChange = (e) => {
const {addTag, removeTag} = this.props;
if (e.target.checked) {
addTag(this.label);
} else {
const idx = this.props.tags.indexOf(this.label);
removeTag(idx);
}
}
render() {
return (
<div className={styles.offTopic}>
<label className={styles.offTopicLabel}>
<input type="checkbox" onChange={this.handleChange}/>
{t('off_topic')}
</label>
{
!this.props.isReply ? (
<label className={styles.offTopicLabel}>
<input type="checkbox" onChange={this.handleChange}/>
{t('off_topic')}
</label>
) : null
}
</div>
);
}
}
const mapStateToProps = ({commentBox}) => ({commentBox});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTag, removeTag}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
@@ -0,0 +1,32 @@
import React from 'react';
import styles from './styles.css';
export default class OffTopicFilter extends React.Component {
tag = 'OFF_TOPIC';
className = 'coral-plugin-off-topic-comment';
cn = {[this.className] : {tags: [this.tag]}};
handleChange = (e) => {
if (e.target.checked) {
this.props.addCommentClassName(this.cn);
this.props.toggleCheckbox();
} else {
const idx = this.props.commentClassNames.findIndex((i) => i[this.className]);
this.props.removeCommentClassName(idx);
this.props.toggleCheckbox();
}
this.props.closeViewingOptions();
}
render() {
return (
<div className={styles.viewingOption}>
<label>
<input type="checkbox" onChange={this.handleChange} checked={this.props.checked} />
Hide Off-Topic Comments
</label>
</div>
);
}
}
@@ -1,7 +1,7 @@
import React from 'react';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
import {t} from 'plugin-api/beta/client/services';
const isOffTopic = (tags) => {
return !!tags.filter((tag) => tag.name === 'OFF_TOPIC').length;
@@ -10,7 +10,7 @@ const isOffTopic = (tags) => {
export default (props) => (
<span>
{
isOffTopic(props.comment.tags) ? (
isOffTopic(props.comment.tags) && props.depth === 0 ? (
<span className={styles.tag}>
{t('off_topic')}
</span>
@@ -8,11 +8,20 @@
}
.tag {
background: rgba(245, 188, 168, 0.6);
background: #3D73D5;
font-size: 12px;
font-weight: bold;
color: white;
display: inline-block;
margin: 0px 5px;
padding: 5px 5px;
border-radius: 2px;
}
.viewingOption {
padding: 5px 0;
}
:global(.coral-plugin-off-topic-comment) {
display: none;
}
@@ -0,0 +1 @@
export const OFFTOPIC_TOGGLE_CHECKBOX = 'OFFTOPIC_TOGGLE_CHECKBOX';
@@ -0,0 +1,14 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addTag, removeTag} from 'plugin-api/alpha/client/actions';
import {commentBoxTagsSelector} from 'plugin-api/alpha/client/selectors';
import OffTopicCheckbox from '../components/OffTopicCheckbox';
const mapStateToProps = (state) => ({
tags: commentBoxTagsSelector(state)
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTag, removeTag}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
@@ -0,0 +1,30 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {toggleCheckbox} from '../actions';
import {commentClassNamesSelector} from 'plugin-api/alpha/client/selectors';
import OffTopicFilter from '../components/OffTopicFilter';
import {
closeViewingOptions
} from 'plugins/coral-plugin-viewing-options/client/actions';
import {
addCommentClassName,
removeCommentClassName
} from 'plugin-api/alpha/client/actions';
const mapStateToProps = (state) => ({
commentClassNames: commentClassNamesSelector(state),
checked: state.coralPluginOfftopic.checked
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
toggleCheckbox,
closeViewingOptions,
addCommentClassName,
removeCommentClassName
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicFilter);

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