Compare commits

...
130 Commits
Author SHA1 Message Date
Wyatt Johnson e91da1c9a3 fix: check if auth params exist in hash before clearing it (#2666) 2019-10-22 17:25:47 -04:00
Wyatt Johnson e816f8e019 fix: adjusted copy, added translation support (#2664) 2019-10-22 13:33:15 -04:00
Wyatt Johnson 121469ade5 feat: readme simplication (#2649) 2019-10-22 17:09:49 +00:00
Cristian Dean 1d6e9e38b0 pt-BR translation some missing phrases (#2661) 2019-10-22 10:04:49 -04:00
Wyatt Johnson 626d46d813 [CORL-726] Repeat Comment Fix (#2655)
* fix: strip tags when comparing for repeat

- share the stripped tag version of the comment
- share the logger to improve pipeline clarity

* fix: renamed variables based on review
2019-10-21 18:34:41 +00:00
Wyatt Johnson 24915be712 fix: repaired translation key name (#2657) 2019-10-21 17:15:52 +00:00
Tessa Thornton 044cbd39f1 Revert "ensure tab text doesn't get cut off if tab titles are long (#2619)" (#2654)
This reverts commit 8409dbb4ea.
2019-10-18 20:58:14 +00:00
Wyatt Johnson cd6a1db85b chore: version bump (#2653) 2019-10-18 19:08:57 +00:00
Wyatt Johnson 8bee5ae1f3 [CORL-721] SSO Logout Fix (#2652)
* fix: hide logout button when jti is undefined

* fix: improved local state init
2019-10-18 18:48:46 +00:00
Cristian Dean 153c910de7 Fix pt-BR moderate-comment-deleted-body translation (#2647) 2019-10-18 12:44:41 -04:00
Wyatt Johnson 990a70d6f9 [CORL-680] Persist Fragment Access Tokens in sessionStorage (#2643)
* feat: persist access tokens passed via fragment to sessionStorage

* fix: review comments
2019-10-18 16:08:43 +00:00
Allan Assis 9aa39bf842 Fix pt-BR translations (#2645) 2019-10-17 15:22:05 -04:00
Wyatt Johnson d169d3dd51 fix: allow unknown fields (#2640) 2019-10-16 18:52:33 -04:00
Wyatt Johnson b38a4f117b [CORL-667] Constraint Directive (#2641)
* feat: added constraints

* fix: fixes bug with index creation

- fixes #2612

* fix: added default resolver

* fix: added defaultTo to handle defaulting
2019-10-16 18:44:44 -04:00
Tessa Thornton ea71230b90 [CORL_653] throw error if inviting existing users (#2637)
* throw error if inviting existing users

* check for existing users before sending out any invites

* fix lints
2019-10-16 16:28:49 -04:00
Vinh 2dac5f29c1 Typescript-eslint bug with new or renamed files in IDE (#2642)
* fix: workaround for eslint typescript bug when creating or renaming files.

* fix: downgrade

* fix: do not lint during build

* fix: no need to permanently show eslint status in vscode.
2019-10-17 02:48:48 +07:00
Tessa Thornton 9df473178b [CORL-8] prevent repeat comments (#2632)
* prevent users from reposting the same comment

* fix tests

* fix lints

* update comments for users service

* make updates based on review
2019-10-16 15:51:34 +00:00
Tessa Thornton 1f7fdb0d00 change keyboard shortcuts for approve/reject (#2635) 2019-10-15 23:04:31 +00:00
Vinh 3bfcc509d2 [CORL-678] Transition to eslint (#2634)
* chore: setup eslint

* chore: tslint checks with types & check for import order

* chore: complete eslint transition

* fix: tests

* fix: linting after rebase, faster lint for lint-staged

* chore: remove line

* fix: lint rules

* feat: add a11y linter and fix errors

* fix: tests
2019-10-15 22:56:38 +00:00
Cristian Dean b0e0ba6633 Remove canChangeStatus condition (#2631) 2019-10-15 22:28:16 +00:00
Cristian Dean 0fbfdac846 Add CHANGE_STORY_STATUS permission to Moderators (#2630) 2019-10-15 22:16:08 +00:00
Wyatt Johnson 8d08382aea feat: skip username validation for SSO users (#2627) 2019-10-15 21:56:47 +00:00
Tessa Thornton 8409dbb4ea ensure tab text doesn't get cut off if tab titles are long (#2619) 2019-10-15 20:49:37 +00:00
Sami Safatli bae88f476e Fix Typo (#2629) 2019-10-09 11:07:28 -04:00
Wyatt Johnson 918ba10867 [CORL-681] Configurable Staff Badges (#2620)
* feat: added configurable staff badge

* fix: removed ambigous styles
2019-10-04 20:22:31 +00:00
Wyatt Johnson e7ab0b27f9 fix: fixed looping effect (#2626) 2019-10-04 19:53:14 +00:00
Wyatt Johnson 191687335b [CORL-682] OIDC Email and setEmail mutation (#2623)
* fix: make email optional for oidc, fix schema perms

- fixes #2622

* fix: adjust defaults for heroku
2019-10-04 17:00:25 +00:00
Tessa Thornton 33487be0ce [CORL-469] keyboard shortcuts (#2588)
* make comments selectable in queue

* manage focus

* switching focus works

* keyboard navigation sort of works

* scroll into view

* prev, next, approve, reject

* shortcuts for queue switching

* ban modal?

* focus search bar with ctrl f

* keyboards shortcuts modal

* zen mode

* single comment view

* ban users from moderate cards

* clean up ts and lints

* update snaps

* add strings

* update smaps

* remove single comment view shortcut

* add single comment view to moderation queue

* clean up types

* fix deps for useEffect/useCallback hooks

* feat: added toggable help
2019-10-03 23:22:24 +00:00
Sami Safatli e56793f2fb Updated email templates (#2621) 2019-10-03 20:28:28 +00:00
Wyatt Johnson 6fe4646755 [CORL-668] User Comment Rate Limiting (#2616)
* feat: added user comment rate limiting

* fix: change 30 second to 3 second

* fix: moved file

* fix: services cleanup

* fix: changes based on review
2019-10-03 19:33:52 +00:00
Tessa Thornton dd45f46b19 update email templates (#2615) 2019-10-03 17:03:03 +00:00
Tessa Thornton 4a1492e88d [CORL-90] moderator notes (#2601)
* add moderator notes and mutations

* add create moderator note mutation

* add components for moderator notes

* style moderator notes

* update styles

* prevent warning about optimistic update unused fields

* fix incorrect comment

* fix optimistic response shape

* fix create note payload type

* add migration for moderator notes

* updates from feedback

* add border width for firefox
2019-10-02 18:41:53 +00:00
Wyatt Johnson 3a4eae87ad [CORL-661] Encode Story ID (#2614)
* fix: encode story ID's for URL

* fix: use shared method
2019-10-02 16:21:49 +00:00
Tessa Thornton fa3d442b36 [CORL-624] add note about word list case-sensitivity (#2607)
* add note about word list case-sensitivity:

* fix: removed unused keys

* fix: removed unused components

* fix: linting and key fix
2019-10-01 22:24:56 +00:00
Tessa Thornton 6edb411175 [CORL-650] remove duplicate text in invite form (#2608)
* remove duplicate text in invite form

* fix: reused message key

- message change consituted a grammar change
  that wasn't reflected in other translated languages
2019-10-01 20:54:50 +00:00
Tessa Thornton 6dbc724f31 [CORL-619] update search results copy (#2606)
* update search results copy

* fix: removed unused messages
2019-10-01 20:43:50 +00:00
Cristian Dean d0df6bc849 Add missing translations pt-BR (#2604) 2019-10-01 19:37:25 +00:00
Vinh 808b355a27 [CORL-181] Comment Count Injection (#2581)
* feat: inject comment counts

* fix: tests

* feat: integrate stream embed with coral counts

* chore: refactor constants

* test: test for count bundle

* test: test live comment count integration with stream embed

* feat: use defer

* fix: snapshot

* feat: auto add count.js in when calling Coral.createStreamEmbed

* fix: tests

* fix: rm duplicate test

* chore: remove unuse file
2019-10-01 19:22:15 +00:00
Wyatt Johnson 53fa5f43e5 [CORL-325] Comment Search API (#2578)
* feat: added comment search support

* fix: updated readme

* Update README.md
2019-10-01 18:57:50 +00:00
Wyatt Johnson c045f52daa [CORL-649] Migrations (#2597)
* feat: added migration framework

* chore: added premod user status migration

* feat: enhanced error handling of migrations

* fix: added missing argument from abstract method

* fix: another templating blunder

* fix: removed debug code

* feat: enhanced migration tracking

* fix: remove skipping migrations

* feat: moved indexing to migration system

* fix: linting
2019-10-01 16:00:27 +00:00
Cristian Dean b3b26bd9f3 Remove X-Powered-By header (#2605) 2019-10-01 15:35:32 +00:00
Wyatt Johnson 0af2fec31b fix: include tenantID in query operation to improve index use (#2596) 2019-09-30 16:25:55 +00:00
Wyatt Johnson 139c1eb01f fix: fixed readme to reflect correct docker image tag (#2593) 2019-09-30 16:15:31 +00:00
Tessa Thornton 941a385d53 [CORL-51] show comment revisions (#2590)
* add comment revisions

* reverse history order

* add strings

* update fixtures

* fix fixtures
2019-09-30 09:18:34 -04:00
Tessa Thornton e61e62a238 [CORL-174] premod user (#2572)
* add types for setting user premod

* add types, fields, and mutations for user premod

* enforce premoderation on users marked premod

* add user premod to dropdown

* update specs

* fix tests

* update premod workflow to match v4

* allow user filtering by premod status

* add premod user history to account actions drawer

* update snaps

* fix permissions on new premod fields

* update strings

* fix tests

* clean up formatting and copy

* fix: nullable fixes pre-migration
2019-09-25 21:31:12 +00:00
Tessa Thornton a5c3e94751 [CORL-544] use react helmet to add favicon declarations to head (#2595)
* use react helmet to add favicon declarations to head

* rename adminHead to head

* fix: case
2019-09-25 20:22:32 +00:00
Wyatt Johnson 5c1eb57d3e chore: cleaned up package.json (#2591) 2019-09-24 19:54:05 +00:00
Wyatt Johnson 7b5b260a18 chore: version bump (#2589) 2019-09-24 18:08:05 +00:00
Kim Gardner 1c24060f94 Add no comments copy in danish (#2586) 2019-09-24 17:57:31 +00:00
Vinh 4e4986fc03 fix: some form fields are not nullable (#2577) 2019-09-23 21:36:24 +00:00
Cristian Dean 37e39ff8c5 Avoid compute metrics with 404 status code (#2587) 2019-09-23 21:26:47 +00:00
Wyatt Johnson fe2d78f1f7 [CORl-640] User Registration Race (#2583)
*  fix: fixes user registration endpoints

- fixes #2579

* feat: cleanup from review

- Added seperate create function
- Moved some validation around

* fix: linting
2019-09-23 21:11:16 +00:00
Kim Gardner 43844bca18 Danish translation fixes (#2582)
* Update da files for email and errors

* Update da translations

* Add errors translations
2019-09-23 15:03:44 -04:00
Wyatt Johnson 6fb772944c feat: expanded allowed characters in usernames to support accents (#2585) 2019-09-23 14:55:38 -04:00
Tessa Thornton b3064c89de [CORL-623] add live updates for comments moving from premod to approved (#2571)
* add live updates for comments moving from premod to approved

* rename disposables

* check if comment is system withheld as well as premod to publish event
2019-09-20 20:11:05 +00:00
Nick Funk e4bfbbc1f8 Update COMMENT_DETECTED_SPAM marker text to Spam Detected (#2576)
Since this is used by Akismet and other spam detection tools, this
wording matches what Wordpress users will expect when seeing comments
detected as spam.

CORL-395
2019-09-20 19:48:00 +00:00
Wyatt Johnson 25c7f89b31 [CORL-620] Persisted Query Improvements (#2567)
* fix: resolves #2566

* fix: improved retry logic
2019-09-19 18:49:57 +00:00
Wyatt Johnson 921461008e [CORL-621] Auth Fixes (#2569)
* fix: resolve error with redirects

- fixes #2529

* fix: apply validations to username for oidc

* fix: converted components to function components

* fix: snapshots
2019-09-18 14:01:06 -04:00
Wyatt Johnson 64f102e6d4 [CORL-540] Logging improvements (#2565)
* fix: enhanced errors around story creation

* feat: enhanced child loggers

* feat: logging enhancements
2019-09-18 13:07:42 -04:00
Nick Funk 741739bc16 Un-feature a comment when it is rejected (#2555)
Also decrements the featured count for the story if rejected using the
in-stream moderation.

CORL-554
2019-09-18 12:17:31 -04:00
Sami Safatli 2cc32d4332 Translation pt-BR (#2542) 2019-09-18 11:47:34 -04:00
Tessa Thornton 867f0993cc [CORL 581] toggleable timestamp (#2561)
* toggle between relative and absolute timestamps

* make one common timestamp component

* remove unused timestamp component from /admin
2019-09-17 17:11:52 -04:00
Nick Funk 24a27ca958 Show the viewer's username in "moderated by" on moderation cards (#2573)
If a mod is looking at a card they themselves moderated, we want their
username to show up under "moderated by". This helps them recognize cards
they themselves moderated.

CORL-615
2019-09-17 10:59:03 -06:00
Nick Funk d03b180a30 Set createdAt in optimistic response during SuspendUserMutation steps (#2575)
CORL-616
2019-09-17 09:51:43 -06:00
Tessa Thornton 72ef65577b [CORL-605] remove user agent margin on safari search bar (#2559)
* remove user agent padding on safari search bar

* remove margin on other text fields too

* add margin reset to TextField
2019-09-13 20:29:01 +00:00
Tessa Thornton 8ef366f132 move toxic comment detection ahead of recent history in moderation pipeline (#2562) 2019-09-13 20:21:23 +00:00
Tessa Thornton 3114727410 [CORL-606] fix incorrect translation string (#2560)
* fix incorrect translation string

* update snap
2019-09-12 16:59:22 +00:00
Tessa Thornton a89dc4675a [CORL-516] forgot password flow in admin (#2558)
* add forgot password page and link to admin

* add translations

* update snaps

* fix: install patch

* fix: adapted localizations

* fix: added email type to email field

* fix: updated snapshots
2019-09-12 16:30:19 +00:00
Vinh 2559d1c0e8 fix: turn off bidi characters in favor of <bidi> tags (#2557) 2019-09-12 16:10:17 +00:00
Tessa Thornton ea3c1e3b58 fix safari user drawer display bugs (#2554) 2019-09-12 16:01:58 +00:00
Wyatt Johnson 457644c45b fix: fixed heroku install button (#2551) 2019-09-10 19:40:18 +00:00
Nick Funk 1b9a72c2f0 Show text field selection border on Firefox (#2550)
CORL-515
2019-09-10 19:28:03 +00:00
dependabot[bot] 47834a82bb chore(deps): bump lodash.merge from 4.6.1 to 4.6.2 (#2549)
Bumps [lodash.merge](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-10 16:14:30 +00:00
dependabot[bot] a8976a2141 chore(deps): bump lodash.template from 4.4.0 to 4.5.0 (#2548)
Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-10 15:57:40 +00:00
dependabot[bot] 718bd2758f chore(deps-dev): bump lodash-es from 4.17.11 to 4.17.14 (#2547)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-10 15:46:26 +00:00
Kim Gardner 721b2e7d67 [CORL-527] English copy tweaks (#2546)
* English copy tweaks

* fix: unit tests

* Use reactions to be more general

* fix: snapshots
2019-09-10 20:55:34 +07:00
Nick Funk f9e1bf144e [CORL-523] Document GraphQL mutations for managing accounts (#2544)
* Create preliminary endpoint for updating user names via API

CORL-523

* Create preliminary endpoint for updating user emails via API

CORL-523

* Create preliminary endpoint for deleting users via the API

CORL-523

* Hook up the delete, update email, and update user name endpoints

CORL-523

* Update readme to include preliminary GDPR endpoint instructions

CORL-523

* Uncomment limiters on updateEmail endpoint

My mistake leaving these commented while testing, should not have been committed.

CORL-523

* Run DocToc on the README to generate table of contents

CORL-523

* feat: enhanced documentation on account management edges

* fix: prevent double delete of user account

* fix: swapped order of params

* Update README.md

* Update README.md

* fix: remove unknown field title

* fix: remove unknown field title on story
2019-09-10 20:16:40 +07:00
Tessa Thornton e86e9d2c68 [CORL-586] update logos to match new branding (#2540)
* update logos to match new branding

* update styles of logo

* move version number to bottom of admin

* update specs
2019-09-10 09:02:35 -04:00
Kim Gardner d031df38f6 Add new da translations (#2543) 2019-09-10 10:27:53 +00:00
Nick Funk 8d368b03ec Add call out placeholder for comment stream empty state (#2545)
CORL-567
2019-09-10 10:18:36 +00:00
Tessa Thornton 3e64aa1193 [CORL-591] add reactions to my comments history (#2541)
* add reactions to my comments history

* remove unused field

* fix lanaguage for reactions
2019-09-10 09:52:05 +00:00
Tessa Thornton f84c3e4a60 [CORL-595] move notification settings update button (#2538)
* move notification settings update button

* move settings save button

* fix: version bump in package lock file
2019-09-10 08:13:14 +00:00
Tessa Thornton 36225e443b [CORL-556] Allow session length configuration (#2530)
* make session duration configurable by tenant

* fix expiration date calculation

* add form field for session length

* use constant for default session

* update spec

* update fixtures

* add missing translation

* update snap

* fix: use shared variable instead of hardcoded value for tests

* fix: adjusted missing expires in parameter
2019-09-10 08:06:07 +00:00
Tessa Thornton ed8be75774 fix styling on radio inputs for long labels (#2539) 2019-09-09 12:06:25 -04:00
Tessa Thornton f8cf4e94d3 change button style for change username box (#2537)
* change button style for change username box

* fix lints
2019-09-09 11:53:28 -04:00
Tessa Thornton 1a69f48087 show cancel button for ignore commenters config (#2536) 2019-09-09 11:39:55 -04:00
Nick Funk e0ab799c84 Add hover state to navigation links in Admin > Config (#2533) 2019-09-09 09:03:30 -06:00
Nick Funk aa8e1e2d7a [CORL-560] Update Admin > Config area to match new designs (#2514)
* Add solid background to Moderate > Config area headers

CORL-560

* Update Config > General section text and font weight

CORL-560

* Update ordering of Config > Moderation page items

CORL-560

* Update text and font weights in Configure > Moderation page

CORL-560

* Update text and font weight on Configure > Organization page

CORL-560

* Update text and font weight on Configure > Banned and Suspect Words page

CORL-560

* Update text and font weight on Configure > Authentication page

CORL-560

* Update text and font weights on Configure > Email page

CORL-560

* Update text and font weights on Configure > Advanced page

CORL-560

* Repair tests around the styling changes made to the moderation config area

CORL-560

* Fix capitalization on the Config > General page

CORL-560

* Make email checkbox light themed so its text shows up on new headers

CORL-560

* Update Header width to account for padding

Prevents it from sticking out over the edge of the page layout

CORL-560

* Align "Sort by" text to the drop down in Admin > Config > General > Reactions

CORL-560

* Update Admin > Config > General snapshot to adjust for vertical aligned text

Vertical flex aligned the "Sort by" text to a combo box in a previous changed

CORL-560

* Rename "Perspective Toxic Comment Filter" header to "Toxic Comment Filter"

CORL-560

* Use overlay scrolling if available to prevent scroll bar width popping

CORL-560

* Remove overflow-y: overlay

CORL-560

* Rename Akismet Spam Detection to Spam Detection

CORL-560

* Make all non-complex text fields full width in Admin > Config

CORL-560

* Remove Authentication Integrations header

Area underneath was empty

CORL-560

* Re-arrange SSO Key Field layout

CORL-560

* Create new typography types for bodyShort and fieldDescription

CORL-560

* Update Admin > Config to use bodyShort, fieldDescription typography

CORL-560

* Add a slight padding on InputLabel

CORL-560

* Add padding on either side of Admin > Config section content

CORL-560
2019-09-06 12:29:27 -06:00
Tessa Thornton d2af78d3d7 [CORL 568] more user drawer links (#2532)
* add drawer links for in reply to and moderated by

* fix ts

* update spec
2019-09-06 14:01:19 -04:00
Tessa Thornton 9bd38db61a allow organization url config (#2531) 2019-09-06 13:52:48 -04:00
Tessa Thornton ac203607ae [CORL-561, CORL-562] Comment stream and account profile styling cleanup (#2517)
* update styles for comment stream

* more comment styling fixes

* reconfigure settings tab and account settings in stream

* add new button variant

* style settings tab

* style change username/email/password

* clean up settings tab

* integrate account deletion to account settings

* update comment history styles

* more history comment styles

* update snaps

* move userbox

* fix tests

* fix delete account tests

* update message box styling

* update snap

* update snaps

* fix lints

* update snaps again

* userbox styles

* history comment styling

* update snaps

* fix ts

* Integrate notifications settings

* clean up account setting styles

* update snaps and tests

* fix lints

* fix: PENDING_DELETION permissions

* chore: sync stable classnames, rename settings to account

* simplify css

* update snaps
2019-09-06 13:41:55 -04:00
Nick Funk e49905c5ad [CORL-571] Filter empty strings from banned and suspect word lists (#2521)
* Filter empty strings from banned and suspect word lists

CORL-571

* fix: changed form
2019-09-05 21:36:09 +00:00
Kim Gardner 15d41f1ae8 Typo fixes and bump Coral version to 5.0.0 (#2528)
* Typo fixes and bump package version

* fix: tests
2019-09-05 17:15:40 -04:00
Wyatt Johnson 60a1f43d51 fix: wrap queries in quotes (#2527) 2019-09-05 18:06:12 +00:00
Wyatt Johnson 3c9ebcf1a1 feat: added moderation email notifications (#2525) 2019-09-05 17:33:49 +00:00
Wyatt Johnson e35978096f feat: added featured comment notifications (#2524) 2019-09-05 17:10:03 +00:00
Wyatt Johnson 04c56b3fb5 [CORL-406] Tenant Locale Selection (#2450)
* feat: added preload config

* feat: support changing locale

* fix: name case

* fix: removed unused code

* feat: added translations for default reactions

* fix: do not translate icon

* fix: shorter i18n keys
2019-09-05 17:02:06 +00:00
Tessa Thornton 5bf4f22931 [CORL-392] add settings config for story configuration on advanced panel (#2499)
* add settings config for story configuration on advanced panel

* include external link in proxy description

* update copy

* update copy

* fix lints

* update snap
2019-09-05 11:35:16 -04:00
Tessa Thornton 1550377f39 update durationfield component to not use lifecycle methods (#2502)
* update durationfield component to not use lifecycle methods

* fix: default param cleanup
2019-09-05 09:51:51 -04:00
Wyatt Johnson efea0e8e1c [CORL-498, CORL-495, CORL-539, CORL-496, CORL-494] Email Notifications Support & Framework (#2498)
* chore: renamed old templates

* feat: initial notifications support

* feat: email enhancements

* fix: linting

* feat: initial digesting beheviour

* feat: added notification configuration

* feat: added unsubscribe routes

* fix: fixed failing snapshots/tests bc random ids

* feat: adjusted the save beheviour, added tests

* feat: added tests

* feat: added staff replies

* feat: renamed E-Mail to Email

* feat: enhanced cron processing

* fix: linting + updating tests

* feat: enhanced cron context

* fix: added staff replies back in
2019-09-05 07:02:26 +00:00
Wyatt Johnson 0fad1070a6 fix: addressed issue with count doubling (#2518) 2019-09-04 17:33:42 -04:00
Wyatt Johnson ac1ed45e4b [CORL-588] Download and token fixes (#2519)
* fix: fixed up download handler to improve error handling

* fix: fixed invalid expiry date being set

* fix: nick found bug
2019-09-04 17:23:59 -04:00
Nick Funk a696ef9be5 [CORL-572] Filter rejected comments from reply counts on user comment history (#2516)
* Filter rejected comments from reply counts on user comment history

CORL-572

* fix: use cache to improve performance

* fix: added TODO
2019-09-04 12:43:00 -04:00
Vinh aa32fcaea6 [CORL-550] More stable css classes (#2515)
* feat: more stable css classes

* chore: renamee ValidationMessageHelper to FormValidationMessage

* chore: Rename FormValidationMessage to FieldValidationMessage

* feat: more css classes, icons use <i> tag

* feat: stable classnames for gdpr features

* feat: more css stable classnames
2019-09-04 12:34:42 -04:00
Tessa Thornton fcf3640adc [CORL-439] Allow admins to configure GDPR features (#2497)
* allow tenant admins to modify commenter account features

* style account features configuration

* prevent changing username or downloading comments based on tenant settings

* update fixtures and snaps

* add i18n strings

* update snap
2019-09-03 16:56:36 -06:00
Nick Funk 46f0d08cf7 [CORL-236] Allow user to delete their account (#2495)
* Create preliminary account deletion program flow

Does not validate password as of yet.
Does not submit a mutation to begin deletion scheduling.

CORL-236

* Create preliminary account deletion confirmation form and schema mutation

Hooks up validation and error handling to confirm the user's
password during account deletion.

Scheduling the account deletion is not implemented yet.

CORL-236

* Update requestAccountDeletion to set the user's scheduled deletion date

CORL-236

* Create preliminary cron job for scheduled account deletions

CORL-236

* Show tombstoned comments when the user has deleted their account and comments

CORL-236

* Pull in client locales for date formatting on account deletion steps

CORL-236

* Update more areas due to comment revision being nullable

CORL-236

* Allow users to cancel their account deletion request

CORL-236

* Show account deletion callout at the top of the profile stream tab

CORL-236

* Further comments and edge case handling for deleted comments site wide

CORL-236

* Show account deletion callout in the comments stream tab

CORL-236

* Send out emails during relevant account deletion events

- Confirm deletion requested
- Cancellation of deletion request
- Account deletion completed

CORL-236

* Unset emails when deleting user accounts instead of setting to null

Fixes email index collision error from setting multiple emails to null.

CORL-236

* Show organization email in account deletion steps

CORL-236

* Mark comments as deleted so that we can properly tombstone them in the stream

CORL-236

* Fix improper templating for account deletion completed emails

Sub organization name in where previously we were using the account username.

CORL-236

* Disable user drawer and display deleted flag on users in community

CORL-236

* Check for author before passing inReplyTo in ModerateCardContainer

Sometimes null when user is deleted.

CORL-236

* Tombstone moderate cards when the underlying comment is deleted

CORL-236

* Re-add deleted field to CommentContainer fragment

CORL-236

* Disable commenting or replying when user is scheduled for deletion

CORL-236

* Add snapshot tests around deletion states for comment cards and forms

CORL-236

* Add tests around the account deletion steps

CORL-236

* Set account deletion to run every 30 minutes

CORL-236

* Rename DELETION authorization to PENDING_DELETION

CORL-236

* Change log message for when account deletion tasks finds no more users to delete

CORL-236

* Update snapshots to handle proper deleted flag for scheduled deleting users

CORL-236

* Fix import ordering on various files to match coding standards

CORL-236

* Specify tenantID when querying for user during deletion

CORL-236

* Add back missing clientMutationId on cancelAccountDeletion mutation

CORL-236

* Use $unset to clear the scheduledDeletionDate

CORL-236

* Simplify create of scheduledAccountDeletion CRON function

CORL-236

* Rename and organize scheduledTask creation logic

- Rename startCronJobs to startScheduledTasks
- Return an object of named ScheduledTask items instead of an array of tasks

CORL-236

* Add comment about returning updated user when clearing scheduledDeletionDate

CORL-236

* Remove unnecessary array around tenant.locale in dateTime formatter

CORL-236

* Remove unnecessary non-null enforcement on tenant.id

CORL-236

* Remove string templated React element content

CORL-236

* Move english translated validation error to correct language file

CORL-236

* Simplify rendering of DeletionRequestCallout when deletionDate isn't available

CORL-236

* Use ternary expression instead of multiple {bool && (...)} checks

CORL-236

* Use classes object with line child style to style the step bar

CORL-236

* Update StepBar so that children of Step nodes can be undefined

Allows us to not have to insert empty placeholder strings/elements when
we want to have an empty step node.

CORL-236

* Remove use of luxon from account deletion request callouts for now

We need to decide if the extra 15KB from luxon is worth it in our bundles
before making this change.

CORL-236

* Remove unnecessary embed/deleteAccount endpoint

CORL-236

* Move DeleteAccountModal contents into a separate component

Cleans up the state management when user closes the modal and we
need the modal steps to restart.

CORL-236

* Remove localization around delete account button icon

CORL-236

* Use inline `collection(mongo)` in clearing and setting deletion date

CORL-236

* Simplify form state validation on account deletion confirm page

CORL-236

* Add todo to iterate over tenants in account deletion

CORL-236

* Remove punctuation and add context to logger messages during account deletion

CORL-236

* Break out mongo collection initialization into a shared object

Also adds a try catch error handling around the scheduled deletion
operations. Prevents errors from taking the server down.

CORL-236

* Add TODO to extract out a common formatter for date times

CORL-236

* Update translations for account deletion details

CORL-236

* Update translations for account deletion completion page

CORL-236

* During scheduled deletion, when locking deletion date, return modified user

CORL-236

* Remove un-necessary fragment container around deletion steps component

CORL-236

* Move preventSubmit outside of render function on ConfirmPage

CORL-236

* Convert collections into object of Mongo db collection initializers

Allows us to only initialize the collections we need with our
Mongo instance.

CORL-236

* Remove un-necessary lambda wrapping around collections initializer functions

CORL-236
2019-09-03 16:38:43 -06:00
Nick Funk fc039f8186 Filter null comments coming into moderation queues to prevent white screening (#2509)
Wyatt and I are not sure why comments are coming back null, but this should
keep the moderation area from blanking out when it errors handling nulled out
comments crashing the queue.

FIXME tag added with Wyatt/Nick as this is only a patching fix and does
not resolve the underlying issue.
2019-08-28 14:21:35 -04:00
Nick Funk 7e7ce9fbd0 Ensure user drawer status is padded properly when a user selects themselves (#2506)
CORL-531
2019-08-26 11:51:54 -04:00
Tessa Thornton 0e37a474fa downloads by user id (#2500) 2019-08-23 16:05:53 -04:00
Tessa Thornton ffecd5e981 change routing for auth and auth/callback (#2493) 2019-08-23 15:00:36 -04:00
Tessa Thornton 07df7ac182 show email verification resend (#2492)
* always show users with unverified email address the prompt and resend option

* udpate snaps

* update snap
2019-08-23 14:16:08 -04:00
Wyatt Johnson 2edabf44c7 feat: added closedAt to createStory mutation (#2496) 2019-08-23 14:01:43 -04:00
Wyatt Johnson 1c75b7659d fix: added more strict handling of input for URL config (#2478) 2019-08-23 13:51:13 -04:00
Nick Funk 6d803b0179 [CORL-382] Allow moderators to ban a commenter from the comment stream (#2385)
* Add a ban user action to the stream moderation drop down

CORL-382

* Show banned stated for user in stream moderation drop down

If the user is actively banned, the ban user option will be
disabled and show a status of "banned".

CORL-382

* Create utility for generating random stories, comments, and users

CORL-382

* Add ban and suspension values to baseUser in fixtures

CORL-382

* Updated banned.spec.tsx to use new test utilities for generating fixture data

CORL-382

* Prevent users from being able to ban themselves in the moderation dropdown

CORL-382

* Kill optimistic response errors for comment mutations

Set the author.status.current to an empty array so it stops
complaining about it being unused.

CORL-382

* Rename util in tests to helpers/fixture.ts

CORL-382

* Remove unused import from CreateCommentReplyMutation.ts

CORL-382

* Put back the optimistic ban responses into comment mutations

The warnings spewing out during tests are false, for further
detail please see: https://github.com/facebook/relay/pull/2760

CORL-382

* Denormalize generated stories and comments

CORL-382

* Clean up import ordering in ModerationActionsContainer.tsx

CORL-382

* Inject appropriate scoped items into callbacks for moderation dropdown

CORL-382

* Set optimistic response author status from known viewer status

CORL-382

* fix: Make in stream banning work

* feat: Send translated email + reject comment + fix tests

* test: add feature test

* feat: add copy mentioning comment rejection

* chore: improve loading state

* chore: add tiny comment
2019-08-22 22:47:30 +07:00
Vinh f3e615a610 [CORL-503] More stable CSS classNames (#2477)
* feat: add more css classnames

* fix: snapshots
2019-08-20 19:22:58 +00:00
Vinh b9f9e9b2d4 [CORL-491] Support event alias loginPrompt (#2474)
* feat: support event alias loginPrompt

* lint: import ordering
2019-08-21 00:58:44 +07:00
Tessa ThorntonandWyatt Johnson 7809cd3d68 [CORL-476] Add badges to user from SSO token (#2470)
* fix: bug in lookup not checking object properly before accessing

* fix: Recursive types not handling optional arrays

* add user badges component

* create user badges from sso token

* update badges type

* revert src/core/client/embed/index.html

* remove duplicated line

* add user badges component

* create user badges from sso token

* revert src/core/client/embed/index.html

* remove duplicated line

* fix types

* fix tests and snaps

* add user badges to user drawer

* update snaps

* update readme

* [CORL-476] add user role from SSO token (#2475)

* add role from token

* use joi to validate role values

Co-Authored-By: Wyatt Johnson <wyattjoh@gmail.com>

* simplify sso.role validation

* add test for invalid role in sso token
2019-08-20 13:15:59 -04:00
Tessa Thornton add5224338 [CORL-504] Configure reaction language (#2490)
* add reaction config to settings

* fix reaction preview styles

* update snaps and fixtures

* add i18n strings

* update spec

* ensure dynamic value shows in reaction config preview
2019-08-20 13:03:25 -04:00
Kim Gardner 8dae0feb52 Updates version number (#2488)
* Updates version number

* fix: updated version in package-lock.json
2019-08-20 12:47:30 -04:00
Tessa Thornton ee93267d97 [CORL-220] Change email address (#2461)
* change email form

* add update email mutation

* validate new email addresses

* add email verification callout

* add translation strings

* fix submit button logic

* rename model methods

* style email verifcation box

* resend email verificatoin button

* show success message on email resend

* update user profile email

* fix duplicate import

* add change email spec

* fix profile spacing

* update snapshots

* update snaps

* add preventSubmit function to email change component

* update business logic for profile updating

* update logic for when users can update email or username

* check for less specific duplicate email error

* prevent sso-only users from editing email

* only allow email and username edit if enabeld in local profile

* use generic server error for cannot update profile

* remove merge conflict

* fix tests

* extract logic to get auth integrations

* fix merge error
2019-08-20 12:28:14 -04:00
Tessa Thornton 0f02b05689 update readme with info about watchman (#2486)
* update readme with info about watchman

* run doctoc

* fix: reordered sections

* run doctoc
2019-08-20 12:14:49 -04:00
Tessa Thornton 3baf4e96f5 prevent improper sort menu styling on ios (#2468) 2019-08-20 11:37:09 -04:00
Wyatt Johnson 8d91bd7dd1 fix: corrected invalid copy (#2479) 2019-08-19 12:57:03 -04:00
Vinh d400c1518c fix: temporary use min-height for single comment view to not cutoff report dialog (#2476) 2019-08-19 12:34:39 -04:00
Wyatt Johnson d9ad1b2ee4 [CORL-529] Proxy (#2473)
* fix: implemented trust configuration

* fix: removed debugging code

* fix: increase hash size (#2484)
2019-08-16 21:03:20 +00:00
Wyatt Johnson 035ee73edd [CORL-543] Persisted Query Fix (#2482)
* fix: addressed query loading bug

* fix: refine logging
2019-08-16 16:09:59 +00:00
Vinh 43b6a2cdcd [CORL-149] Persisted Queries (#2445)
* feat: enable persisted queries on the client

* fix: use `id` inside websocket message

* feat: initial server support for PQ

* feat: deeper server support

* feat: abstracted persisted query replacing logic
2019-08-15 21:03:32 +00:00
Vinh 635e740fc0 [next] Fix lookup and recursive types (#2472)
* fix: bug in lookup not checking object properly before accessing

* fix: Recursive types not handling optional arrays
2019-08-15 01:07:49 +07:00
Tessa Thornton c37ee12e57 Bugfix/userdrawer changestatus (#2469)
* fix date formatting breaking user details popvoer

* update mutation return fields for user status changes
2019-08-14 08:32:03 -04:00
Wyatt Johnson e76fddd13c [next] Added trust proxy configuration (#2471)
* feat: added trust proxy configuration

* chore: version bump
2019-08-13 15:41:50 -04:00
1221 changed files with 37897 additions and 15087 deletions
+3
View File
@@ -0,0 +1,3 @@
*.d.ts
*.graphql.ts
**/__generated__/**
+208
View File
@@ -0,0 +1,208 @@
const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/eslint-recommended').default.overrides[0];
const typescriptRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/recommended.json');
const typescriptRecommendedTypeChecking = require('@typescript-eslint/eslint-plugin/dist/configs/recommended-requiring-type-checking.json');
const typescriptEslintPrettier = require('eslint-config-prettier/@typescript-eslint');
const react = require('eslint-plugin-react').configs.recommended;
const jsxA11y = require('eslint-plugin-jsx-a11y').configs.recommended;
const reactPrettier = require('eslint-config-prettier/react');
const typescriptOverrides = {
files: ["*.ts", "*.tsx"],
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
plugins: [
"@typescript-eslint",
"@typescript-eslint/tslint",
"react",
"jsx-a11y",
],
settings: {
react: {
version: "detect",
}
},
rules: Object.assign(
typescriptEslintRecommended.rules,
typescriptRecommended.rules,
typescriptEslintPrettier.rules,
react.rules,
jsxA11y.rules,
reactPrettier.rules,
{
"@typescript-eslint/adjacent-overload-signatures": "error",
// TODO: (cvle) change `readonly` param to `array-simple` when upgraded typescript.
"@typescript-eslint/array-type": ["error", { "default": "array-simple", "readonly": "generic"}],
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"overrides": {
"constructors": "off",
},
},
],
"@typescript-eslint/indent": "off",
"@typescript-eslint/interface-name-prefix": "error",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
"@typescript-eslint/no-use-before-define": "off", // TODO: (cvle) Should be on?
"@typescript-eslint/no-use-before-declare": "off",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/type-annotation-spacing": "off",
"@typescript-eslint/unified-signatures": "error",
// (cvle) disabled, because the way we use labels in our code cause to many
// false positives.
"jsx-a11y/label-has-associated-control": "off",
"react/display-name": "error",
"react/prop-types": "off",
"react/no-unescaped-entities": "off",
}
),
};
let typescriptTypeCheckingOverrides = {
files: ["*.ts", "*.tsx"],
parserOptions: {
project: ["tsconfig.json", "./src/tsconfig.json", "./src/core/client/tsconfig.json"],
// TODO: (cvle) this is a workaround, see: https://github.com/typescript-eslint/typescript-eslint/issues/1091.
createDefaultProgram: true,
},
rules: Object.assign(
typescriptRecommendedTypeChecking.rules,
{
"@typescript-eslint/tslint/config": ["error", {
"rules": {
"ordered-imports": {
"options": {
"import-sources-order": "case-insensitive",
"module-source-path": "full",
"named-imports-order": "case-insensitive",
},
},
},
}],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/unbound-method": "off", // 10.10.19: (cvle) seems to give false positive.
}
),
};
const jestOverrides = {
env: {
jest: true,
},
files: ["test/**/*.ts", "test/**/*.tsx"],
globals: {
"expectAndFail": "readonly",
"fail": "readonly",
},
};
// Setup the overrides.
const overrides = [jestOverrides, typescriptOverrides];
// Skip type information to make it faster!
if (process.env.FAST_LINT !== "true") {
overrides.push(typescriptTypeCheckingOverrides);
}
module.exports = {
overrides,
env: {
browser: true,
es6: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:jsdoc/recommended",
"plugin:prettier/recommended",
],
parserOptions: {
"ecmaVersion": 2018,
},
rules: {
"arrow-body-style": "off",
"arrow-parens": [
"off",
"as-needed",
],
"camelcase": "off",
"complexity": "off",
"constructor-super": "error",
"spaced-comment": ["error", "always"],
"curly": "error",
"dot-notation": "error",
"eol-last": "off",
"eqeqeq": "error",
"guard-for-in": "error",
"jsdoc/require-jsdoc": "off",
"jsdoc/require-returns": "off",
"jsdoc/require-param": "off",
"jsdoc/require-param-type": "off",
"jsdoc/require-returns-type": "off",
"linebreak-style": "off",
"max-classes-per-file": [
"error",
1,
],
"member-ordering": "off",
"new-parens": "off",
"newline-per-chained-call": "off",
"no-bitwise": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": "error",
"no-debugger": "error",
"no-empty": "error",
"no-eval": "error",
"no-extra-semi": "off",
"no-fallthrough": "error",
"no-invalid-this": "off",
"no-irregular-whitespace": "off",
"no-multiple-empty-lines": "off",
"no-new-wrappers": "error",
"no-prototype-builtins": "off",
"no-shadow": "error",
"no-throw-literal": "error",
"no-undef": "off",
"no-undef-init": "error",
"no-unsafe-finally": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
"no-var": "error",
"object-shorthand": "error",
"one-var": "off",
"prefer-arrow-callback": "off",
"prefer-const": "error",
"quote-props": "off",
"radix": "error",
"require-atomic-updates": "off",
"space-before-function-paren": "off",
"sort-imports": "off",
"use-isnan": "error",
"valid-typeof": "off",
},
};
+1
View File
@@ -21,3 +21,4 @@ dist
*.css.d.ts
__generated__
README.md.orig
persisted-queries.json
+3 -2
View File
@@ -1,8 +1,9 @@
{
"recommendations": [
"ms-vscode.vscode-typescript-tslint-plugin",
"dbaeumer.vscode-eslint",
"kumar-harsh.graphql-for-vscode",
"editorconfig.editorconfig",
"ms-azuretools.vscode-cosmosdb"
"ms-azuretools.vscode-cosmosdb",
"mike-co.import-sorter"
]
}
+41 -5
View File
@@ -10,12 +10,48 @@
"**/.DS_Store": true,
".vs": true
},
"tslint.exclude": "**/node_modules/**",
"tslint.jsEnable": true,
"tslint.enable": false,
"eslint.validate": [
{ "language": "javascript", "autoFix": true },
{ "language": "typescript", "autoFix": true },
{ "language": "typescriptreact", "autoFix": true }
],
"typescript.tsdk": "node_modules/typescript/lib",
"postcss.validate": false,
"javascript.preferences.importModuleSpecifier": "non-relative",
"editor.codeActionsOnSave": {
"source.fixAll.tslint": true
}
"importSorter.importStringConfiguration.quoteMark": "double",
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 80,
"importSorter.sortConfiguration.customOrderingRules.rules": [
{
"type": "importMember",
"regex": "^$",
"orderLevel": 70,
"disableSort": true
},
{
"regex": "__generated__",
"orderLevel": 40
},
{
"regex": "^coral-",
"orderLevel": 30
},
{
"regex": "\\.css$",
"orderLevel": 60,
"disableSort": true
},
{
"regex": "^[.]",
"orderLevel": 50
},
],
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne",
"importSorter.importStringConfiguration.trailingComma": "multiLine",
"importSorter.importStringConfiguration.tabSize": 2,
"eslint.enable": true,
"importSorter.generalConfiguration.exclude": [
"d\\.ts$",
"__generated__"
],
}
+12 -365
View File
@@ -9,385 +9,32 @@ Built with <3 by The Coral Project, a part of [Vox Media](https://product.voxmed
Preview Coral easily by running Coral via a Heroku App:
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/coralproject/talk/tree/next)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/coralproject/talk)
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents
- [Requirements](#requirements)
- [Running](#running)
- [Docker](#docker)
- [Source](#source)
- [Development](#development)
- [Embed On Your Site](#embed-on-your-site)
- [Single Sign On](#single-sign-on)
- [Email](#email)
- [Design Language System (UI Components)](#design-language-system-ui-components)
- [Configuration](#configuration)
- [Documentation](#documentation)
- [Pre-Launch Guide](#pre-launch-guide)
- [More Resources](#more-resources)
- [License](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Requirements
## Documentation
- MongoDB >=3.6
- Redis >=3.2
- NodeJS >=10
- NPM >=6.7
You can get started with Coral using our [Documentation](https://docs.coralproject.net/talk/).
## Running
## Pre-Launch Guide
You can install Coral using Docker or via Source. We recommend Docker, as it
provides the easiest deployment solution going forward, as all the dependencies
are baked and shipped with the provided
[coralproject/talk:next](https://hub.docker.com/r/coralproject/talk) image.
When v5 releases to master, you'll be able to select it using
`coralproject/talk:5`.
Youve installed Talk on your server, and youre preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/).
### Docker
## More Resources
The easiest way to get started with Coral is through our published Docker image
and provided example `docker-compose.yml` file. The following assumes that you
have Docker and Docker Compose installed on your local machine:
- Install Docker: https://docs.docker.com/install/
- Install Docker Compose: https://docs.docker.com/compose/install/ (this is typically included in the Docker Desktop editions already)
```bash
# Create directories to persist the data in MongoDB and Redis.
mkdir -p data/{mongo,redis}
# Create the `docker-compose.yml` file to get started.
cat > docker-compose.yml <<EOF
version: "2"
services:
talk:
image: coralproject/talk:next
restart: always
ports:
- "127.0.0.1:3000:5000"
depends_on:
- mongo
- redis
environment:
- MONGODB_URI=mongodb://mongo:27017/coral
- REDIS_URI=redis://redis:6379
- SIGNING_SECRET=<replace me with something secret>
mongo:
image: mongo:3.6
volumes:
- ./data/mongo:/data/db
redis:
image: redis:3.2
volumes:
- ./data/redis:/data
EOF
# Start up Coral using Docker.
docker-compose up -d
```
Then head on over to http://localhost:3000 to install Coral!
### Source
Coral requires NodeJS >=10, we recommend using `nvm` to help manage node
versions: https://github.com/creationix/nvm.
```bash
# Clone and cd into the Coral directory.
git clone https://github.com/coralproject/talk.git
cd talk
# Install dependencies.
npm install
# Build the application dependencies.
# This might take a while.
npm run build
```
This should output all the compiled application code to `./dist`.
Running Coral with default settings assumes that you have:
- MongoDB >=3.6 running on `127.0.0.1:27017`
- Redis >=3.2 running on `127.0.0.1:6379`
If you don't already have these databases running, you can execute the following
assuming you have Docker installed on your local machine:
```bash
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
```
Then start Coral with:
```bash
# Start the server in production mode.
npm run start
```
Then head on over to http://localhost:3000 to install Coral!
### Development
Running Coral for development is very similar to installing Coral via Source as
described above.
Coral requires NodeJS >=10, we recommend using `nvm` to help manage node
versions: https://github.com/creationix/nvm.
```bash
# Clone and cd into the Coral directory.
git clone https://github.com/coralproject/talk.git
cd talk
# Install dependencies.
npm install
```
Running Coral with default settings assumes that you have:
- MongoDB >=3.6 running on `127.0.0.1:27017`
- Redis >=3.2 running on `127.0.0.1:6379`
If you don't already have these databases running, you can execute the following
assuming you have Docker installed on your local machine:
```bash
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
```
Then start Coral with:
```bash
# Run the server in development mode in order to facilitate auto-restarting and
# rebuilding when file changes are detected. This might take a while to fully run.
npm run watch
```
When the client code has been built, navigate to http://localhost:8080/install
to start the installation wizard. **Note: Ensure `localhost:8080` is used in the permitted domains list.**
To see the comment stream goto http://localhost:8080/.
To run linting and tests use the following commands:
```bash
# Run the linters.
npm run lint
# Run our unit and integration tests.
npm run test
```
#### Embed On Your Site
With Coral setup and running locally you can test embeding the comment stream with this sample embed script:
```
<div id="coral_thread"></div>
<script type="text/javascript">
(function() {
var talk = document.createElement('script'); talk.type = 'text/javascript'; talk.async = true;
var url = '{{ CORAL_DOMAIN_NAME }}';
talk.src = '//' + url + '/assets/js/embed.js';
talk.onload = function() {
Coral.createStreamEmbed({
id: "coral_thread",
autoRender: true,
rootURL: '//' + url,
});
};
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(talk);
})();
</script>
```
> **NOTE:** Replace the value of `{{ CORAL_DOMAIN_NAME }}` with the location of your running instance of Coral.
#### Single Sign On
In order to allow seamless connection to an existing authentication system,
Coral utilizes the industry standard [JWT Token](https://jwt.io/) to connect. To
learn more about how to create a JWT token, see [this introduction](https://jwt.io/introduction/).
1. Visit: `https://{{ CORAL_DOMAIN_NAME }}/admin/configure/auth`
2. Scroll to the `Login with Single Sign On` section
3. Enable the Single Sign On Authentication Integration
4. Enable `Allow Registration`
5. Copy the string in the `Key` box
6. Click Save
> **NOTE:** Replace the value of `{{ CORAL_DOMAIN_NAME }}` with the location of your running instance of Coral.
You will then have to generate a JWT with the following claims:
- `jti` (_optional_) - A unique ID for this particular JWT token. We recommend
using a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)
for this value. Without this parameter, the logout functionality inside the
embed stream will not work and you will need to call logout on the embed
itself.
- `exp` (_optional_) - When the given SSO token should expire. This is
specified as a unix time stamp in seconds. Once the token has expired, a new
token should be generated and passed into Coral. Without this parameter, the
logout functionality inside the embed stream will not work and you will need
to call logout on the embed itself.
- `iat` (_optional_) - When the given SSO token was issued. This is required to
utilize the automatic user detail update system. If this time is newer than
the time we received the last update, the contents of the token will be used
to update the user.
- `user.id` (**required**) - the ID of the user from your authentication system.
This is required to connect the user in your system to allow a seamless
connection to Coral.
- `user.email` (**required**) - the email address of the user from your
authentication system. This is required to facilitate notification email's
about status changes on a user account such as bans or suspensions.
- `user.username` (**required**) - the username that should be used when being
presented inside Coral to moderators and other users.
An example of the claims for this token would be:
```json
{
"jti": "151c19fc-ad15-4f80-a49c-09f137789fbb",
"exp": 1572172094,
"iat": 1562172094,
"user": {
"id": "628bdc61-6616-4add-bfec-dd79156715d4",
"email": "bob@example.com",
"username": "bob"
}
}
```
With the claims provided, you can sign them with the `Key` obtained from the
Coral administration panel in the previous steps with a `HS256` algorithm. This
token can be provided in the above mentioned embed code by adding it to the
`createStreamEmbed` function:
```js
Coral.createStreamEmbed({
// Don't forget to include the parameters from the
// "Embed On Your Site" section.
accessToken: "{{ SSO_TOKEN }}",
});
```
Or by calling the `login/logout` method on the embed object:
```js
var embed = Coral.createStreamEmbed({
// Don't forget to include the parameters from the
// "Embed On Your Site" section.
});
// Login the current embed with the generated SSO token.
embed.login("{{ SSO_TOKEN }}");
// Logout the user.
embed.logout();
```
#### Email
To test out the email sending functionality, you can run [inbucket](https://www.inbucket.org/)
which provides a test SMTP server that can visualize emails in the browser:
```bash
docker run -d --name inbucket --restart always -p 2500:2500 -p 9000:9000 inbucket/inbucket
```
You can then configure the email server on Coral
by setting the email settings in
`Configure -> Email` in the admin:
| Field | Value |
| -------------- | -------------------- |
| From Address | `community@test.com` |
| Secure | `No` |
| Host | `localhost` |
| Port | `2500` |
| Authentication | `No` |
Navigate to http://localhost:9000, click the "Monitor" tab. New emails received
on this screen.
#### Design Language System (UI Components)
We use [docz](https://docz.site) to document and develop our Design Language System. To start docz run:
```bash
# Make sure CSS types are generated.
# This is not required when `npm run watch` is already running.
npm run generate:css-types
# Run docz in development.
npm run docz -- dev
```
After compilation has finished you can access docz at http://localhost:3030/.
## Configuration
The following environment variables can be set to configure the Coral Server. You
can expose them in your shell via `export NODE_ENV=development` or by placing
the variables in a `.env` file in the root of the project in a simple
`NODE_ENV=development` format delimited by newlines.
- `NODE_ENV` - Can be one of `production` or `development`. All production
deployments should use `production`. Defaults to `production` when ran with
`npm run start` and `development` when run with `npm run start:development`.
- `PORT` - The port to listen for HTTP and Websocket requests. (Default `3000`)
- `MONGODB_URI` - The MongoDB database URI to connect to.
(Default `mongodb://127.0.0.1:27017/coral`)
- `REDIS_URI` - The Redis database URI to connect to.
(Default `redis://127.0.0.1:6379`)
- `REDIS_OPTIONS` - A JSON string with optional configuration options to be used
when connecting to Redis as specified in the [ioredis](https://github.com/luin/ioredis/blob/1dac50a63753c2afc969315cfe38faf0edc50bc5/API.md#new_Redis_new) documentation.
(Default: `{}`)
- `SIGNING_SECRET` - The shared secret to use to sign JSON Web Tokens (JWT) with
the selected signing algorithm. 🚨 **Don't forget to set this variable!** 🚨
(Default: `keyboard cat`)
- `SIGNING_ALGORITHM` - The signing algorithm to use for signing JWT's.
(Default `HS256`).
- `LOGGING_LEVEL` - The logging level that can be set to one of `fatal`,
`error`, `warn`, `info`, `debug`, or `trace`. (Default `info`)
- `STATIC_URI` - The URI that static assets can be accessed from. This URI can
be to a proxy that uses this Coral server on `PORT` as the upstream. Disabled
by default.
- `DISABLE_TENANT_CACHING` - When `true`, all tenants will be loaded from the
database when needed rather than keeping a in-memory copy in sync via
published events on Redis. (Default `false`)
- `DISABLE_MONGODB_AUTOINDEXING` - When `true`, Coral will not perform indexing
operations when it starts up. This can be desired when you've already
installed Coral on the target MongoDB, but want to improve start performance.
**You should not use this parameter unless you know what you're doing! Upgrades
may introduce additional indexes that the application relies on.**
(Default `false`)
- `LOCALE` - Specify the default locale to use for all requests without a locale
specified. (Default `en-US`)
- `ENABLE_GRAPHIQL` - When `true`, it will enable the `/graphiql` even in
production, use with care. (Default `false`)
- `CONCURRENCY` - The number of worker nodes to spawn to handle web traffic,
this should be tied to the number of CPU's available. (Default
`os.cpus().length`)
- `DEV_PORT` - The port where the Webpack Development server is running on.
(Default `8080`)
- `METRICS_USERNAME` - The username for _Basic Authentication_ at the `/metrics` and `/cluster_metrics`
endpoint.
- `METRICS_PASSWORD` - The password for _Basic Authentication_ at the `/metrics` and `/cluster_metrics`
endpoint.
- `CLUSTER_METRICS_PORT` - If `CONCURRENCY` is more than `1`, the metrics are provided at this port under `/cluster_metrics`. (Default `3001`)
- `DISABLE_LIVE_UPDATES` - When `true`, disables subscriptions for the comment
stream for all stories across all tenants (Default `false`)
- `WEBSOCKET_KEEP_ALIVE_TIMEOUT` - A duration in a parsable format (e.g. `30 seconds`
, `1 minute`) that should be used to send keep alive messages through the
websocket to keep the socket alive (Default `30 seconds`)
- [Our Blog](https://coralproject.net/blog)
- [Community Guides for Journalism](https://guides.coralproject.net/)
- [More About Us](https://coralproject.net/)
## License
+4
View File
@@ -21,6 +21,10 @@
"ENABLE_GRAPHIQL": {
"description": "When true, this will enable the GraphiQL routes",
"value": "false"
},
"TRUST_PROXY": {
"description": "When set to 1, it instructs Coral to trust up to one proxy, this is needed for authentication support",
"value": "1"
}
},
"addons": [
+1
View File
@@ -29,6 +29,7 @@ module.exports = {
"^coral-account/(.*)$": "<rootDir>/src/core/client/account/$1",
"^coral-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
"^coral-auth/(.*)$": "<rootDir>/src/core/client/auth/$1",
"^coral-count/(.*)$": "<rootDir>/src/core/client/count/$1",
"^coral-ui/(.*)$": "<rootDir>/src/core/client/ui/$1",
"^coral-stream/(.*)$": "<rootDir>/src/core/client/stream/$1",
"^coral-framework/(.*)$": "<rootDir>/src/core/client/framework/$1",
+5 -5
View File
@@ -27,7 +27,7 @@ const config: Config = {
"**/test/**/*",
"core/**/*.spec.*",
],
executor: new CommandExecutor("npm run --silent generate:relay-stream", {
executor: new CommandExecutor("npm run --silent generate:relay:stream", {
runOnInit: true,
}),
},
@@ -44,7 +44,7 @@ const config: Config = {
"**/test/**/*",
"core/**/*.spec.*",
],
executor: new CommandExecutor("npm run generate:relay-account", {
executor: new CommandExecutor("npm run generate:relay:account", {
runOnInit: true,
}),
},
@@ -61,7 +61,7 @@ const config: Config = {
"**/test/**/*",
"core/**/*.spec.*",
],
executor: new CommandExecutor("npm run --silent generate:relay-admin", {
executor: new CommandExecutor("npm run --silent generate:relay:admin", {
runOnInit: true,
}),
},
@@ -78,7 +78,7 @@ const config: Config = {
"**/test/**/*",
"core/**/*.spec.*",
],
executor: new CommandExecutor("npm run --silent generate:relay-auth", {
executor: new CommandExecutor("npm run --silent generate:relay:auth", {
runOnInit: true,
}),
},
@@ -95,7 +95,7 @@ const config: Config = {
"**/test/**/*",
"core/**/*.spec.*",
],
executor: new CommandExecutor("npm run --silent generate:relay-install", {
executor: new CommandExecutor("npm run --silent generate:relay:install", {
runOnInit: true,
}),
},
+1 -1
View File
@@ -71,7 +71,7 @@ export default function({
{ from: /^\/account/, to: "/account.html" },
{ from: /^\/admin/, to: "/admin.html" },
{ from: /^\/embed\/stream/, to: "/stream.html" },
{ from: /^\/embed\/auth/, to: "/auth.html" },
{ from: /^\/embed\/auth$/, to: "/auth.html" },
{ from: /^\/embed\/auth\/callback/, to: "/auth-callback.html" },
{ from: /^\/install/, to: "/install.html" },
],
+1614 -1044
View File
File diff suppressed because it is too large Load Diff
+50 -35
View File
@@ -1,6 +1,6 @@
{
"name": "@coralproject/talk",
"version": "5.0.0-beta.6",
"version": "5.2.0",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"sideEffects": [
@@ -20,27 +20,31 @@
],
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
"scripts": {
"build": "NODE_ENV=production npm-run-all generate --parallel lint:client build:client build:server",
"build:development": "NODE_ENV=development npm-run-all generate --parallel lint:client build:client build:server",
"build": "NODE_ENV=production npm-run-all generate-persist --parallel build:client build:server",
"build:development": "NODE_ENV=development npm-run-all generate --parallel build:client build:server",
"build:client": "ts-node --transpile-only ./scripts/build.ts",
"build:server": "gulp server",
"migration:create": "ts-node --transpile-only ./scripts/migration/create.ts",
"doctoc": "doctoc --title='## Table of Contents' --github README.md",
"generate": "npm-run-all --parallel generate:*",
"generate": "npm-run-all generate:css-types generate:schema generate:relay",
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
"generate:css-types": "tcm src/core/client/",
"generate:relay-stream": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
"generate:relay-account": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/account --schema tenant",
"generate:relay-auth": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
"generate:relay-install": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
"generate:relay-admin": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
"generate:relay": "npm-run-all --parallel generate:relay:*",
"generate:relay-persist": "npm-run-all --parallel 'generate:relay:* -- --persist'",
"generate:relay:stream": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
"generate:relay:account": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/account --schema tenant",
"generate:relay:auth": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
"generate:relay:install": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
"generate:relay:admin": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
"generate:schema": "node ./scripts/generateSchemaTypes.js",
"docz": "docz",
"start": "NODE_ENV=production node dist/index.js",
"start:development": "NODE_ENV=development CONCURRENCY=${CONCURRENCY:-1} TS_NODE_PROJECT=./src/tsconfig.json ts-node-dev --inspect --transpile-only --no-notify -r tsconfig-paths/register ./src/index.ts",
"start:webpackDevServer": "ts-node --transpile-only ./scripts/start.ts",
"lint": "npm-run-all --parallel lint:* tscheck:*",
"lint:server": "tslint --project ./src/tsconfig.json",
"lint:client": "tslint --project ./src/core/client/tsconfig.json",
"lint:scripts": "tslint --project ./tsconfig.json",
"lint:server": "eslint 'src/**/*.{js,ts,tsx}' --ignore-pattern 'src/core/client/**'",
"lint:client": "eslint 'src/core/client/**/*.{js,ts,tsx}'",
"lint:scripts": "eslint 'scripts/**/*.{js,ts,tsx}'",
"lint:graphql": "graphql-schema-linter src/core/server/graph/tenant/schema/schema.graphql",
"lint-fix": "npm run lint:server -- --fix && npm run lint:client -- --fix && npm run lint:scripts -- --fix",
"test": "node scripts/test.js --env=jsdom",
@@ -53,22 +57,21 @@
"license": "Apache-2.0",
"dependencies": {
"@coralproject/bunyan-prettystream": "^0.1.4",
"@types/archiver": "^3.0.0",
"akismet-api": "^4.2.0",
"apollo-server-express": "^2.1.0",
"apollo-server-express": "^2.8.1",
"archiver": "^3.0.3",
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"bull": "^3.8.1",
"bunyan": "^1.8.12",
"cheerio": "^1.0.0-rc.2",
"common-tags": "^1.8.0",
"consolidate": "0.14.0",
"content-security-policy-builder": "^2.0.0",
"convict": "^4.3.1",
"cookie": "^0.4.0",
"cookie-parser": "^1.4.4",
"cors": "^2.8.4",
"cron": "^1.7.1",
"csv-stringify": "^5.3.0",
"dataloader": "^1.4.0",
"dompurify": "^1.0.8",
@@ -77,11 +80,8 @@
"env-rewrite": "^1.0.2",
"express": "^4.16.3",
"express-static-gzip": "^0.3.2",
"farce": "^0.2.6",
"fluent": "^0.10.0",
"fluent-dom": "^0.4.1",
"found": "^0.4.0-alpha.17",
"found-relay": "^0.4.0-alpha.8",
"fs-extra": "^6.0.1",
"graphql": "^0.13.2",
"graphql-config": "^2.0.1",
@@ -99,9 +99,10 @@
"jsonwebtoken": "^8.3.0",
"juice": "^5.2.0",
"jwks-rsa": "^1.3.0",
"linkify-it": "^2.1.0",
"keymaster": "^1.6.2",
"linkifyjs": "^2.1.8",
"lodash": "^4.17.10",
"lodash": "^4.17.15",
"lru-cache": "^5.1.1",
"luxon": "^1.12.0",
"metascraper-author": "^3.11.8",
"metascraper-date": "^3.11.4",
@@ -126,15 +127,14 @@
"prom-client": "^11.3.0",
"proxy-agent": "^3.1.0",
"querystringify": "^2.1.0",
"react-relay-network-modern": "^3.0.4",
"react-helmet": "^5.2.1",
"source-map-support": "^0.5.12",
"stack-utils": "^1.0.2",
"striptags": "^3.1.1",
"throng": "^4.0.0",
"tlds": "^1.203.1",
"tsscmp": "^1.0.6",
"url-regex": "^5.0.0",
"uuid": "^3.3.2",
"uuid": "^3.3.3",
"verror": "^1.10.0"
},
"devDependencies": {
@@ -149,6 +149,7 @@
"@coralproject/rte": "^0.10.15",
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
"@types/agent-base": "^4.2.0",
"@types/archiver": "^3.0.0",
"@types/basic-auth": "^1.1.2",
"@types/bcryptjs": "^2.4.1",
"@types/bull": "^3.5.12",
@@ -164,6 +165,7 @@
"@types/cookie": "^0.3.3",
"@types/cookie-parser": "^1.4.1",
"@types/cors": "^2.8.4",
"@types/cron": "^1.7.1",
"@types/cross-spawn": "^6.0.0",
"@types/dotenv": "^4.0.3",
"@types/enzyme": "^3.1.15",
@@ -181,9 +183,9 @@
"@types/joi": "^13.0.8",
"@types/jsdom": "^12.2.3",
"@types/jsonwebtoken": "^7.2.7",
"@types/linkify-it": "^2.1.0",
"@types/linkifyjs": "^2.1.1",
"@types/lodash": "^4.14.118",
"@types/lru-cache": "^5.1.0",
"@types/luxon": "^1.12.0",
"@types/marked": "^0.6.0",
"@types/mini-css-extract-plugin": "^0.2.0",
@@ -193,6 +195,7 @@
"@types/node-fetch": "^2.3.3",
"@types/nodemailer": "^4.6.2",
"@types/nunjucks": "^3.1.1",
"@types/object-diff": "0.0.0",
"@types/on-finished": "^2.3.1",
"@types/passport": "^0.4.6",
"@types/passport-facebook": "^2.1.8",
@@ -204,6 +207,7 @@
"@types/react": "^16.8.15",
"@types/react-copy-to-clipboard": "^4.2.6",
"@types/react-dom": "^16.8.4",
"@types/react-helmet": "^5.0.10",
"@types/react-relay": "^1.3.14",
"@types/react-responsive": "^3.0.1",
"@types/react-test-renderer": "^16.8.1",
@@ -217,7 +221,6 @@
"@types/source-map-support": "^0.5.0",
"@types/stack-utils": "^1.0.1",
"@types/throng": "^4.0.2",
"@types/tlds": "^1.199.0",
"@types/uuid": "^3.4.4",
"@types/verror": "^1.10.3",
"@types/vinyl": "^2.0.2",
@@ -226,6 +229,9 @@
"@types/webpack-bundle-analyzer": "^2.13.1",
"@types/webpack-dev-server": "^3.1.5",
"@types/ws": "^5.1.2",
"@typescript-eslint/eslint-plugin": "2.3.3",
"@typescript-eslint/eslint-plugin-tslint": "2.3.3",
"@typescript-eslint/parser": "2.3.3",
"acorn": "^6.1.1",
"ansi-styles": "^3.2.0",
"autoprefixer": "^9.5.1",
@@ -245,6 +251,7 @@
"classnames": "^2.2.6",
"commander": "^2.20.0",
"comment-json": "^1.1.3",
"common-tags": "^1.8.0",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^5.0.3",
"cross-spawn": "^6.0.5",
@@ -256,13 +263,22 @@
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.12.1",
"enzyme-to-json": "^3.3.5",
"eslint": "^6.5.1",
"eslint-config-prettier": "^6.3.0",
"eslint-plugin-jsdoc": "^15.9.7",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.15.1",
"eventemitter2": "^5.0.1",
"farce": "^0.2.6",
"final-form": "4.11.0",
"flat": "^4.1.0",
"fluent-intl-polyfill": "^0.1.0",
"fluent-langneg": "^0.1.1",
"fluent-react": "^0.8.4",
"fork-ts-checker-webpack-plugin": "^1.3.0",
"fork-ts-checker-webpack-plugin": "^1.5.0",
"found": "^0.4.0-alpha.17",
"found-relay": "^0.4.0-alpha.8",
"graphql-schema-linter": "^0.2.0",
"graphql-schema-typescript": "^1.2.9",
"gulp": "^4.0.2",
@@ -279,10 +295,11 @@
"jest-mock-console": "^1.0.0",
"lint-staged": "^8.1.6",
"loader-utils": "^1.2.3",
"lodash-es": "^4.17.11",
"marked": "^0.6.0",
"lodash-es": "^4.17.14",
"marked": "^0.7.0",
"material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.6.0",
"object-diff": "0.0.4",
"postcss-advanced-variables": "^3.0.0",
"postcss-css-variables": "^0.11.0",
"postcss-flexbugs-fixes": "^4.1.0",
@@ -294,7 +311,7 @@
"postcss-nested": "^4.1.1",
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^6.5.0",
"prettier": "^1.17.0",
"prettier": "^1.18.2",
"prop-types": "^15.6.2",
"pstree.remy": "^1.1.6",
"pym.js": "^1.3.2",
@@ -307,6 +324,7 @@
"react-final-form": "4.0.2",
"react-popper": "^1.3.2",
"react-relay": "^4.0.0",
"react-relay-network-modern": "^4.0.4",
"react-responsive": "^7.0.0",
"react-test-renderer": "^16.9.0-alpha.0",
"react-timeago": "^4.1.9",
@@ -336,10 +354,7 @@
"ts-node-dev": "^1.0.0-pre.37",
"tsconfig-paths": "^3.8.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"tslint": "^5.16.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"tslint-react": "^4.0.0",
"tslint": "^5.20.0",
"typed-css-modules": "^0.4.2",
"typeface-manuale": "^0.0.71",
"typeface-source-sans-pro": "^0.0.54",
@@ -355,12 +370,12 @@
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
"pre-commit": "FAST_LINT=true lint-staged"
}
},
"lint-staged": {
"*.{j,t}s{,x}": [
"tslint"
"eslint"
],
"src/core/server/graph/tenant/schema/schema.graphql": [
"graphql-schema-linter"
+2 -3
View File
@@ -12,7 +12,7 @@
*/
"use strict";
// tslint:disable:no-console
/* eslint-disable no-console */
const address = require("address");
const url = require("url");
@@ -21,7 +21,6 @@ const clearConsole = require("react-dev-utils/clearConsole");
const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
const typescriptFormatter = require("react-dev-utils/typescriptFormatter");
const forkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpackPlugin");
const Stats = require("webpack/lib/Stats");
// (cvle): Changed to false as we are sharing the tty with other processes.
// const isInteractive = process.stdout.isTTY && false;
@@ -142,7 +141,7 @@ function createCompiler({
});
let isFirstCompile = true;
let tsMessagesPromises = [];
const tsMessagesPromises = [];
if (useTypeScript) {
compiler.compilers.forEach(singleCompiler => {
+1 -1
View File
@@ -17,7 +17,7 @@ import paths from "../config/paths";
import config from "../src/core/build/config";
import createWebpackConfig from "../src/core/build/createWebpackConfig";
// tslint:disable: no-console
/* eslint-disable no-console */
process.env.WEBPACK = "true";
process.env.NODE_ENV = process.env.NODE_ENV || "production";
+31 -8
View File
@@ -2,7 +2,7 @@
import program from "commander";
import spawn from "cross-spawn";
import fs from "fs";
import fs from "fs-extra";
import path from "path";
const config = JSON.parse(
@@ -14,33 +14,34 @@ program
.usage("--src ./src/core/client/stream --schema tenant")
.option("--src <folder>", "Find gql recursively in this folder")
.option("--schema <schema>", "Identifier of schema")
.option("--persist", "Use persisted queries")
.description("Compile relay gql data")
.parse(process.argv);
if (!program.schema) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error("Schema identifier not provided");
process.exit(1);
}
if (!program.src) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error("Src not provided");
process.exit(1);
}
if (!config.projects) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error("Missing projects key in .graphqconfig");
process.exit(1);
}
if (!config.projects[program.schema]) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error(`Project ${program.schema} not found in .graphqconfig`);
process.exit(1);
}
if (!config.projects[program.schema].schemaPath) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error(
`SchemaPath for project ${program.schema} not found in .graphqconfig`
);
@@ -57,8 +58,30 @@ const args = [
`${program.src}/__generated__`,
"--schema",
config.projects[program.schema].schemaPath,
// "--persist-output",
// `${program.src}/persisted-queries.json`,
];
// Set the persisted query path.
const persist = program.persist
? `${program.src}/persisted-queries.json`
: null;
if (persist) {
args.push("--persist-output", persist);
}
spawn.sync("relay-compiler", args, { stdio: "inherit" });
if (persist) {
if (fs.existsSync(persist)) {
// Create the new filename.
const name = path.basename(program.src);
const generated = "./src/core/server/graph/common/persisted/__generated__";
// Create the generated directory if it doesn't exist.
fs.ensureDirSync(generated);
// Copy the file over to the destination directory.
fs.copySync(persist, `${generated}/${name}.json`, {
overwrite: true,
});
}
}
+2 -3
View File
@@ -1,4 +1,3 @@
const { Linter, Configuration } = require("tslint");
const { generateTSTypesAsString } = require("graphql-schema-typescript");
const { getGraphQLConfig } = require("graphql-config");
const path = require("path");
@@ -69,12 +68,12 @@ if (require.main === module) {
main()
.then(files => {
for (const { fileName } of files) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.log(`Generated ${fileName}`);
}
})
.catch(err => {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error(err);
});
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env ts-node
/**
* This script can be invoked via:
*
* npm run migration:create <migration name>
*
* To create new database migrations.
*/
/* eslint-disable no-console */
import fs from "fs-extra";
import lodash from "lodash";
import path from "path";
const templateFilePath = path.resolve(
path.join(
__dirname,
"../../src/core/server/services/migrate/migration_sample.ts"
)
);
const argv = process.argv.slice(2);
if (argv.length !== 1) {
console.error("usage: npm run migration:create <migration name>");
process.exit(1);
}
// Get the name of the new migration.
const name = lodash.snakeCase(argv[0]);
// Get the version of the new migration.
const version = Date.now();
// Get the filePath of the new migration.
const filePath = path.resolve(
path.join(
__dirname,
`../../src/core/server/services/migrate/migrations/${version}_${name}.ts`
)
);
if (fs.existsSync(filePath)) {
console.error(`migration already exists at: ${filePath}`);
process.exit(1);
}
// Write the template out to the file.
fs.copyFileSync(templateFilePath, filePath);
console.log(`created new migration at: ${filePath}`);
+1 -1
View File
@@ -14,7 +14,7 @@ import createDevServerConfig from "../config/webpackDevServer.config";
import config from "../src/core/build/config";
import createWebpackConfig from "../src/core/build/createWebpackConfig";
// tslint:disable: no-console
/* eslint-disable no-console */
// Enforce environment to be development.
config.validate().set("env", "development");
+1 -1
View File
@@ -24,7 +24,7 @@ process.on("unhandledRejection", err => {
const paths = require("../config/paths.ts").default;
const jest = require("jest");
let argv = process.argv.slice(2);
const argv = process.argv.slice(2);
argv.push("--config", paths.appJestConfig);
// Watch unless on CI or in coverage mode
+3 -3
View File
@@ -21,8 +21,8 @@ export default class CommandExecutor implements Executor {
private args?: ReadonlyArray<string>;
private spawnMultiple: boolean;
private runOnInit: boolean;
private isRunning: boolean = false;
private shouldRespawn: boolean = false;
private isRunning = false;
private shouldRespawn = false;
private spawnProcessDebounced?: (() => void) & Cancelable;
constructor(cmd: string, opts: CommandExecutorOptions = {}) {
@@ -67,7 +67,7 @@ export default class CommandExecutor implements Executor {
child.on("close", (code: number) => {
this.isRunning = false;
if (code !== 0 && code !== null) {
// tslint:disable-next-line: no-console
// eslint-disable-next-line no-console
console.log(chalk.red(`Command exited with ${code}`));
}
if (this.shouldRespawn) {
+4 -4
View File
@@ -17,8 +17,8 @@ export default class LongRunningExecutor implements Executor {
private cmd: string;
private args?: ReadonlyArray<string>;
private process: ChildProcess | null = null;
private isRunning: boolean = false;
private shouldRestart: boolean = false;
private isRunning = false;
private shouldRestart = false;
private restartDebounced: (() => void) & Cancelable;
constructor(cmd: string, opts: LongRunningExecutorOptions = {}) {
@@ -37,11 +37,11 @@ export default class LongRunningExecutor implements Executor {
shell: !this.args,
});
this.process!.on("exit", (code: number) => {
this.process.on("exit", (code: number) => {
this.isRunning = false;
if (code !== 0 && code !== null) {
// tslint:disable-next-line: no-console
// eslint-disable-next-line no-console
console.log(chalk.red(`Command exited with ${code}`));
return;
}
+2 -2
View File
@@ -18,7 +18,7 @@ function canUseWatchman(): boolean {
try {
execSync("watchman --version", { stdio: ["ignore"] });
return true;
// tslint:disable-next-line:no-empty
// eslint-disable-next-line no-empty
} catch (e) {}
return false;
}
@@ -34,7 +34,7 @@ export default class SaneWatcher implements Watcher {
// Autodetect watchman.
if (this.watchman === undefined && canUseWatchman()) {
this.watchman = true;
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.log(chalk.grey(`Watchman detected`));
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ async function run(
throw new Error("Config file not specified");
}
// tslint:disable-next-line:no-var-requires
// eslint-disable-next-line @typescript-eslint/no-var-requires
let config: any = require(path.resolve(configFile));
if (config.__esModule) {
config = config.default;
@@ -31,7 +31,7 @@ const cmd = program
.parse(process.argv);
run(cmd.args, cmd.opts()).catch(err => {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});
+4 -4
View File
@@ -21,7 +21,7 @@ async function beginWatch(
await executor.onInit();
}
for await (const filePath of watcher.watch(rootDir, paths, { ignore })) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.log(chalk.cyanBright(`Execute "${key}"`));
executor.execute(filePath);
}
@@ -72,7 +72,7 @@ function filterOnly(
}
return pickBy(watchers, (value, key) => {
if (resolved.indexOf(key) === -1) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.log(chalk.grey(`Disabled watcher "${key}"`));
return false;
}
@@ -98,11 +98,11 @@ export default async function watch(config: Config, options: Options = {}) {
}
for (const key of Object.keys(watchersConfigs)) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.log(chalk.cyanBright(`Start watcher "${key}"`));
const watcherConfig = watchersConfigs[key];
beginWatch(watcher, key, watcherConfig, rootDir).catch(err => {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});
+1 -1
View File
@@ -7,7 +7,7 @@
"use strict";
// tslint:disable:no-console
/* eslint-disable */
// This alternative WebpackDevServer combines the functionality of:
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
+8
View File
@@ -1,5 +1,7 @@
import convict from "convict";
import { LOCALES } from "../common/helpers/i18n/locales";
const config = convict({
env: {
doc: "The application environment.",
@@ -21,6 +23,12 @@ const config = convict({
env: "DEV_PORT",
arg: "dev-port",
},
defaultLocale: {
doc: "Specify the default locale to use",
format: LOCALES,
default: "en-US",
env: "LOCALE",
},
generateReport: {
doc: "Generate a report using webpack-bundle-analyzer",
format: Boolean,
+50 -11
View File
@@ -92,23 +92,23 @@ export default function createWebpackConfig(
const localesOptions = {
pathToLocales: paths.appLocales,
// Default locale if non could be negotiated.
defaultLocale: "en-US",
// Default locale if none was specified.
defaultLocale: config.get("defaultLocale"),
// Fallback locale if a translation was not found.
// If not set, will use the text that is already
// in the code base.
fallbackLocale: "en-US",
fallbackLocale: config.get("defaultLocale"),
// Common fluent files are always included in the locale bundles.
commonFiles: ["framework.ftl", "common.ftl", "ui.ftl"],
// Locales that come with the main bundle. Others are loaded on demand.
bundled: ["en-US"],
bundled: [config.get("defaultLocale")],
// All available locales can be loadable on demand.
// To restrict available locales set:
// availableLocales: ["en-US"],
// availableLocales: [config.get("defaultLocale")],
};
const additionalPlugins = [
@@ -223,10 +223,10 @@ export default function createWebpackConfig(
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
filename: isProduction
? "assets/js/[name].[chunkhash:8].js"
? "assets/js/[name].[chunkhash].js"
: "assets/js/[name].js",
chunkFilename: isProduction
? "assets/js/[name].[chunkhash:8].chunk.js"
? "assets/js/[name].[chunkhash].chunk.js"
: "assets/js/[name].chunk.js",
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath,
@@ -357,7 +357,7 @@ export default function createWebpackConfig(
options: {
limit: 10000,
name: isProduction
? "assets/media/[name].[hash:8].[ext]"
? "assets/media/[name].[hash].[ext]"
: "assets/media/[name].[ext]",
},
},
@@ -370,7 +370,7 @@ export default function createWebpackConfig(
options: {
modules: true,
importLoaders: 2,
localIdentName: "[name]-[local]-[hash:base64:5]",
localIdentName: "[name]-[local]-[contenthash]",
sourceMap: !disableSourcemaps,
},
},
@@ -530,9 +530,9 @@ export default function createWebpackConfig(
// TODO: (cvle) this should work in build too but for some reasons it terminates the build afterwards.
// Preventing from running post build steps.
...ifWatch(
// We run tslint in a separate process to have a quicker build.
// We run eslint in a separate process to have a quicker build.
new ForkTsCheckerWebpackPlugin({
tslint: true,
eslint: true,
typescript: require.resolve("typescript"),
async: true,
// TODO: (cvle) For some reason if incremental build is turned on it does not find lint errors during initial build.
@@ -752,5 +752,44 @@ export default function createWebpackConfig(
),
]),
},
/* Webpack config for count */
{
...baseConfig,
optimization: {
...baseConfig.optimization,
// Ensure that we never split the count into chunks.
splitChunks: {
chunks: "async",
},
// We can turn on sideEffects here as we don't use
// css here and don't run into: https://github.com/webpack/webpack/issues/7094
sideEffects: true,
},
entry: [paths.appCountIndex],
output: {
...baseConfig.output,
// don't hash the count, cache-busting must be completed by the requester
// as this lives in a static template on the embed site.
filename: "assets/js/count.js",
},
plugins: filterPlugins([
...baseConfig.plugins!,
...ifWatch(
// Generates an `embed.html` file with the <script> injected.
new HtmlWebpackPlugin({
filename: "count.html",
template: paths.appCountHTML,
inject: "body",
})
),
...ifBuild(
new WebpackAssetsManifest({
output: "count-asset-manifest.json",
entrypoints: true,
integrity: true,
})
),
]),
},
];
}
-1
View File
@@ -59,7 +59,6 @@ function generateTarget(target, context) {
defaultLocale,
fallbackLocale,
pathToLocales,
resourcePath,
locales,
bundled,
} = context;
+3
View File
@@ -34,6 +34,9 @@ export default {
appAuthCallbackHTML: resolveSrc("core/client/auth-callback/index.html"),
appAuthCallbackIndex: resolveSrc("core/client/auth-callback/index.ts"),
appCountHTML: resolveSrc("core/client/count/index.html"),
appCountIndex: resolveSrc("core/client/count/index.ts"),
appInstallHTML: resolveSrc("core/client/install/index.html"),
appInstallLocalesTemplate: resolveSrc("core/client/install/locales.ts"),
appInstallIndex: resolveSrc("core/client/install/index.tsx"),
-6
View File
@@ -23,12 +23,6 @@ const flatKebabVariables = mapKeys(
(_, k) => kebabCase(k)
);
// These are the default css standard variables.
const cssVariables = pickBy(
flatKebabVariables,
(v, k) => !k.startsWith("breakpoints-")
);
// These are sass style variables used in media queries.
const mediaQueryVariables = mapValues(
pickBy(flatKebabVariables, (v, k) => k.startsWith("breakpoints-")),
+1 -1
View File
@@ -1,2 +1,2 @@
__webpack_public_path__ =
JSON.parse(document.getElementById("config").innerText) || "/";
JSON.parse(document.getElementById("config").innerText).staticURI || "/";
+12 -11
View File
@@ -1,10 +1,10 @@
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
import { BrowserProtocol, queryMiddleware } from "farce";
import { createFarceRouter, ElementsRenderer } from "found";
import { Resolver } from "found-relay";
import React, { FunctionComponent } from "react";
import { CoralContextConsumer } from "coral-framework/lib/bootstrap/CoralContext";
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
import routeConfig from "../routeConfig";
import NotFound from "../routes/NotFound";
@@ -16,16 +16,17 @@ const Router = createFarceRouter({
historyProtocol: new BrowserProtocol(),
historyMiddlewares: [queryMiddleware],
routeConfig,
renderReady: ({ elements }) => (
<>
<ElementsRenderer elements={elements} />
{// this enables router transition control when writing tests.
process.env.NODE_ENV === "test" && <TransitionControl />}
</>
),
renderError: ({ error }) => (
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
),
renderReady: function FarceRouterReady({ elements }) {
return (
<>
<ElementsRenderer elements={elements} />
{process.env.NODE_ENV === "test" && <TransitionControl />}
</>
);
},
renderError: function FarceRouterError({ error }) {
return <div>{error.status === 404 ? <NotFound /> : "Error"}</div>;
},
});
const EntryContainer: FunctionComponent = () => (
+2 -2
View File
@@ -1,7 +1,8 @@
import { createManaged } from "coral-framework/lib/bootstrap";
import React, { FunctionComponent } from "react";
import ReactDOM from "react-dom";
import { createManaged } from "coral-framework/lib/bootstrap";
import App from "./App";
import { initLocalState } from "./local";
import localesData from "./locales";
@@ -13,7 +14,6 @@ async function main() {
const ManagedCoralContextProvider = await createManaged({
initLocalState,
localesData,
userLocales: navigator.languages,
});
const Index: FunctionComponent = () => (
+1
View File
@@ -6,4 +6,5 @@
*/
import { LocalesData } from "coral-framework/lib/i18n";
export default {} as LocalesData;
+4
View File
@@ -3,6 +3,7 @@ import React from "react";
import DownloadRoute from "./routes/download/Download";
import ConfirmRoute from "./routes/email/Confirm";
import UnsubscribeRoute from "./routes/notifications/Unsubscribe";
import ResetRoute from "./routes/password/Reset";
export default makeRouteConfig(
@@ -14,5 +15,8 @@ export default makeRouteConfig(
<Route path="confirm" {...ConfirmRoute.routeConfig} />
</Route>
<Route path="download" {...DownloadRoute.routeConfig} />
<Route path="notifications">
<Route path="unsubscribe" {...UnsubscribeRoute.routeConfig} />
</Route>
</Route>
);
+2 -1
View File
@@ -1,6 +1,7 @@
import { HorizontalGutter, Typography } from "coral-ui/components";
import React, { FunctionComponent } from "react";
import { HorizontalGutter, Typography } from "coral-ui/components";
const NotFound: FunctionComponent = () => (
<HorizontalGutter>
<Typography variant="heading3">Not Found</Typography>
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import React, { FunctionComponent, useCallback, useState } from "react";
import { Button, HorizontalGutter } from "coral-ui/components";
@@ -10,6 +10,11 @@ interface Props {
}
const DownloadForm: FunctionComponent<Props> = ({ token }) => {
const [submitted, setSubmitted] = useState(false);
const onClick = useCallback(() => {
setSubmitted(true);
}, [setSubmitted]);
return (
<HorizontalGutter size="double">
<form
@@ -23,7 +28,8 @@ const DownloadForm: FunctionComponent<Props> = ({ token }) => {
type="submit"
variant="filled"
color="primary"
fullWidth={false}
disabled={submitted}
onClick={onClick}
className={styles.downloadButton}
>
Download My Comment History
@@ -17,7 +17,7 @@ import styles from "./DownloadRoute.css";
const fetcher = createFetch(
"downloadToken",
async (environment: Environment, variables: { token: string }, { rest }) =>
await rest.fetch<void>("/account/downloadcheck", {
await rest.fetch<void>("/account/download", {
method: "GET",
token: variables.token,
})
@@ -65,9 +65,11 @@ const DownloadRoute: FunctionComponent<Props> = ({ token }) => {
};
const enhanced = withRouteConfig<Props>({
render: ({ match, Component }) => (
<Component token={parseHashQuery(match.location.hash).downloadToken} />
),
render: function DownloadRouteRender({ match, Component }) {
return (
<Component token={parseHashQuery(match.location.hash).downloadToken} />
);
},
})(DownloadRoute);
export default enhanced;
@@ -1,7 +1,7 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { CallOut, Flex, Icon } from "coral-ui/components";
import { Localized } from "fluent-react/compat";
import styles from "./Sorry.css";
@@ -69,9 +69,11 @@ const ConfirmRoute: React.FunctionComponent<Props> = ({ token }) => {
};
const enhanced = withRouteConfig<Props>({
render: ({ match, Component }) => (
<Component token={parseHashQuery(match.location.hash).confirmToken} />
),
render: function ConfirmRouteRender({ match, Component }) {
return (
<Component token={parseHashQuery(match.location.hash).confirmToken} />
);
},
})(ConfirmRoute);
export default enhanced;
@@ -0,0 +1,32 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
interface Props {
reason: React.ReactNode;
}
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
return (
<HorizontalGutter size="double">
<Localized id="unsubscribe-oopsSorry">
<Typography variant="heading1">Oops Sorry!</Typography>
</Localized>
<CallOut color="error" fullWidth>
{reason ? (
reason
) : (
<Localized id="account-tokenNotFound">
<span data-testid="invalid-link">
The specified link is invalid, check to see if it was copied
correctly.
</span>
</Localized>
)}
</CallOut>
</HorizontalGutter>
);
};
export default Sorry;
@@ -0,0 +1,18 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { HorizontalGutter, Typography } from "coral-ui/components";
const Success: React.FunctionComponent = () => {
return (
<HorizontalGutter data-testid="success" size="double">
<Localized id="unsubscribe-successfullyUnsubscribed">
<Typography variant="heading1">
You are now unsubscribed from all notifications
</Typography>
</Localized>
</HorizontalGutter>
);
};
export default Success;
@@ -0,0 +1,76 @@
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { useCallback } from "react";
import { Form } from "react-final-form";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { useMutation } from "coral-framework/lib/relay";
import {
Button,
CallOut,
HorizontalGutter,
Typography,
} from "coral-ui/components";
import UnsubscribeNotificationsMutation from "./UnsubscribeNotificationsMutation";
interface Props {
token: string;
disabled?: boolean;
onSuccess: () => void;
}
const UnsubscribeForm: React.FunctionComponent<Props> = ({
onSuccess,
token,
}) => {
const unsubscribe = useMutation(UnsubscribeNotificationsMutation);
const onSubmit = useCallback(async () => {
try {
await unsubscribe({ token });
onSuccess();
} catch (error) {
if (error instanceof InvalidRequestError) {
return error.invalidArgs;
}
return { [FORM_ERROR]: error.message };
}
return;
}, [token]);
return (
<div data-testid="unsubscribe-form">
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form onSubmit={handleSubmit}>
<HorizontalGutter>
<Localized id="unsubscribe-clickToConfirm">
<Typography variant="heading1">
Click below to confirm that you want to unsubscribe from all
notifications.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Localized id="unsubscribe-confirm">
<Button
type="submit"
variant="filled"
color="primary"
disabled={submitting}
fullWidth
>
Confirm
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</div>
);
};
export default UnsubscribeForm;
@@ -0,0 +1,14 @@
import { Environment } from "relay-runtime";
import { createMutation } from "coral-framework/lib/relay";
const UnsubscribeNotificationsMutation = createMutation(
"unsubscribeNotifications",
async (environment: Environment, variables: { token: string }, { rest }) =>
await rest.fetch<void>("/account/notifications/unsubscribe", {
method: "DELETE",
token: variables.token,
})
);
export default UnsubscribeNotificationsMutation;
@@ -0,0 +1,16 @@
.container {
width: 100%;
text-align: center;
}
.root {
display: inline-block;
max-width: calc(70 * var(--mini-unit));
@media (min-width: $breakpoints-xs) {
max-width: calc(42 * var(--mini-unit));
}
text-align: left;
}
@@ -0,0 +1,79 @@
import React, { useCallback, useState } from "react";
import { Environment } from "relay-runtime";
import Loading from "coral-account/components/Loading";
import { useToken } from "coral-framework/hooks";
import { createFetch } from "coral-framework/lib/relay";
import { withRouteConfig } from "coral-framework/lib/router";
import { parseHashQuery } from "coral-framework/utils";
import Sorry from "./Sorry";
import Success from "./Success";
import UnsubscribeForm from "./UnsubscribeForm";
import styles from "./UnsubscribeRoute.css";
const fetcher = createFetch(
"unsubscribeToken",
async (environment: Environment, variables: { token: string }, { rest }) =>
await rest.fetch<void>("/account/notifications/unsubscribe", {
method: "GET",
token: variables.token,
})
);
interface Props {
token: string | undefined;
}
const UnsubscribeRoute: React.FunctionComponent<Props> = ({ token }) => {
const [finished, setFinished] = useState(false);
const onSuccess = useCallback(() => {
setFinished(true);
}, []);
const [state, error] = useToken(fetcher, token);
if (state === "UNCHECKED") {
return (
<div className={styles.container}>
<div className={styles.root}>
<Loading />
</div>
</div>
);
}
if (state !== "VALID" || error) {
return (
<div className={styles.container}>
<div className={styles.root}>
<Sorry reason={error} />
</div>
</div>
);
}
return !finished ? (
<div className={styles.container}>
<div className={styles.root}>
<UnsubscribeForm token={token!} onSuccess={onSuccess} />
</div>
</div>
) : (
<div className={styles.container}>
<div className={styles.root}>
<Success />
</div>
</div>
);
};
const enhanced = withRouteConfig<Props>({
render: function UnsubscribeRouteRender({ match, Component }) {
return (
<Component token={parseHashQuery(match.location.hash).unsubscribeToken} />
);
},
})(UnsubscribeRoute);
export default enhanced;
@@ -0,0 +1 @@
export { default, default as UnsubscribeRoute } from "./UnsubscribeRoute";
@@ -69,9 +69,9 @@ const ResetRoute: React.FunctionComponent<Props> = ({ token }) => {
};
const enhanced = withRouteConfig<Props>({
render: ({ match, Component }) => (
<Component token={parseHashQuery(match.location.hash).resetToken} />
),
render: function ResetRouteRender({ match, Component }) {
return <Component token={parseHashQuery(match.location.hash).resetToken} />;
},
})(ResetRoute);
export default enhanced;
@@ -42,6 +42,8 @@ exports[`renders form 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
data-color="primary"
data-variant="filled"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
@@ -52,7 +52,7 @@ Make sure it is unique and be sure to keep it secure.
Password
</label>
<p
className="Box-root Typography-root Typography-detail Typography-colorTextSecondary"
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -81,22 +81,25 @@ Make sure it is unique and be sure to keep it secure.
<div
className="PasswordField-icon"
onClick={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
title="Hide password"
>
<span
<i
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</i>
</div>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
data-color="primary"
data-variant="filled"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
@@ -0,0 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders form 1`] = `
<div
data-testid="main-layout"
>
<div
className="MainLayout-bar"
/>
<div
className="MainLayout-centered"
>
<div
className="UnsubscribeRoute-container"
>
<div
className="UnsubscribeRoute-root"
>
<div
data-testid="unsubscribe-form"
>
<form
onSubmit={[Function]}
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Click below to confirm that you want to unsubscribe from all notifications.
</h1>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
data-color="primary"
data-variant="filled"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Confirm
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders missing confirm token 1`] = `
<div
data-testid="main-layout"
>
<div
className="MainLayout-bar"
/>
<div
className="MainLayout-centered"
>
<div
className="UnsubscribeRoute-container"
>
<div
className="UnsubscribeRoute-root"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Oops Sorry!
</h1>
<div
className="CallOut-root CallOut-colorError CallOut-fullWidth"
>
<div>
<span
data-testid="invalid-link"
>
The specified link is invalid, check to see if it was copied correctly.
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -1,5 +1,7 @@
import sinon from "sinon";
import { ERROR_CODES } from "coral-common/errors";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { GQLResolver } from "coral-framework/schema";
import {
act,
@@ -10,8 +12,6 @@ import {
within,
} from "coral-framework/testHelpers";
import { ERROR_CODES } from "coral-common/errors";
import { InvalidRequestError } from "coral-framework/lib/errors";
import create from "./create";
const token = createAccessToken();
@@ -1,5 +1,7 @@
import sinon from "sinon";
import { ERROR_CODES } from "coral-common/errors";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { GQLResolver } from "coral-framework/schema";
import {
act,
@@ -10,8 +12,6 @@ import {
within,
} from "coral-framework/testHelpers";
import { ERROR_CODES } from "coral-common/errors";
import { InvalidRequestError } from "coral-framework/lib/errors";
import create from "./create";
const token = createAccessToken();
@@ -0,0 +1,134 @@
import sinon from "sinon";
import { ERROR_CODES } from "coral-common/errors";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { GQLResolver } from "coral-framework/schema";
import {
act,
createAccessToken,
CreateTestRendererParams,
replaceHistoryLocation,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "./create";
const token = createAccessToken();
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create();
return {
context,
testRenderer,
root: testRenderer.root,
};
}
it("renders missing confirm token", async () => {
replaceHistoryLocation("http://localhost/account/notifications/unsubscribe");
const { root } = await createTestRenderer();
await waitForElement(() => within(root).getByTestID("invalid-link"));
expect(within(root).toJSON()).toMatchSnapshot();
});
it("renders form", async () => {
replaceHistoryLocation(
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
);
const { root, context } = await createTestRenderer();
const restMock = sinon.mock(context.rest);
restMock
.expects("fetch")
.withArgs("/account/notifications/unsubscribe", {
method: "GET",
token,
})
.once();
await act(async () => {
await waitForElement(() => within(root).getByTestID("unsubscribe-form"));
});
expect(within(root).toJSON()).toMatchSnapshot();
restMock.verify();
});
it("renders error from server", async () => {
replaceHistoryLocation(
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
);
const codes = [
ERROR_CODES.RATE_LIMIT_EXCEEDED,
ERROR_CODES.INTEGRATION_DISABLED,
ERROR_CODES.USER_NOT_FOUND,
ERROR_CODES.TOKEN_INVALID,
];
for (const code of codes) {
const { root, context } = await createTestRenderer();
const restMock = sinon.mock(context.rest);
restMock
.expects("fetch")
.withArgs("/account/notifications/unsubscribe", {
method: "GET",
token,
})
.once()
.throwsException(
new InvalidRequestError({
code,
})
);
await act(async () => {
await waitForElement(() =>
within(root).getByText(code, {
exact: false,
})
);
});
restMock.verify();
}
});
it("submits form", async () => {
replaceHistoryLocation(
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
);
const { root, context } = await createTestRenderer();
const restMock = sinon.mock(context.rest);
restMock
.expects("fetch")
.withArgs("/account/notifications/unsubscribe", {
method: "GET",
token,
})
.once();
restMock
.expects("fetch")
.withArgs("/account/notifications/unsubscribe", {
method: "DELETE",
token,
})
.once();
await act(async () => {
await waitForElement(() => within(root).getByTestID("unsubscribe-form"));
});
const form = within(root).getByType("form");
// Submit valid form.
await act(async () => {
form.props.onSubmit();
await waitForElement(() => within(root).getByTestID("success"));
});
restMock.verify();
});
+12 -11
View File
@@ -1,10 +1,10 @@
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
import { BrowserProtocol, queryMiddleware } from "farce";
import { createFarceRouter, ElementsRenderer } from "found";
import { Resolver } from "found-relay";
import React, { FunctionComponent } from "react";
import { CoralContextConsumer } from "coral-framework/lib/bootstrap/CoralContext";
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
import routeConfig from "../routeConfig";
import NotFound from "../routes/NotFound";
@@ -13,16 +13,17 @@ const Router = createFarceRouter({
historyProtocol: new BrowserProtocol(),
historyMiddlewares: [queryMiddleware],
routeConfig,
renderReady: ({ elements }) => (
<>
<ElementsRenderer elements={elements} />
{// this enables router transition control when writing tests.
process.env.NODE_ENV === "test" && <TransitionControl />}
</>
),
renderError: ({ error }) => (
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
),
renderReady: function FarceRouterReady({ elements }) {
return (
<>
<ElementsRenderer elements={elements} />
{process.env.NODE_ENV === "test" && <TransitionControl />}
</>
);
},
renderError: function FarceRouterError({ error }) {
return <div>{error.status === 404 ? <NotFound /> : "Error"}</div>;
},
});
const App: FunctionComponent = () => (
@@ -1,6 +1,8 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { Typography } from "coral-ui/components";
import ApprovedIcon from "./ApprovedIcon";
import DecisionItem from "./DecisionItem";
import DotDivider from "./DotDivider";
@@ -9,8 +11,6 @@ import GoToCommentLink from "./GoToCommentLink";
import Info from "./Info";
import Timestamp from "./Timestamp";
import { Typography } from "coral-ui/components";
interface Props {
href: string;
username: string;
@@ -1,9 +1,10 @@
import React from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { withPaginationContainer } from "coral-framework/lib/relay";
import { DecisionHistoryContainer_viewer as ViewerData } from "coral-admin/__generated__/DecisionHistoryContainer_viewer.graphql";
import { DecisionHistoryContainerPaginationQueryVariables } from "coral-admin/__generated__/DecisionHistoryContainerPaginationQuery.graphql";
import { withPaginationContainer } from "coral-framework/lib/relay";
import DecisionHistory from "./DecisionHistory";
@@ -45,7 +46,7 @@ export class DecisionHistoryContainer extends React.Component<
error => {
this.setState({ disableLoadMore: false });
if (error) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error(error);
}
}
@@ -1,9 +1,10 @@
import React from "react";
import { graphql } from "react-relay";
import { DecisionHistoryItemContainer_action as ActionData } from "coral-admin/__generated__/DecisionHistoryItemContainer_action.graphql";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { DecisionHistoryItemContainer_action as ActionData } from "coral-admin/__generated__/DecisionHistoryItemContainer_action.graphql";
import ApprovedComment from "./ApprovedComment";
import RejectedComment from "./RejectedComment";
@@ -16,9 +17,7 @@ class DecisionHistoryItemContainer extends React.Component<
DecisionHistoryItemContainerProps
> {
public render() {
const href = `/admin/moderate/comment/${
this.props.action.revision.comment.id
}`;
const href = `/admin/moderate/comment/${this.props.action.revision.comment.id}`;
const username =
(this.props.action.revision.comment.author &&
this.props.action.revision.comment.author.username) ||
@@ -1,6 +1,7 @@
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
import React, { Component } from "react";
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
import { DecisionHistoryQuery as QueryTypes } from "coral-admin/__generated__/DecisionHistoryQuery.graphql";
import DecisionHistoryContainer from "./DecisionHistoryContainer";
@@ -1,6 +1,8 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { Typography } from "coral-ui/components";
import DecisionItem from "./DecisionItem";
import DotDivider from "./DotDivider";
import Footer from "./Footer";
@@ -9,8 +11,6 @@ import Info from "./Info";
import RejectedIcon from "./RejectedIcon";
import Timestamp from "./Timestamp";
import { Typography } from "coral-ui/components";
interface Props {
href: string;
username: string;
+1 -1
View File
@@ -1,7 +1,7 @@
import { removeFragmentRefs } from "coral-framework/testHelpers";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { removeFragmentRefs } from "coral-framework/testHelpers";
import { PropTypesOf } from "coral-framework/types";
import Main from "./Main";
+3 -3
View File
@@ -1,7 +1,7 @@
import React, { FunctionComponent } from "react";
import { PropTypesOf } from "coral-framework/types";
import { Logo } from "coral-ui/components";
import { LogoHorizontal } from "coral-ui/components";
import { AppBar, Begin, Divider, End } from "coral-ui/components/AppBar";
import { DecisionHistoryButton } from "./DecisionHistory";
@@ -22,8 +22,7 @@ const Main: FunctionComponent<Props> = ({ children, viewer }) => (
<AppBar gutterBegin gutterEnd>
<Begin itemGutter="double">
<div className={styles.logoContainer}>
<Logo />
<Version />
<LogoHorizontal />
</div>
<NavigationContainer viewer={viewer} />
</Begin>
@@ -34,6 +33,7 @@ const Main: FunctionComponent<Props> = ({ children, viewer }) => (
</End>
</AppBar>
{children}
<Version />
</div>
);
+2 -2
View File
@@ -1,10 +1,10 @@
import React from "react";
import { MainRouteQueryResponse } from "coral-admin/__generated__/MainRouteQuery.graphql";
import { graphql } from "coral-framework/lib/relay";
import { withRouteConfig } from "coral-framework/lib/router";
import { MainRouteQueryResponse } from "coral-admin/__generated__/MainRouteQuery.graphql";
import Main from "./Main";
interface Props {
@@ -1,6 +1,5 @@
import React from "react";
import { NavigationContainer_viewer as ViewerData } from "coral-admin/__generated__/NavigationContainer_viewer.graphql";
import { Ability, can } from "coral-admin/permissions";
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
import {
@@ -8,6 +7,8 @@ import {
withSignOutMutation,
} from "coral-framework/mutations";
import { NavigationContainer_viewer as ViewerData } from "coral-admin/__generated__/NavigationContainer_viewer.graphql";
import Navigation from "./Navigation";
interface Props {
@@ -1,12 +1,13 @@
import React from "react";
import { UserMenuContainer_viewer as ViewerData } from "coral-admin/__generated__/UserMenuContainer_viewer.graphql";
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
import {
SignOutMutation,
withSignOutMutation,
} from "coral-framework/mutations";
import { UserMenuContainer_viewer as ViewerData } from "coral-admin/__generated__/UserMenuContainer_viewer.graphql";
import UserMenu from "./UserMenu";
interface Props {
+3 -3
View File
@@ -1,6 +1,6 @@
.version {
position: absolute;
right: 0;
top: 23px;
font-size: calc(10rem / var(--rem-base));
color: var(--palette-text-secondary);
text-align: center;
padding-bottom: var(--spacing-3);
}
@@ -14,8 +14,7 @@ exports[`renders correctly 1`] = `
<div
className="Main-logoContainer"
>
<withPropsOnChange(Logo) />
<Version />
<withPropsOnChange(LogoHorizontal) />
</div>
<withContext(createMutationContainer(Relay(NavigationContainer)))
viewer={Object {}}
@@ -30,5 +29,6 @@ exports[`renders correctly 1`] = `
</withPropsOnChange(End)>
</withPropsOnChange(AppBar)>
child
<Version />
</div>
`;
+30
View File
@@ -0,0 +1,30 @@
import React, { FunctionComponent } from "react";
import { Helmet } from "react-helmet";
import favicon120 from "./assets/favicon120.png";
import favicon128 from "./assets/favicon128.png";
import favicon152 from "./assets/favicon152.png";
import favicon180 from "./assets/favicon180.png";
import favicon196 from "./assets/favicon196.png";
import favicon228 from "./assets/favicon228.png";
import favicon32 from "./assets/favicon32.png";
import favicon57 from "./assets/favicon57.png";
import favicon76 from "./assets/favicon76.png";
import favicon96 from "./assets/favicon96.png";
const Head: FunctionComponent = () => (
<Helmet>
<link rel="icon" href={favicon32} sizes="32x32" />
<link rel="icon" href={favicon57} sizes="57x57" />
<link rel="icon" href={favicon76} sizes="76x76" />
<link rel="icon" href={favicon96} sizes="96x96" />
<link rel="icon" href={favicon128} sizes="128x128" />
<link rel="icon" href={favicon228} sizes="228x228" />
<link rel="shortcut icon" sizes="196x196" href={favicon196} />
<link rel="apple-touch-icon" href={favicon120} sizes="120x120" />
<link rel="apple-touch-icon" href={favicon152} sizes="152x152" />
<link rel="apple-touch-icon" href={favicon180} sizes="180x180" />
</Helmet>
);
export default Head;
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+2 -2
View File
@@ -1,7 +1,7 @@
import React, { FunctionComponent } from "react";
import {
BrandIcon,
BrandMark,
BrandName,
Flex,
HorizontalGutter,
@@ -22,7 +22,7 @@ const AuthBox: FunctionComponent<Props> = ({ title, children }) => {
<HorizontalGutter className={styles.container} size="double">
<Flex justifyContent="center">
<div className={styles.brandIcon}>
<BrandIcon size="lg" />
<BrandMark size="lg" />
</div>
</Flex>
<div>
@@ -11,7 +11,7 @@ interface Props {
}
class AutoLoadMoresContainer extends React.Component<Props> {
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (nextProps.inView && !nextProps.disableLoadMore) {
nextProps.onLoadMore();
}
@@ -1,10 +1,10 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import ApproveButton from "./ApproveButton";
import { PropTypesOf } from "coral-framework/types";
import ApproveButton from "./ApproveButton";
it("renders correctly", () => {
const props: PropTypesOf<typeof ApproveButton> = {
invert: false,
@@ -0,0 +1,53 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import { BanCommentUserMutation as MutationTypes } from "coral-admin/__generated__/BanCommentUserMutation.graphql";
import {
commitMutationPromiseNormalized,
createMutation,
MutationInput,
} from "coral-framework/lib/relay";
const clientMutationId = 0;
const BanCommentUserMutation = createMutation(
"banUser",
(environment: Environment, input: MutationInput<MutationTypes>) => {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation: graphql`
mutation BanCommentUserMutation($input: BanUserInput!) {
banUser(input: $input) {
user {
id
status {
current
}
}
clientMutationId
}
}
`,
variables: {
input: {
...input,
clientMutationId: clientMutationId.toString(),
},
},
updater: store => {
const user = store.get(input.userID);
if (user) {
const comments = user.getLinkedRecords("comments");
if (comments) {
comments.forEach(comment => {
if (comment) {
comment.setLinkedRecord(user, "author");
}
});
}
}
},
});
}
);
export default BanCommentUserMutation;
@@ -0,0 +1,3 @@
.authorStatus {
padding-right: var(--spacing-2);
}
@@ -0,0 +1,45 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { CommentAuthorContainer_comment as CommentData } from "coral-admin/__generated__/CommentAuthorContainer_comment.graphql";
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
import { Tag } from "coral-ui/components";
import styles from "./CommentAuthorContainer.css";
interface Props {
comment: CommentData;
}
const CommentAuthorContainer: FunctionComponent<Props> = ({ comment }) => {
if (!comment.author || !comment.author.status.ban.active) {
return null;
}
return (
<>
<Localized id="commentAuthor-status-banned">
<div className={styles.authorStatus}>
<Tag color="error">BANNED</Tag>
</div>
</Localized>
</>
);
};
const enhanced = withFragmentContainer<Props>({
comment: graphql`
fragment CommentAuthorContainer_comment on Comment {
author {
id
username
status {
ban {
active
}
}
}
}
`,
})(CommentAuthorContainer);
export default enhanced;
@@ -1,10 +1,10 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import CommentContent from "./CommentContent";
import { PropTypesOf } from "coral-framework/types";
import CommentContent from "./CommentContent";
it("renders correctly", () => {
const props: PropTypesOf<typeof CommentContent> = {
suspectWords: ["worse"],
@@ -14,7 +14,7 @@ const purify = createPurify(window, false);
interface Props {
className?: string;
children: string;
children: string | React.ReactElement;
suspectWords: ReadonlyArray<string>;
bannedWords: ReadonlyArray<string>;
}
@@ -117,25 +117,29 @@ const CommentContent: FunctionComponent<Props> = ({
className,
children,
}) => {
// We create a Shadow DOM Tree with the HTML body content and
// use it as a parser.
const node = document.createElement("div");
node.innerHTML = purify.sanitize(children);
if (typeof children === "string") {
// We create a Shadow DOM Tree with the HTML body content and
// use it as a parser.
const node = document.createElement("div");
node.innerHTML = purify.sanitize(children);
if (suspectWords.length || bannedWords.length) {
// Then we traverse it recursively and manipulate it to highlight suspect words
// and banned words.
markHTMLNode(node, suspectWords, bannedWords);
if (suspectWords.length || bannedWords.length) {
// Then we traverse it recursively and manipulate it to highlight suspect words
// and banned words.
markHTMLNode(node, suspectWords, bannedWords);
}
// Finally we render the content of the Shadow DOM Tree
return (
<Typography
className={cn(className, styles.root)}
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
container="div"
/>
);
}
// Finally we render the content of the Shadow DOM Tree
return (
<Typography
className={cn(className, styles.root)}
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
container="div"
/>
);
return <>{children}</>;
};
export default CommentContent;
@@ -0,0 +1,68 @@
import React, { FunctionComponent } from "react";
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
import { HorizontalGutter, Timestamp } from "coral-ui/components";
import { CommentRevisionContainer_comment as CommentData } from "coral-admin/__generated__/CommentRevisionContainer_comment.graphql";
import { CommentRevisionContainer_settings as SettingsData } from "coral-admin/__generated__/CommentRevisionContainer_settings.graphql";
import CommentContent from "./CommentContent";
interface Props {
comment: CommentData;
settings: SettingsData;
}
const CommentRevisionContainer: FunctionComponent<Props> = ({
settings,
comment,
}) => {
return (
<HorizontalGutter>
{comment.revisionHistory
.concat()
.reverse()
.filter(c =>
comment && comment.revision && comment.revision.id
? comment.revision.id !== c.id
: true
)
.map(c => (
<div key={c.id}>
<Timestamp>{c.createdAt}</Timestamp>
<CommentContent
suspectWords={settings.wordList.suspect}
bannedWords={settings.wordList.banned}
>
{c.body ? c.body : ""}
</CommentContent>
</div>
))}
</HorizontalGutter>
);
};
const enhanced = withFragmentContainer<Props>({
comment: graphql`
fragment CommentRevisionContainer_comment on Comment {
revision {
id
}
revisionHistory {
id
body
createdAt
}
}
`,
settings: graphql`
fragment CommentRevisionContainer_settings on Settings {
wordList {
banned
suspect
}
}
`,
})(CommentRevisionContainer);
export default enhanced;
@@ -9,10 +9,12 @@ import styles from "./FeatureButton.css";
interface Props extends PropTypesOf<typeof BaseButton> {
featured: boolean;
enabled?: boolean;
}
const FeatureButton: FunctionComponent<Props> = ({
featured,
enabled = true,
className,
...rest
}) => (
@@ -21,6 +23,7 @@ const FeatureButton: FunctionComponent<Props> = ({
className={cn(className, styles.root, {
[styles.invert]: featured,
})}
disabled={!enabled}
>
{featured ? (
<Localized id="moderate-comment-featuredText">
@@ -1,7 +1,6 @@
import { graphql } from "react-relay";
import { ConnectionHandler, Environment } from "relay-runtime";
import { FeatureCommentMutation } from "coral-admin/__generated__/FeatureCommentMutation.graphql";
import { getQueueConnection } from "coral-admin/helpers";
import { CoralContext } from "coral-framework/lib/bootstrap";
import {
@@ -11,6 +10,8 @@ import {
} from "coral-framework/lib/relay";
import { GQLCOMMENT_STATUS, GQLTAG } from "coral-framework/schema";
import { FeatureCommentMutation } from "coral-admin/__generated__/FeatureCommentMutation.graphql";
let clientMutationId = 0;
const FeatureCommentMutation = createMutation(
@@ -2,14 +2,15 @@ import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";
import { FlagDetailsContainer_comment } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
import { FlagDetailsContainer_settings } from "coral-admin/__generated__/FlagDetailsContainer_settings.graphql";
import NotAvailable from "coral-admin/components/NotAvailable";
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { GQLCOMMENT_FLAG_REASON } from "coral-framework/schema";
import { HorizontalGutter } from "coral-ui/components";
import { FlagDetailsContainer_comment } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
import { FlagDetailsContainer_settings } from "coral-admin/__generated__/FlagDetailsContainer_settings.graphql";
import FlagDetailsCategory from "./FlagDetailsCategory";
import FlagDetailsEntry from "./FlagDetailsEntry";
import ToxicityLabel from "./ToxicityLabel";
@@ -21,13 +22,13 @@ interface Props {
}
const FlagDetailsContainer: FunctionComponent<Props> = ({
comment: {
revision: { metadata },
flags: { nodes },
},
comment,
onUsernameClick,
settings,
}) => {
const metadata = comment.revision ? comment.revision.metadata : null;
const nodes = comment.flags.nodes;
const offensive = nodes.filter(
({ reason }) => reason === GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE
);
@@ -37,7 +38,7 @@ const FlagDetailsContainer: FunctionComponent<Props> = ({
return (
<HorizontalGutter size="oneAndAHalf">
{metadata.perspective && (
{metadata && metadata.perspective && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-toxicityScore">
@@ -1,6 +1,7 @@
import React, { FunctionComponent } from "react";
import { BaseButton } from "coral-ui/components";
import styles from "./FlagDetailsEntry.css";
interface Props {
@@ -7,3 +7,7 @@
.username {
color: var(--palette-grey-main);
}
.usernameButton {
display: inline-flex;
}
@@ -16,5 +16,5 @@
}
.detailsDivider {
border: 1px solid var(--palette-grey-lightest);
border-color: var(--palette-grey-lightest);
}
@@ -13,6 +13,9 @@ it("renders all markers", () => {
const props: PropTypesOf<typeof MarkersContainerN> = {
comment: {
status: "PREMOD",
editing: {
edited: false,
},
revision: {
actionCounts: {
flag: {
@@ -25,6 +28,7 @@ it("renders all markers", () => {
COMMENT_DETECTED_SUSPECT_WORD: 1,
COMMENT_REPORTED_OFFENSIVE: 2,
COMMENT_REPORTED_SPAM: 3,
COMMENT_DETECTED_REPEAT_POST: 1,
},
},
},
@@ -53,6 +57,9 @@ it("renders some markers", () => {
const props: PropTypesOf<typeof MarkersContainerN> = {
comment: {
status: "PREMOD",
editing: {
edited: false,
},
revision: {
actionCounts: {
flag: {
@@ -65,6 +72,7 @@ it("renders some markers", () => {
COMMENT_DETECTED_SUSPECT_WORD: 0,
COMMENT_REPORTED_OFFENSIVE: 2,
COMMENT_REPORTED_SPAM: 0,
COMMENT_DETECTED_REPEAT_POST: 0,
},
},
},
@@ -2,12 +2,14 @@ import { Localized } from "fluent-react/compat";
import React from "react";
import { graphql } from "react-relay";
import { MarkersContainer_comment } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
import { MarkersContainer_settings } from "coral-admin/__generated__/MarkersContainer_settings.graphql";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { Marker, MarkerCount } from "coral-ui/components";
import FlagDetailsContainer from "./FlagDetailsContainer";
import { MarkersContainer_comment } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
import { MarkersContainer_settings } from "coral-admin/__generated__/MarkersContainer_settings.graphql";
import Markers from "./Markers";
import ModerateCardDetailsContainer from "./ModerateCardDetailsContainer";
interface MarkersContainerProps {
comment: MarkersContainer_comment;
@@ -16,11 +18,11 @@ interface MarkersContainerProps {
}
function hasDetails(c: MarkersContainer_comment) {
return (
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
0 || c.revision.metadata.perspective
);
return c.revision
? c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
0 || c.revision.metadata.perspective
: false;
}
let keyCounter = 0;
@@ -30,77 +32,93 @@ const markers: Array<
c =>
(c.status === "PREMOD" && (
<Localized id="moderate-marker-preMod" key={keyCounter++}>
<Marker color="primary">Pre-Mod</Marker>
<Marker color="primary">Pre-mod</Marker>
</Localized>
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
<Localized id="moderate-marker-link" key={keyCounter++}>
<Marker color="primary">Link</Marker>
</Localized>
)) ||
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
<Localized id="moderate-marker-link" key={keyCounter++}>
<Marker color="primary">Link</Marker>
</Localized>
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
<Marker color="error">Banned Word</Marker>
</Localized>
)) ||
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
<Marker color="error">Banned word</Marker>
</Localized>
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
<Marker color="error" variant="filled">
Suspect Word
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
<Marker color="error" variant="filled">
Suspect word
</Marker>
</Localized>
)) ||
null,
c =>
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
<Localized id="moderate-marker-spamDetected" key={keyCounter++}>
<Marker color="error">Spam detected</Marker>
</Localized>
)) ||
null,
c =>
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
<Localized id="moderate-marker-toxic" key={keyCounter++}>
<Marker color="error">Toxic</Marker>
</Localized>
)) ||
null,
c =>
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_REPEAT_POST && (
<Localized id="moderate-marker-repeatPost" key={keyCounter++}>
<Marker color="error">Repeat comment</Marker>
</Localized>
)) ||
null,
c =>
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_RECENT_HISTORY && (
<Localized id="moderate-marker-recentHistory" key={keyCounter++}>
<Marker color="error">Recent history</Marker>
</Localized>
)) ||
null,
c =>
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
<Marker key={keyCounter++} color="error">
<Localized id="moderate-marker-offensive">
<span>Offensive</span>
</Localized>{" "}
<MarkerCount>
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
</MarkerCount>
</Marker>
</Localized>
)) ||
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
<Localized id="moderate-marker-spam" key={keyCounter++}>
<Marker color="error">Spam</Marker>
</Localized>
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
<Localized id="moderate-marker-toxic" key={keyCounter++}>
<Marker color="error">Toxic</Marker>
</Localized>
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_RECENT_HISTORY && (
<Localized id="moderate-marker-recentHistory" key={keyCounter++}>
<Marker color="error">Recent History</Marker>
</Localized>
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
<Marker key={keyCounter++} color="error">
<Localized id="moderate-marker-offensive">
<span>Offensive</span>
</Localized>{" "}
<MarkerCount>
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
</MarkerCount>
</Marker>
)) ||
null,
c =>
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
<Marker key={keyCounter++} color="error">
<Localized id="moderate-marker-spam">
<span>Spam</span>
</Localized>{" "}
<MarkerCount>
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
</MarkerCount>
</Marker>
)) ||
(c.revision &&
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
<Marker key={keyCounter++} color="error">
<Localized id="moderate-marker-spam">
<span>Spam</span>
</Localized>{" "}
<MarkerCount>
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
</MarkerCount>
</Marker>
)) ||
null,
];
@@ -115,8 +133,10 @@ export class MarkersContainer extends React.Component<MarkersContainerProps> {
return (
<Markers
details={
doesHaveDetails ? (
<FlagDetailsContainer
doesHaveDetails || this.props.comment.editing.edited ? (
<ModerateCardDetailsContainer
hasDetails={!!doesHaveDetails}
hasRevisions={this.props.comment.editing.edited}
onUsernameClick={this.props.onUsernameClick}
comment={this.props.comment}
settings={this.props.settings}
@@ -133,8 +153,11 @@ export class MarkersContainer extends React.Component<MarkersContainerProps> {
const enhanced = withFragmentContainer<MarkersContainerProps>({
comment: graphql`
fragment MarkersContainer_comment on Comment {
...FlagDetailsContainer_comment
...ModerateCardDetailsContainer_comment
status
editing {
edited
}
revision {
actionCounts {
flag {
@@ -147,6 +170,7 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
COMMENT_DETECTED_SUSPECT_WORD
COMMENT_REPORTED_OFFENSIVE
COMMENT_REPORTED_SPAM
COMMENT_DETECTED_REPEAT_POST
}
}
}
@@ -160,7 +184,7 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
`,
settings: graphql`
fragment MarkersContainer_settings on Settings {
...FlagDetailsContainer_settings
...ModerateCardDetailsContainer_settings
}
`,
})(MarkersContainer);
@@ -8,7 +8,7 @@
}
.username {
margin-right: var(--mini-unit);
margin-right: var(--spacing-1);
padding: var(--spacing-1);
margin-left: calc(-1 * var(--spacing-1));
line-height: calc(16rem / var(--rem-base));
@@ -58,8 +58,8 @@
}
.asideMiniWithReplyTo {
/*
We have reply to field, so we align below the fattest
/*
We have reply to field, so we align below the fattest
element plus the reply to field.
*/
padding-top: calc(2 * (14rem / var(--rem-base)) + 5px + var(--spacing-4));
@@ -89,6 +89,10 @@
transition: background 100ms, box-shadow 100ms;
}
.root:focus {
outline: none;
}
.dangling {
background-color: var(--palette-grey-lightest);
box-shadow: none;
@@ -151,4 +155,26 @@
.miniButton {
width: 40px;
height: 40px;
}
}
.deleted {
background: var(--palette-grey-lightest);
}
.timestamp {
color: var(--palette-grey-lighter);
}
.edited {
color: var(--palette-grey-lighter);
padding-left: var(--spacing-2)
}
.selected {
box-shadow: 1px 4px 15px rgba(0, 0, 0, 0.25);
}
.authorStatus {
padding-right: var(--spacing-2);
}
@@ -14,9 +14,10 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
username: "Theon",
createdAt: "2018-11-29T16:01:51.897Z",
body: "content",
edited: false,
inReplyTo: null,
comment: {},
settings: {},
comment: {},
status: "undecided",
featured: false,
viewContextHref: "http://localhost/comment",
@@ -25,7 +26,9 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
onApprove: noop,
onReject: noop,
onFeature: noop,
onBan: noop,
onUsernameClick: noop,
onFocusOrClick: noop,
showStory: false,
moderatedBy: null,
};
@@ -42,7 +45,10 @@ it("renders correctly", () => {
it("renders reply correctly", () => {
const props: PropTypesOf<typeof ModerateCardN> = {
...baseProps,
inReplyTo: "Julian",
inReplyTo: {
username: "Julian",
id: "1234",
},
};
const renderer = createRenderer();
renderer.render(<ModerateCardN {...props} />);
@@ -91,3 +97,13 @@ it("renders story info", () => {
renderer.render(<ModerateCardN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders tombstoned when comment is deleted", () => {
const props: PropTypesOf<typeof ModerateCardN> = {
...baseProps,
deleted: true,
};
const renderer = createRenderer();
renderer.render(<ModerateCardN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -1,7 +1,15 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback } from "react";
import key from "keymaster";
import { noop } from "lodash";
import React, {
FunctionComponent,
useCallback,
useEffect,
useRef,
} from "react";
import { HOTKEYS } from "coral-admin/constants";
import { PropTypesOf } from "coral-framework/types";
import {
BaseButton,
@@ -9,15 +17,17 @@ import {
Flex,
HorizontalGutter,
TextLink,
Timestamp,
Typography,
} from "coral-ui/components";
import ApproveButton from "./ApproveButton";
import CommentAuthorContainer from "./CommentAuthorContainer";
import CommentContent from "./CommentContent";
import FeatureButton from "./FeatureButton";
import InReplyTo from "./InReplyTo";
import MarkersContainer from "./MarkersContainer";
import RejectButton from "./RejectButton";
import Timestamp from "./Timestamp";
import Username from "./Username";
import styles from "./ModerateCard.css";
@@ -27,8 +37,12 @@ interface Props {
username: string;
createdAt: string;
body: string;
inReplyTo: string | null;
comment: PropTypesOf<typeof MarkersContainer>["comment"];
inReplyTo?: {
id: string;
username: string | null;
} | null;
comment: PropTypesOf<typeof MarkersContainer>["comment"] &
PropTypesOf<typeof CommentAuthorContainer>["comment"];
settings: PropTypesOf<typeof MarkersContainer>["settings"];
status: "approved" | "rejected" | "undecided";
featured: boolean;
@@ -43,15 +57,22 @@ interface Props {
onApprove: () => void;
onReject: () => void;
onFeature: () => void;
onUsernameClick: () => void;
onUsernameClick: (id?: string) => void;
onFocusOrClick: () => void;
mini?: boolean;
hideUsername?: boolean;
selected?: boolean;
/**
* If set to true, it means this comment is about to be removed
* from the queue. This will trigger some styling changes to
* reflect that
*/
dangling?: boolean;
deleted?: boolean;
edited: boolean;
selectPrev?: () => void;
selectNext?: () => void;
onBan: () => void;
}
const ModerateCard: FunctionComponent<Props> = ({
@@ -77,20 +98,84 @@ const ModerateCard: FunctionComponent<Props> = ({
storyHref,
onModerateStory,
moderatedBy,
selected,
onFocusOrClick,
mini = false,
hideUsername = false,
deleted = false,
edited,
selectNext,
selectPrev,
onBan,
}) => {
const div = useRef<HTMLDivElement>(null);
useEffect(() => {
if (selected) {
if (selectNext) {
key(HOTKEYS.NEXT, id, selectNext);
}
if (selectPrev) {
key(HOTKEYS.PREV, id, selectPrev);
}
if (onBan) {
key(HOTKEYS.BAN, id, onBan);
}
key(HOTKEYS.APPROVE, id, onApprove);
key(HOTKEYS.REJECT, id, onReject);
// The the scope such that only events attached to the ${id} scope will
// be honored.
key.setScope(id);
return () => {
// Remove all events that are set in the ${id} scope.
key.deleteScope(id);
};
} else {
// Remove all events that were set in the ${id} scope.
key.deleteScope(id);
}
return noop;
}, [selected, id]);
useEffect(() => {
if (selected && div && div.current) {
div.current.focus();
}
}, [selected]);
const commentBody = deleted ? (
<Localized id="moderate-comment-deleted-body">
<Typography>
This comment is no longer available. The commenter has deleted their
account.
</Typography>
</Localized>
) : (
body
);
const commentAuthorClick = useCallback(() => {
onUsernameClick();
}, [onUsernameClick]);
const commentParentAuthorClick = useCallback(() => {
if (inReplyTo) {
onUsernameClick(inReplyTo.id);
}
}, [onUsernameClick, inReplyTo]);
return (
<Card
className={cn(
styles.root,
{ [styles.borderless]: mini },
{ [styles.dangling]: dangling }
{ [styles.dangling]: dangling },
{ [styles.deleted]: deleted },
{ [styles.selected]: selected }
)}
ref={div}
tabIndex={0}
data-testid={`moderate-comment-${id}`}
id={`moderate-comment-${id}`}
onClick={onFocusOrClick}
>
<Flex>
<div className={styles.mainContainer}>
@@ -108,12 +193,29 @@ const ModerateCard: FunctionComponent<Props> = ({
<Username>{username}</Username>
</BaseButton>
)}
<CommentAuthorContainer comment={comment} />
<Timestamp>{createdAt}</Timestamp>
<FeatureButton featured={featured} onClick={onFeature} />
{edited && (
<Localized id="moderate-comment-edited">
<Typography variant="timestamp" className={styles.edited}>
(edited)
</Typography>
</Localized>
)}
<FeatureButton
featured={featured}
onClick={onFeature}
enabled={!deleted}
/>
</Flex>
{inReplyTo && (
{inReplyTo && inReplyTo.username && (
<div>
<InReplyTo>{inReplyTo}</InReplyTo>
<BaseButton
onClick={commentParentAuthorClick}
className={styles.username}
>
<InReplyTo>{inReplyTo.username}</InReplyTo>
</BaseButton>
</div>
)}
</div>
@@ -122,7 +224,7 @@ const ModerateCard: FunctionComponent<Props> = ({
bannedWords={bannedWords}
className={styles.content}
>
{body}
{commentBody}
</CommentContent>
<div className={styles.footer}>
<HorizontalGutter>
@@ -170,7 +272,8 @@ const ModerateCard: FunctionComponent<Props> = ({
className={cn(styles.aside, {
[styles.asideWithoutReplyTo]: !inReplyTo,
[styles.asideMini]: mini && !inReplyTo,
[styles.asideMiniWithReplyTo]: mini && inReplyTo,
[styles.asideMiniWithReplyTo]:
mini && inReplyTo && inReplyTo.username,
})}
alignItems="center"
direction="column"
@@ -185,13 +288,13 @@ const ModerateCard: FunctionComponent<Props> = ({
<RejectButton
onClick={onReject}
invert={status === "rejected"}
disabled={status === "rejected" || dangling}
disabled={status === "rejected" || dangling || deleted}
className={cn({ [styles.miniButton]: mini })}
/>
<ApproveButton
onClick={onApprove}
invert={status === "approved"}
disabled={status === "approved" || dangling}
disabled={status === "approved" || dangling || deleted}
className={cn({ [styles.miniButton]: mini })}
/>
</Flex>
@@ -1,25 +1,31 @@
import { Match, Router, withRouter } from "found";
import React, { FunctionComponent, useCallback } from "react";
import React, { FunctionComponent, useCallback, useState } from "react";
import { graphql } from "react-relay";
import NotAvailable from "coral-admin/components/NotAvailable";
import BanModal from "coral-admin/components/UserStatus/BanModal";
import { getModerationLink } from "coral-admin/helpers";
import {
ApproveCommentMutation,
RejectCommentMutation,
} from "coral-admin/mutations";
import FadeInTransition from "coral-framework/components/FadeInTransition";
import {
MutationProp,
withFragmentContainer,
withMutation,
} from "coral-framework/lib/relay";
import { GQLUSER_STATUS } from "coral-framework/schema";
import { GQLTAG } from "coral-framework/schema";
import {
COMMENT_STATUS,
ModerateCardContainer_comment,
} from "coral-admin/__generated__/ModerateCardContainer_comment.graphql";
import { ModerateCardContainer_settings } from "coral-admin/__generated__/ModerateCardContainer_settings.graphql";
import { ModerateCardContainer_viewer } from "coral-admin/__generated__/ModerateCardContainer_viewer.graphql";
import NotAvailable from "coral-admin/components/NotAvailable";
import { getModerationLink } from "coral-admin/helpers";
import { ApproveCommentMutation } from "coral-admin/mutations";
import { RejectCommentMutation } from "coral-admin/mutations";
import FadeInTransition from "coral-framework/components/FadeInTransition";
import {
MutationProp,
withFragmentContainer,
withMutation,
} from "coral-framework/lib/relay";
import { GQLTAG } from "coral-framework/schema";
import BanCommentUserMutation from "./BanCommentUserMutation";
import FeatureCommentMutation from "./FeatureCommentMutation";
import ModerateCard from "./ModerateCard";
import ModeratedByContainer from "./ModeratedByContainer";
@@ -33,6 +39,7 @@ interface Props {
rejectComment: MutationProp<typeof RejectCommentMutation>;
featureComment: MutationProp<typeof FeatureCommentMutation>;
unfeatureComment: MutationProp<typeof UnfeatureCommentMutation>;
banUser: MutationProp<typeof BanCommentUserMutation>;
danglingLogic: (status: COMMENT_STATUS) => boolean;
match: Match;
router: Router;
@@ -40,6 +47,11 @@ interface Props {
mini?: boolean;
hideUsername?: boolean;
onUsernameClicked?: (userID: string) => void;
onSetSelected?: () => void;
selected?: boolean;
selectPrev?: () => void;
selectNext?: () => void;
loadNext?: (() => void) | null;
}
function getStatus(comment: ModerateCardContainer_comment) {
@@ -71,25 +83,50 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
unfeatureComment,
mini,
hideUsername,
selected,
selectPrev,
selectNext,
onUsernameClicked: usernameClicked,
onSetSelected: setSelected,
banUser,
loadNext,
}) => {
const handleApprove = useCallback(() => {
approveComment({
const [showBanModal, setShowBanModal] = useState(false);
const handleApprove = useCallback(async () => {
if (!comment.revision) {
return;
}
await approveComment({
commentID: comment.id,
commentRevisionID: comment.revision.id,
storyID: match.params.storyID,
});
if (loadNext) {
loadNext();
}
}, [approveComment, comment, match]);
const handleReject = useCallback(() => {
rejectComment({
const handleReject = useCallback(async () => {
if (!comment.revision) {
return;
}
await rejectComment({
commentID: comment.id,
commentRevisionID: comment.revision.id,
storyID: match.params.storyID,
});
if (loadNext) {
loadNext();
}
}, [rejectComment, comment, match]);
const handleFeature = useCallback(() => {
if (!comment.revision) {
return;
}
featureComment({
commentID: comment.id,
commentRevisionID: comment.revision.id,
@@ -134,6 +171,35 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
[router, comment]
);
const onFocusOrClick = useCallback(() => {
if (setSelected) {
setSelected();
}
}, [selected, comment]);
const handleBanModalClose = useCallback(() => {
setShowBanModal(false);
}, []);
const openBanModal = useCallback(() => {
if (
!comment.author ||
comment.author.status.current.includes(GQLUSER_STATUS.BANNED)
) {
return;
}
setShowBanModal(true);
}, [comment]);
const handleBanConfirm = useCallback(
async (message: string) => {
if (comment.author) {
await banUser({ userID: comment.author.id, message });
}
setShowBanModal(false);
},
[comment]
);
return (
<>
<FadeInTransition active={Boolean(comment.enteredLive)}>
@@ -146,7 +212,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
}
createdAt={comment.createdAt}
body={comment.body!}
inReplyTo={comment.parent && comment.parent.author!.username!}
inReplyTo={comment.parent && comment.parent.author}
comment={comment}
settings={settings}
dangling={danglingLogic(comment.status)}
@@ -159,9 +225,18 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
onReject={handleReject}
onFeature={onFeature}
onUsernameClick={onUsernameClicked}
selected={selected}
selectPrev={selectPrev}
selectNext={selectNext}
onBan={openBanModal}
moderatedBy={
<ModeratedByContainer viewer={viewer} comment={comment} />
<ModeratedByContainer
onUsernameClicked={onUsernameClicked}
viewer={viewer}
comment={comment}
/>
}
onFocusOrClick={onFocusOrClick}
showStory={showStoryInfo}
storyTitle={
(comment.story.metadata && comment.story.metadata.title) || (
@@ -172,8 +247,20 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
onModerateStory={handleModerateStory}
mini={mini}
hideUsername={hideUsername}
deleted={comment.deleted ? comment.deleted : false}
edited={comment.editing.edited}
/>
</FadeInTransition>
<BanModal
username={
comment.author && comment.author.username
? comment.author.username
: ""
}
open={showBanModal}
onClose={handleBanModalClose}
onConfirm={handleBanConfirm}
/>
</>
);
};
@@ -185,6 +272,9 @@ const enhanced = withFragmentContainer<Props>({
author {
id
username
status {
current
}
}
statusLiveUpdated
createdAt
@@ -196,8 +286,12 @@ const enhanced = withFragmentContainer<Props>({
revision {
id
}
editing {
edited
}
parent {
author {
id
username
}
}
@@ -209,8 +303,10 @@ const enhanced = withFragmentContainer<Props>({
}
permalink
enteredLive
deleted
...MarkersContainer_comment
...ModeratedByContainer_comment
...CommentAuthorContainer_comment
}
`,
settings: graphql`
@@ -229,10 +325,12 @@ const enhanced = withFragmentContainer<Props>({
`,
})(
withRouter(
withMutation(ApproveCommentMutation)(
withMutation(RejectCommentMutation)(
withMutation(FeatureCommentMutation)(
withMutation(UnfeatureCommentMutation)(ModerateCardContainer)
withMutation(BanCommentUserMutation)(
withMutation(ApproveCommentMutation)(
withMutation(RejectCommentMutation)(
withMutation(FeatureCommentMutation)(
withMutation(UnfeatureCommentMutation)(ModerateCardContainer)
)
)
)
)
@@ -0,0 +1,4 @@
.button {
text-transform: uppercase;
font-size: 14px;
}

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