Compare commits

...
330 Commits
Author SHA1 Message Date
immberandjessie.rushing 4e464c48df Docs update- Jul 2020 (#3056)
* added existing closing stories behavior

* updated find your embed

* updated sso docs

* updated graphiql section

* clarified A user, not all the users

* clarified manual override on open/close stories

* clarified uniqueness constraints on sso

Co-authored-by: jessie.rushing <jessie.rushing@voxmedia.com>
2020-08-03 18:28:40 +00:00
Vinh fd64469c00 [DOCS] CSS Variables (#3015)
* docs: document css variables

* Update 02-04-css.md
2020-07-16 17:20:56 -04:00
Wyatt Johnson e488e878d0 feat: updated docs (#2998) 2020-06-25 09:21:24 -06:00
Wyatt Johnson 9f9f5a8a8e Bump 4.11.6 (#2997)
* chore: bump version

* fix: update docs
2020-06-23 19:06:19 +00:00
Mendel Konikov 77e54aab1f [v4] fix: Pin node-gyp to address issue building on Node 8 (#2992)
* Update Dockerfile

* Update config.yml
2020-06-23 18:41:34 +00:00
Andrew Losowsky 9109d5ab71 Added moderation queues detail (#2985)
Should these go into a separate 'Moderation' menu item below 'Admin Settings'?
2020-06-11 22:12:25 +00:00
Vinh 104b719dec docs: bodyClassName (#2970) 2020-06-01 14:12:21 -04:00
vladimir04andKim Gardner 9e69cecb11 add RO translation yml file (#2957)
Co-authored-by: Kim Gardner <kgardnr@gmail.com>
2020-05-11 11:48:46 -04:00
Leandro 791f9bdabd Add translation keys on email subjects of user services (#2938) 2020-04-23 11:58:24 -04:00
Leandro 8d183915fa [v4] Allow MODERATORS to close asset on stream. (#2921)
* Allow MODERATORS to close asset on stream.

MODERATORS can close asset on Admin side, but on Stream when they
click to close, got an unathorized error. Change it to make
consistency about what actions can be done.

* Fix tests
2020-04-13 21:37:50 +00:00
Wyatt Johnson 9e0d35e737 docs: added docs for FORCE_SSL (#2918) 2020-03-30 20:36:05 +00:00
Wyatt Johnson 21f436255e fix: fixed bug with plugin docs (#2896) 2020-03-18 16:55:13 +00:00
Wyatt Johnson 6ba7a43cb4 [DOCS] 6.0.0 Release (#2888)
* docs: added docs for 6.0.0 release

* fix: restored url docs
2020-03-17 17:12:50 +00:00
Andrew Losowsky 331a83d1fc Changed title, small updates (#2875) 2020-03-06 10:59:09 -05:00
8d3de4b082 [DOCS DAY] added event tracking to v5 section (#2857)
* added event tracking to v5 section

* Add SEO docs and cleanup sidebar

* Fixes raw html in SSO docs

* fixed typo on sso page

Co-authored-by: Kim Gardner <kgardnr@gmail.com>
Co-authored-by: Vinh <vinh@vinh.tech>
2020-02-26 12:29:42 -05:00
Nick FunkandKim Gardner c4801abfbe Break contributing out into sub-sections (#2862)
Main section links to main contributing guidelines
on the main GitHub repo.

Second subsection details translation contributions.

Co-authored-by: Kim Gardner <kgardnr@gmail.com>
2020-02-25 18:15:39 -05:00
Kim Gardner c0861c20d3 [CORL-880] SEO & iframe docs (#2859)
* Add SEO docs and cleanup sidebar

* Fixes raw html in SSO docs
2020-02-25 17:59:07 -05:00
Tessa Thornton 63545a48c4 add docs for user.url jwt claim (#2835) 2020-02-19 14:40:15 -05:00
Vinh e1bfe5b608 [Docs] Notes about IE11 compatibility (#2810)
* docs: ie11

* fix: wordings
2020-01-29 15:27:01 -05:00
Andrew Losowsky dc096137e3 Minor grammatical changes (#2793) 2020-01-14 16:41:05 +00:00
4d41d999fb CORL-753 | DOCs Updates (#2776)
* added storyURL and storyID

* added story scraping section

* fixed typo on sso

* rebaseing on latest release/4 branch

* updated version on slack docs

* added Contributing a Translation section to developing

* fixing typos

* updated v5 config page, reorder list, add toc

* added cli sect, fixed contact menu link

* updated talk to coral on contact page

* added a new v5 auth section

* added session length to auth docs

* added an admin settings page to v5

* added v5 css section

* added a v5 notifications section

* updated faq and troubleshooting, moved out of v4 menu

* added migrating to v5 and moved migrating section out of v4

* added plugins note to migration pg

* fix: linting and extra HTML

* change sidebar migrating v5 to v5.0+

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

* downtime being likely is too optimistic, it will be required

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

* rewords plugins note on migrating to v5

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

* tag code block as html

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

* added link to contributing

* rephrased openid connect description

* fixed link to contributing

* correcting descriptions of email auth behavior

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

* Adding link to css classnames

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

* renamed new auth to Social and Email Authentication

* pulled extra line breaks

Co-authored-by: Wyatt Johnson <accounts+github@wyattjoh.ca>
2020-01-08 21:25:58 +00:00
Nick FunkandKim Gardner b50d3dc96f Add missing environment variables to v5 config docs (#2770)
DISABLE_CLIENT_ROUTES
DISABLE_RATE_LIMITERS
SCRAPE_TIMEOUT
DISABLE_FORCE_SSL

CORL-806

Co-authored-by: Kim Gardner <kgardnr@gmail.com>
2019-12-23 11:26:29 -05:00
Hans SprecherandWyatt Johnson 8c0f55af21 Update version-5-installation.md (#2771)
* Update version-5-installation.md

The command `npm run start` doesn't work locally following these directions, but `npm run start:development` does. Based on the package.json commands, the `:development` command seems reasonable:

```
    "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",
```

Here's the output I received from `npm run start`:

```
Hanss-MBP:talk [redacted]$ npm run start

> @coralproject/talk@5.4.0 start /Users/[redacted]/workspace/talk
> NODE_ENV=production node dist/index.js

{"name":"coral","clusterNode":"master","hostname":"[redacted].com","pid":25656,"level":50,"err":{"message":"SIGNING_SECRET is required in production environments","name":"Error","stack":"new Server (src/core/server/index.ts:126:15)\nObject.createCoral [as default] (src/core/index.ts:10:10)\nbootstrap (src/index.ts:76:20)\nObject.bootstrap (src/index.ts:109:1)\nModule._compile (internal/modules/cjs/loader.js:959:30)\nObject.Module._extensions..js (internal/modules/cjs/loader.js:995:10)\nModule.load (internal/modules/cjs/loader.js:815:32)\nFunction.Module._load (internal/modules/cjs/loader.js:727:14)\nFunction.Module.runMain (internal/modules/cjs/loader.js:1047:10)\ninternal/main/run_main_module.js:17:11\n"},"msg":"can not bootstrap server","time":"2019-12-20T23:27:38.314Z","src":{"file":"/Users/[redacted]/workspace/talk/src/index.ts","line":104,"func":"error"},"v":0}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! @coralproject/talk@5.4.0 start: `NODE_ENV=production node dist/index.js`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the @coralproject/talk@5.4.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/[redacted]/.npm/_logs/2019-12-20T23_27_38_335Z-debug.log
```

* fix: expanded on docs

Co-authored-by: Wyatt Johnson <accounts+github@wyattjoh.ca>
2019-12-21 00:14:32 +00:00
Nick Funk ed98b218bb Update logo to match new VoxMedia branding (#2764)
CORL-804
2019-12-20 17:02:18 -05:00
Wyatt Johnson 64800ffaee [CORL-822] Safari Fix (#2760)
* fix: forced version

* fix: removed cookie code

* chore: version bump
2019-12-19 14:14:59 -05:00
Wyatt Johnson 637605a002 fix: added trust proxy config (#2751) 2019-12-12 22:50:00 +00:00
Nick Funk 5580e14bf3 Create Slack integration documentation (#2714)
Explains how to create a Slack App and hook it into
Coral as well as how to re-find your webhook URLs
if you have lost them.

CORL-645
2019-12-03 16:06:52 -05:00
Wyatt Johnson 109d9e93f5 docs: added doc updates for Node 12 (#2730) 2019-11-22 23:40:11 +00:00
Kim GardnerandWyatt Johnson ff72d79748 Update SSO docs and add GDPR docs (#2701)
* Update SSO docs and add GDPR docs

* Update docs/source/version-5-gdpr.md

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

* Update docs/source/version-5-gdpr.md

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

* Update docs/source/version-5-gdpr.md

Co-Authored-By: Wyatt Johnson <wyattjoh@gmail.com>
2019-11-12 14:47:05 -05:00
Max Baumann 0645735d2b Error responses from scraped pages should not return metadata (#2691)
* Don't return error pages as valid scrapes

* Fix linting

* whitespace
2019-11-08 18:57:20 +00:00
Kim Gardner f1a0febd6c Bump version to 4.11.2 (#2699) 2019-11-08 17:48:13 +00:00
Wyatt Johnson 16d0b39ebc fix: added index for query that loads flags for user (#2678) 2019-11-07 13:43:05 -05:00
Wyatt Johnson 66c7430ff1 fix: fixed deploy scripts (#2665) 2019-10-22 14:15:23 -04:00
Kim Gardner ac06f0d13b Bump version 4.11.1 (#2663) 2019-10-22 13:20:35 -04:00
Wyatt Johnson ca10062498 [CORL-646] Docs update (#2648)
* docs: added docs for trust proxy

* fix: adjusted incorrect HTML

- fixes #2656
2019-10-22 17:01:48 +00:00
Wyatt Johnson c9381c6367 fix: addressed link detection issue (#2660) 2019-10-22 10:34:32 -04:00
Andrew Losowsky 13853ef87e Added notes about hosting and improved sell (#2638) 2019-10-15 14:20:28 -04:00
Pepe Franco fb2f36d0ee Update getReactionConfig.js (#2618)
* Update getReactionConfig.js

Delete duplicated code

* fix: linting
2019-10-03 17:10:20 +00:00
Wyatt Johnson 1a2fa73941 [CORL-663] v4 release fixes (#2613)
* fix: removed latest pushing from docker deploy script

* fix: addressed circleci bug
2019-10-01 22:37:42 +00:00
Vinh d3931db285 [CORL-181] Add docs for comment count integration (#2598)
* docs: add docs for comment count integration

* docs: update count docs

* chore: improve integration docs
2019-10-01 19:27:00 +00:00
Wyatt Johnson 093202f9e2 fix: disable browserstack tests (#2610) 2019-10-01 13:49:22 -04:00
Kim Gardner 60be682a5f Update package.json (#2609) 2019-10-01 16:01:34 +00:00
Joe Simon 41c9ae32b1 v4 - Adding live updates for accepted comments (#2584)
* First pass at live updates for accepted comments

* fix: logic cleanup
2019-09-30 23:47:30 +00:00
Wyatt Johnson 4445479237 chore: updated minimum mongo versions (#2592) 2019-09-30 16:15:24 +00:00
Andrew Losowsky aab44587a6 Small changes (#2599)
Name change and small edit
2019-09-30 15:59:03 +00:00
immber 0af28edbd9 fixed broken links, added username restrictions, minor docs updates (#2580) 2019-09-20 19:31:20 +00:00
Wyatt Johnson 9db7571fe2 fix: fixed heroku install button (#2552) 2019-09-10 19:40:41 +00:00
Wyatt Johnson 683adceec4 [CORL-603] Documentation Updates (#2553)
* fix: replaced png with svg

* feat: updated docs

* feat: added netlify config

* fix: fixed netlify config
2019-09-10 19:20:24 +00:00
Wyatt Johnson aa6058e9d1 [CORL-584] Add Version 5 Documentation (#2523)
* feat: initial version

* fix: adjusted height

* fix: tweaked sidebar styles again

* fix: adjusted overflow property

* fix: reverted overflow, cleaned sidebar
2019-09-05 20:53:03 +00:00
Wyatt Johnson a4c4032b0d [CORL-546] Adjusted rejected username copy (#2483)
* fix: fixed copy, removed broken translations

* chore: version bump
2019-08-16 21:15:59 +00:00
Wyatt Johnson 2ea0d51613 chore: bumped version (#2459) 2019-08-09 17:43:22 -04:00
Wyatt Johnson f2564f4eda fix: addressed issues with create flow (#2458) 2019-08-09 21:38:38 +00:00
Kim Gardner 73040b1314 Update rejected username language (#2420) 2019-07-25 15:19:17 -04:00
Wyatt Johnson c3c106c7b0 Update README.md (#2413) 2019-07-19 11:59:31 -04:00
Andrew Losowsky 8faf69ce48 Updated copyright notice from Mozilla to Vox Media (#2412)
* Updated copyright notice from Mozilla to Vox Media

* fix: update package.json
2019-07-19 15:45:49 +00:00
Wyatt Johnson 23f17db0c9 [CORL-447] Stories Fix (#2408)
* fix: fixes close/open bug

* fix: fixed version display

* chore: bumped version
2019-07-17 15:52:58 +01:00
Kim Gardner 1385a0b961 Bump version 4.10.0 (#2406) 2019-07-15 19:53:33 +01:00
Wyatt Johnson 7fd01e5845 [CORL-444] Stories Tab Adjustments (#2404)
* feat: improved stories tab

* feat: swapped date sorting with text sorting
2019-07-12 20:18:21 +00:00
immber 340052cdf0 DOCS | Add asset_url note to CMS integration & Update FB config (#2393)
* added note RE passing asset_url, and updated fb configs

* fix: syntax tweaks

* feat: small tweaks to embedding
2019-07-10 09:41:52 -07:00
Leandro a085e4b6f9 Translation key typo. (#2389)
Fix translation key "login.request_passowrd"
to "login.request_password".
2019-07-03 17:28:34 +01:00
Kim Gardner 1575b15e36 Merge pull request #2374 from leeeandroo/german-translations
Added missing German translations.
2019-06-21 15:32:55 +01:00
Kim Gardner 7da9126a76 Merge branch 'master' into german-translations 2019-06-21 15:24:17 +01:00
Kim Gardner 9453d207f5 Merge pull request #2373 from leeeandroo/featured-tooltip-breakword
Break headline on featured comments plugin
2019-06-21 15:23:18 +01:00
Leandro Vasco da Rocha 869c760a8b Grammatical changes on German wording 2019-06-20 14:44:52 +02:00
Leandro Vasco da Rocha b283595c97 Break headline on featured comments plugin.
If a headline is too long, we need to break it to fit the tooltip
content box.
2019-06-20 13:24:05 +02:00
Leandro Vasco da Rocha d672af63f9 Added missing german translations. 2019-06-18 15:53:27 +02:00
Kim Gardner b4ad78fd65 Bump version 4.9.1 (#2361) 2019-06-14 15:28:26 +00:00
Kim Gardner 713de46c2a Merge pull request #2359 from coralproject/corl-427
[CORL-427] AddEmailAddressDialog should appear at the top
2019-06-14 14:53:46 +01:00
Chi Vinh Le fc1e51ed62 fix: Position Add Email Address Dialog to the top 2019-06-14 00:08:37 +02:00
Etienne Martin 4eef6f4218 Added missing translations (#2357) 2019-06-13 19:06:41 +00:00
Kim Gardner c8cfd7c0f8 Merge pull request #2345 from coralproject/bump-4-9-0
Bump version 4.9.0
2019-06-06 20:11:11 +01:00
Kim Gardner 0e4cbf5f44 Bump version 4.9.0 2019-06-06 19:59:58 +01:00
Kim Gardner 448b988281 Merge pull request #2327 from coralproject/next-amp-docs
AMP docs
2019-06-05 20:55:40 +01:00
Kim Gardner 7a688495f2 Merge branch 'master' into next-amp-docs 2019-05-29 13:29:57 +01:00
Kim Gardner b9cf7084eb Merge pull request #2326 from Sentynel/master
Don't crash on email address lookup failure
2019-05-29 13:29:34 +01:00
Kim Gardner b99161f0cb Merge branch 'master' into next-amp-docs 2019-05-29 12:59:22 +01:00
Kim Gardner 6faa4862de Merge branch 'master' into master 2019-05-29 12:58:57 +01:00
Aram Zucker-Scharff eff3f1dfd8 Small correction of a spelling error (#2325) 2019-05-28 16:36:17 +00:00
immber 73aa77d7da CORL-380 | DOCS | Updates pre-launch checklist, asset scraping (#2317)
* updated asset scraping docs

* added note about setting close date

* added close window setting details
2019-05-27 17:58:18 +02:00
Chi Vinh Le 5580ab385f fix: remove margin from amp body 2019-05-27 17:20:07 +02:00
Chi Vinh Le 40eb0e97ec docs: add amp docs 2019-05-27 17:19:05 +02:00
Sam Lade b7e1ce0205 Don't crash on email address lookup failure
If for whatever reason an email job gets queued for a user with no
email, an exception is thrown on the getEmailAddress() step. This
exception was never handled and therefore the app crashed. Handle it and
log instead..
2019-05-27 14:12:14 +01:00
Kiwi bfde9bf8c7 [CORL-373] Experimental AMP Support (#2324)
* feat: support amp

* fix: support a custom PORT for amp development
2019-05-23 20:42:58 +02:00
Gerardo Gálvez 54f9b28086 Show/hide a comment's edit history from Moderation and User Detail. (#2299)
* Show/Hide comment edit history in moderation queues and user detail.

* Add spanish translations

* Place timestamp and comment body on separate lines

* Remove padding between timestamp and comment
2019-05-15 19:57:21 +02:00
immber 03a5eb7f9b Merge pull request #2307 from coralproject/fbdocs
DOCS | Update fb app creation steps
2019-05-09 15:51:51 -07:00
immber 43e60e6544 Merge branch 'master' into fbdocs 2019-05-09 15:37:24 -07:00
Jessie Rushing ebd16666e6 Merge branch 'fbdocs' of https://github.com/coralproject/talk into fbdocs 2019-05-09 15:36:37 -07:00
Jessie Rushing f2129082a9 added note about location of live button 2019-05-09 15:35:41 -07:00
Kim Gardner 1098c068f2 Merge pull request #2310 from coralproject/bump-version-4-8-7
Bump version 4.8.7
2019-05-09 23:19:10 +01:00
Kim Gardner f0c95d0044 Update package.json 2019-05-09 23:12:14 +01:00
immber 74226aa6fc Merge branch 'master' into fbdocs 2019-05-09 15:08:59 -07:00
Jessie Rushing b6ed5b792b updated Fb dev steps 2019-05-09 15:07:40 -07:00
Kim Gardner d31969592c Remove quotes from Featured Comments (#2309) 2019-05-09 23:50:43 +02:00
Kiwi 794487b1a9 fix: bug where erasing the edit RTE content would respawn the original content (#2308) 2019-05-09 23:22:14 +02:00
Jessie Rushing 39e45c30ba added callback url 2019-05-08 16:05:13 -07:00
Jessie Rushing ed734ebb08 update fb app creation steps 2019-05-08 15:42:06 -07:00
Kim Gardner 9f059e0c4a Docs typo (#2303) 2019-05-07 21:59:25 +00:00
Kim Gardner 7d369f2d35 Merge pull request #2279 from type-face/admin-ui-custom-css
apply custom CSS to admin panel
2019-05-07 19:40:18 +01:00
type_face 5fb5240f89 Merge branch 'master' into admin-ui-custom-css 2019-05-02 12:28:59 -07:00
Wyatt Johnson 976931135c fix: added username config (#2295) 2019-05-02 16:07:42 +00:00
Wyatt Johnson 1e3d47eb87 Tags Permissions (#2291)
* fix: fixed a bug that exposed tags

* chore: bumped versions
2019-04-29 22:28:49 +02:00
Kim Gardner 2d804d783b Merge pull request #2283 from coralproject/docs
Removed Old Roadmap
2019-04-26 14:48:49 +01:00
type_face 3404578ecc Merge branch 'master' into admin-ui-custom-css 2019-04-24 15:54:12 -07:00
Rob Ring 48c5355a5b apply custom CSS to admin panel 2019-04-24 15:50:29 -07:00
Wyatt Johnson e3a300c643 fix: removed roadmap from sidebar 2019-04-24 13:00:58 -06:00
Andrew Losowsky a6653152e4 added info box (#2281)
* added info box

* fix: styles
2019-04-23 22:08:48 +00:00
Kim Gardner 441422542c Fix broken links and update info (#2277) 2019-04-22 23:41:34 +00:00
Kim Gardner 1d1822c086 Bump version 4.8.4 (#2266) 2019-04-17 16:54:58 +00:00
Kim Gardner 1c2ff7d2ae Update default plugins and docs (#2265)
* Update default plugins

* Update plugin docs with missing plugin

* Fixed broken link in docs

* Update Code of Conduct in docs

* Fix mixup that both plugins are moderation-actions

* fix: adapt server tests to use the rich text plugin

* fix: e2e

* fix: sync docs
2019-04-16 20:54:44 +02:00
Kim Gardner 9319bb4e1f Remove unnecessary Talk branding (#2271)
* Remove Talk copy from Login view

* Remove favicon from base template and only use it for admin view
2019-04-15 11:06:09 +02:00
Kim Gardner 6611f8b636 Fix email copy typo (#2267) 2019-04-11 17:50:10 +02:00
cpoonolly 738542ab99 Cherry seeing if he can fix stuff! (#2254) 2019-04-11 17:42:17 +02:00
Kim Gardner cd4ae47958 [Docs] Initial list of events you can track from Talk (#2262)
* Initial list of events you can track from Talk

* Remove duplicates

* Remove more duplicates
2019-04-08 19:01:42 +02:00
Wyatt Johnson 2ec66007d9 feat: added docs for featured comments tab (#2259) 2019-04-05 21:42:17 +00:00
immber ea34c936d0 DOCS || CORL-290 || Add Custom SSO Plugin Recipe (#2249)
* updated auth strategies

* added custom auth plugin recipe

* fixed format on TALK_SMTP_FROM_ADDRESS

* cleaned up formatting a bit

* added CDN to checklist

* Update docs/source/01-05-pre-launch-checklist.md

Co-Authored-By: immber <jessiekrushing@gmail.com>

* Update docs/source/integrating/authentication.md

Co-Authored-By: immber <jessiekrushing@gmail.com>

* added links to plugins
2019-04-01 22:23:22 +00:00
Mendel Konikov ef1fd9c159 Clarify that Reliable commenter is also not yet in use (#2253) 2019-04-01 21:14:04 +00:00
Andrew Losowsky c7ccf03132 Updated Vox Media Logo on Documentation (#2244)
* added Vox Media logo

* fix: added logo

* fix: cleanup
2019-03-27 22:34:58 -06:00
Andrew Losowsky 8015098305 Changed 'Mozilla' to 'Vox Media' in the sidebar (#2245)
* Changed 'Mozilla' to 'Vox Media' in the sidebar

* Delete moz-logo-bw-rgb.svg
2019-03-27 22:43:12 +00:00
Kim Gardner c85a2703f9 Merge pull request #2241 from coralproject/CORL-290
DOCS | CORL-290 | Documentation updates
2019-03-26 13:24:06 +00:00
Jessie Rushing 9aec6b026c updated event tracking page 2019-03-22 13:25:40 -07:00
immber b2ca9dba40 Merge pull request #2227 from coralproject/CORL-308
update tokenUserNotFound docs
2019-03-20 11:43:16 -07:00
immber 9186c73fff Merge branch 'master' into CORL-308 2019-03-20 11:07:20 -07:00
immber 86d55fa2de Merge pull request #2232 from GlynnPhillips/slack-doc-update
Update the slack config name in the docs
2019-03-20 11:02:56 -07:00
Wyatt Johnson 12c4cfbbf6 feat: improved server api docs 2019-03-19 16:24:32 -06:00
Jessie Rushing c875a8d9ed added note about subdomain whitelisting 2019-03-19 16:24:32 -06:00
Jessie Rushing 4d082165aa update tokenUserNotFound docs 2019-03-19 16:24:31 -06:00
Glynn Phillips 08c01981ac Update the slack config name in the docs
The documentation for the slack notification plugin referenced the wrong
environment variable name.
2019-03-19 14:16:07 +00:00
Wyatt Johnson cd01aae76c fix: fixed incorrect users lookup (#2229) 2019-03-18 12:06:29 -06:00
Andrew Losowsky 53367eeaac Updated text to include our pricing page (#2221)
A few updates plus corrections.
2019-03-11 22:58:50 +00:00
Wyatt Johnson 32962aa1e8 External User Fix (#2220)
* fix: fixed bug with user upserting

* chore: version bump
2019-03-11 19:35:22 +00:00
Kim Gardner 049357a09c Bump version 4.8.1 (#2218) 2019-03-07 16:00:18 +00:00
Wyatt Johnson 5e7fe7cf5a fix: upgrade passport-google-oauth2 (#2217)
- Fixes #2215
2019-03-07 01:12:08 +00:00
Kim Gardner 6243fcba91 Merge pull request #2212 from vittoboa/italian-translation
Italian translation
2019-03-04 11:53:41 -05:00
vittoboa 805825e5d2 Fix typo 2019-03-02 22:42:13 +01:00
vittoboa a40e721a68 Fix typo 2019-03-02 22:38:16 +01:00
vittoboa 1dd8d0a706 Add italian translation for plugins 2019-03-02 22:33:52 +01:00
Leandro da1a8bb539 Remove duplicate placeholder proptype. (#2211) 2019-03-01 19:34:24 +01:00
Kim Gardner 18add4cdea Merge pull request #2210 from sanoma/feature/talk_470_dutch_translations
Feature/talk 470 dutch translations
2019-02-28 12:28:17 -05:00
Kim Gardner de5de26c82 Merge branch 'master' into feature/talk_470_dutch_translations 2019-02-28 11:09:19 -05:00
Besma 934e4b4754 Replace email with e-mail for Dutch translation consistency 2019-02-27 10:11:39 +01:00
Kim Gardner 049658508c Merge pull request #2206 from coralproject/bump-version-4-8-0
v4.8.0
2019-02-26 17:03:38 -05:00
Kim Gardner f0ead66f68 v4.8.0 2019-02-26 14:30:35 -05:00
David Burke 14f1343930 Additional Replies Increment (#2203)
* added ADDTL_REPLIES_ON_LOAD_MORE variable

* feat: added docs and onbuild ref
2019-02-26 16:18:55 +00:00
Besma b21845b963 Update translations 2019-02-26 09:39:54 +01:00
Kim Gardner 0ff20bed5e Merge pull request #2201 from mazzi/fix/issue-2141
Suspended vs. Banned  translations and typos in Spanish fixed
2019-02-25 17:33:27 -05:00
Besma 0bb2d4cbca Add Dutch translation 2019-02-25 16:18:33 +01:00
Lautaro Mazzitelli 927a34a579 ci: quotation not needed 2019-02-22 17:37:38 -03:00
Lautaro Mazzitelli 75f2f05534 fix: Suspended vs. Banned. Fixed in spanish translations. 2019-02-22 17:21:56 -03:00
Lautaro Mazzitelli 2998db46ad fix: three different typos were fixed on spanish translation file. 2019-02-22 16:21:02 -03:00
Gerardo Gálvez 8163b52301 Flag users as "Always premoderate" (#2145)
* Flag users as "Always premoderate"

Status can be set using the action dropdown in the People tab and in User Details.
Users flagged as "Always premoderate" will have their comments sent to the premod queue.
Users can be filtered with the Always Premoderate status.
Include Always Premoderate status changes in User History.
Add spanish translations.

* Reorder CSS as per cvle's suggestion

* Address second comment
2019-02-13 20:08:22 +01:00
Kim Gardner 19b425dc1a Merge pull request #2182 from mkonikov/snackbar-improvements
Snackbar improvements
2019-02-13 13:57:35 -05:00
Kim Gardner be81ced2f5 Merge branch 'master' into snackbar-improvements 2019-02-13 11:42:57 -05:00
Kim Gardner f6e5f24722 Merge pull request #2189 from javierbarriosafonso/master
Spanish (es) translations for talk notifications plugins
2019-02-13 11:42:48 -05:00
Kim Gardner 2c1dc9c118 Merge branch 'master' into master 2019-02-13 11:34:21 -05:00
Steven 657bf388cf fix: rename devToolsExtension (#2192)
Note that starting from v2.7, window.devToolsExtension was renamed to window.__REDUX_DEVTOOLS_EXTENSION__ / window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__.
2019-02-12 17:12:29 +01:00
Javier Barrios b3412a46cf Spanish (es) translations for talk notifications plugins 2019-02-11 14:08:31 -03:00
Mendel Konikov c6ee54c2f5 Rename classes to match id 2019-02-08 10:46:32 -05:00
Mendel Konikov 174a80c377 Reorder style as previous 2019-02-08 10:34:40 -05:00
Mendel Konikov 17a05059d9 Add working close button and style 2019-02-08 10:31:57 -05:00
Mendel Konikov 8c53c25a89 Merge branch 'master' of github.com:mkonikov/talk into snackbar-improvements 2019-02-08 09:15:07 -05:00
Mendel Konikov 1928a7b02e Create Moderation actions notification plugin (#2175)
* Create Moderation actions plugin

* Clarify env var in readme

* Reword log message

* Update with suggestion from Wyatt

* Whitelist notification types

* Update messaging

* Rename unpublished to pending
2019-02-07 15:20:44 +01:00
immber 5173af2a27 talk-plugin-toxic-comments - ADD MODEL VARIABLE TO TOXIC COMMENTS (#2178)
* updated readme

* added config var TALK_PERSPECTIVE_MODEL

* fixed duplication on readme

* fixed link on readme

* fix: applied some changes
2019-02-07 00:22:10 +00:00
Kim Gardner 9077f31872 Merge pull request #2179 from mkonikov/patch-1
Break word on long description
2019-02-06 18:14:51 -05:00
Mendel Konikov 51e8035533 Break word on long description
Also fix font size
2019-02-06 11:40:51 -05:00
Wyatt Johnson 94786c2711 fix: repairs poor performance for assets (#2174)
- fixes #2158
2019-02-05 22:06:05 +01:00
Nacho Toledo 456abaf862 Update appropriate <section> class in Profile section (#2170)
* Update appropriate <section> class in user profile section

Download my comment history has `talk-plugin-ignore-user-section` class. Should be `talk-plugin-profile-data--download-my-comment-history` (couldn't figure out the proper naming convention used).

* Fix linting error

* Fix lint second try
2019-02-05 16:49:25 +01:00
Kim Gardner fe9f92d3fc Merge pull request #2165 from mkonikov/hebrew
Add Hebrew Language Support
2019-02-04 17:37:46 -05:00
Kim Gardner a49fbf2d05 Yml fix 2019-02-04 17:16:15 -05:00
Kim Gardner 51bb158446 Merge branch 'master' into hebrew 2019-02-04 15:52:14 -05:00
Kim Gardner d601bd1466 Merge pull request #2177 from coralproject/remove-pt-link
Update Readme
2019-02-04 15:51:53 -05:00
immber 8eb8294987 Merge branch 'master' into remove-pt-link 2019-02-04 12:08:11 -08:00
Kim Gardner 99332323a8 Merge branch 'master' into hebrew 2019-02-04 14:52:29 -05:00
Kim Gardner 1a23cc1fa7 Merge pull request #2159 from mkonikov/fix-ios-copy
Fix Permalink copy on iOS
2019-02-04 19:48:38 +00:00
Kim Gardner 798f55ed79 Merge branch 'master' into hebrew 2019-02-04 19:47:24 +00:00
Kim Gardner 6897c0e18d Merge branch 'master' into fix-ios-copy 2019-02-04 19:34:31 +00:00
Kim Gardner 9d7b9a91cd Merge pull request #2153 from ljubar/master
sr_ME translations (Montenegrin version of Serbian language, Latin letters)
2019-02-04 19:32:56 +00:00
Kim Gardner 51a62b8c26 Merge branch 'master' into master 2019-02-04 19:28:23 +00:00
Kim Gardner f11a51ebfc Update Readme 2019-02-04 12:56:50 -05:00
Kim Gardner dbdf38af73 Merge branch 'master' into hebrew 2019-02-04 17:30:32 +00:00
Mendel Konikov abc1664ef7 WIP: Snackbar close 2019-01-31 09:59:58 -05:00
immber c16514502c Update recommended release version to link (#2168)
* Update recommended release version to 4.7

* update release link on readme!
2019-01-29 20:41:26 +00:00
Mendel Konikov 9d3e3ce45d Add additional Hebrew translations 2019-01-27 21:59:38 -05:00
Mendel Konikov f6bfef3fb5 Sort Translations ABC, Add missing Hebrew 2019-01-27 21:31:51 -05:00
Mendel Konikov 9b0173e6bf Add few more translations 2019-01-26 21:41:02 -05:00
Mendel Konikov 43219eed4b Replace quote marks 2019-01-24 22:32:50 -05:00
Mendel Konikov 9300359ed1 Merge branch 'master' of github.com:coralproject/talk into hebrew 2019-01-24 22:31:15 -05:00
Mendel Konikov 80c3367259 Merge branch 'master' into fix-ios-copy 2019-01-24 13:58:22 -05:00
Andrew Losowsky ef731f719d added Vox Media link (#2161) 2019-01-24 18:11:04 +00:00
Kim Gardner 9c09509a08 Merge pull request #2163 from mkonikov/fix-typo
Fix typo for Asset URL translation
2019-01-23 20:10:58 +00:00
Mendel Konikov 313310a097 Fix typo 2019-01-23 00:55:23 -05:00
Mendel Konikov fa42c028df Fix quotes 2019-01-23 00:45:31 -05:00
Mendel Konikov 905e43e745 Remove deleted plugin 2019-01-23 00:41:29 -05:00
Mendel Konikov 6cc0c60ade Add Final hebrew translations 2019-01-23 00:38:53 -05:00
Mendel Konikov ca44588095 Add Hebrew profile translations - server 2019-01-22 23:38:10 -05:00
Mendel Konikov 9a49044030 Add client profile translations 2019-01-22 23:30:39 -05:00
Mendel Konikov 02963dc5ac Fix translation 2019-01-22 23:18:18 -05:00
Mendel Konikov 5ad90e00cb Add Hebrew to profile plugin 2019-01-22 23:16:09 -05:00
Mendel Konikov 1dfc4b83c7 Remove unnecessary diffs 2019-01-22 23:11:25 -05:00
Mendel Konikov d522a72d42 Add additional hebrew 2019-01-22 23:09:30 -05:00
Mendel Konikov d1a22f9dc8 Remove merge comments 2019-01-22 22:10:53 -05:00
Mendel Konikov 5ac3910687 Fix lint 2019-01-22 22:08:29 -05:00
Mendel Konikov d007c2295e Fix Permalink copy on iOS 2019-01-21 17:15:16 -05:00
Kim Gardner 7ebcc4c793 Merge pull request #2157 from coralproject/docs-update
added native comments pdf
2019-01-18 19:41:19 +00:00
immber 582d45bb21 moved pdf to wp and updated copy 2019-01-17 09:56:55 -08:00
immber 6ef5c3a42f added link to sidebar 2019-01-16 14:29:35 -08:00
immber 00883a3739 added link to sidebar 2019-01-16 14:25:52 -08:00
immber d7fb0056be added native comments pdf 2019-01-16 14:16:04 -08:00
Kim Gardner ad55d715f6 Merge branch 'master' into master 2019-01-16 21:24:52 +00:00
Kim Gardner 0a855cd3d4 Merge pull request #2154 from coralproject/losowsky-patch-1
Fixed blog url
2019-01-16 21:24:18 +00:00
Andrew Losowsky c02d2b9b4e Fixed blog url 2019-01-16 10:46:49 -05:00
Kim Gardner 68255bec70 Merge branch 'master' into master 2019-01-16 14:44:51 +00:00
immber 28edd98ff1 Merge pull request #2151 from coralproject/docs-update
added TALK_CONCURRENCY to adv config
2019-01-14 13:53:12 -08:00
immber d1b5409e44 Merge branch 'master' into docs-update 2019-01-14 13:46:43 -08:00
Wyatt Johnsonandimmber df5e863cc4 updated default phrasing to be consistent
Co-Authored-By: immber <jessiekrushing@gmail.com>
2019-01-14 13:45:28 -08:00
immber acc83e27ce added TALK_CONCURRENCY to adv config 2019-01-14 10:37:36 -08:00
Kim Gardner ca13fde87d Merge pull request #2148 from coralproject/docs-update
Docs update
2019-01-11 15:46:35 +00:00
immber f69a9c826d removed link to community from faq 2019-01-10 17:04:22 -08:00
immber 76ef0f7d52 pulled community link from readme 2019-01-10 10:55:20 -08:00
immber 8f54c1d3c3 fixed md typo 2019-01-09 12:38:33 -08:00
immber 83379b58a7 em that can not be opted out of 2019-01-08 16:46:49 -08:00
immber b0b5807c8b added scaling talk 2019-01-08 16:39:06 -08:00
immber c98d227411 updated notifications page 2019-01-08 15:12:44 -08:00
immber 83819df922 added talk-plugin-comment-count to plugins directory 2019-01-08 13:27:08 -08:00
immber db5bf9a676 added twitter faq 2019-01-08 10:04:39 -08:00
immber ba31dbbdf9 added logout wait time 2019-01-08 09:53:46 -08:00
immber 8f633fbe78 added comment count to checklist 2019-01-04 17:20:49 -08:00
immber 886222260d fb requires ssl 2019-01-04 16:55:56 -08:00
immber 2c3589cdac added cert to auth section 2019-01-04 16:49:16 -08:00
Ljuba Rankovic ed953b5e7e Change translation 2018-12-24 15:41:05 +01:00
Ljuba Rankovic 79d1f059c2 fixes 2018-12-22 16:34:16 +01:00
Kim Gardner dd0601a801 Merge pull request #2135 from coralproject/concurrency
Concurrency
2018-12-19 20:53:30 +00:00
Wyatt Johnson 9f7d485871 Update package.json 2018-12-19 20:39:25 +00:00
Wyatt Johnson ec816636e6 feat: added concurrency features 2018-12-19 12:40:08 -07:00
Kim Gardner 08f579225b Bump version 4.6.11 (#2130) 2018-12-17 16:34:43 +00:00
Kim Gardner 46987b6303 Merge pull request #2122 from oafx/oafx/qbb-localization
Add localization for QuestionBoxBuilder
2018-12-17 15:09:31 +00:00
Kim Gardner c2d07ec79f Merge branch 'master' into oafx/qbb-localization 2018-12-17 14:43:32 +00:00
Kim Gardner a98f5ef980 Merge pull request #2128 from oafx/oafx/fix/moderate-commentcontainer-wordbreak
fix minor ui/css bug in moderation/comment component caused by long links
2018-12-17 14:42:54 +00:00
Christian Ruttorf d6515e23f8 fix ui/css bug in moderation/comment component caused by long words/links 2018-12-16 08:48:21 +01:00
Ljuba Rankovic 282db6847a locale sr added 2018-12-15 09:40:53 +01:00
immber a3db05845d Merge pull request #2126 from coralproject/docs-update
Add Documentation Redirect - asset management to cms integration
2018-12-14 13:06:59 -08:00
Wyatt Johnson c8fc419cec Update _redirects 2018-12-14 20:46:57 +00:00
immber 677c8a0828 Merge branch 'master' into docs-update 2018-12-14 12:06:22 -08:00
immber 08146fd979 added redirect 2018-12-14 12:04:28 -08:00
Kim Gardner ab92fe3fe8 Merge branch 'master' into oafx/qbb-localization 2018-12-14 18:18:33 +00:00
immber 95435f0793 Docs update (#2120)
* merging in typo fixes from docs (rtd) branch

* revert generateIntrospectionResult.json

* revert git ignore

* added asset mgmt to prelaunch checklist

* updated cusotm plugin docker onbuild instructions

* add google to auth options

* add google to auth options

* added yarn watch to from source

* added dev links to quickstart

* typo on following

* fixed typos

* fixing links

* added lazy render and updated lazy load

* added version note

* renamed asset management to cms-integration

* added link to asset scraping
2018-12-14 17:05:52 +00:00
immber 7eed540671 added link to asset scraping 2018-12-13 13:10:14 -08:00
immber 374951bd33 renamed asset management to cms-integration 2018-12-13 13:04:01 -08:00
immber 42a694f56e Merge branch 'master' of https://github.com/coralproject/talk into docs-update 2018-12-13 12:52:18 -08:00
immber 1c997aea31 Merge branch 'docs-update' of https://github.com/coralproject/talk into docs-update 2018-12-13 12:51:58 -08:00
immber 44830f6cb1 added version note 2018-12-13 12:51:50 -08:00
Wyatt Johnson 22d263c6d2 Embed Optimizations (#2121)
* feat: refactored bridge

- Cleaned up implementation of bridge
- Removed unneeded babel-polyfill and webpack globals

* fix: fixed up some static headers

- bumped version

* fix: moved intersection observer out of chunk

* fix: fixed misplaced "!"
2018-12-13 20:41:40 +00:00
Christian Ruttorf 78ba82cc05 add localization for QuestionBoxBuilder title, add PropTypes for QuestionBoxBuilder 2018-12-13 17:42:50 +01:00
immber a6b9cc57ac Merge branch 'master' into docs-update 2018-12-12 12:46:04 -08:00
immber cd9cb56844 added lazy render and updated lazy load 2018-12-12 12:44:19 -08:00
immber 8b1b61f695 pulling changes from upstream 2018-12-10 14:26:58 -08:00
immber 99777d6b49 fixing links 2018-12-03 11:07:40 -08:00
immber 6f6da1fa25 merging changes from master 2018-12-03 09:34:25 -08:00
immber 2fac304163 fixed typos 2018-12-03 09:33:03 -08:00
immber 5ca7c092db Merge branch 'master' into docs-update 2018-11-30 18:05:35 -08:00
immber cfae6b5374 typo on following 2018-11-30 18:04:20 -08:00
immber 3668a2cffa added dev links to quickstart 2018-11-30 17:56:20 -08:00
immber 92681f52aa added yarn watch to from source 2018-11-30 17:39:02 -08:00
immber 847ded5bb4 add google to auth options 2018-11-30 17:19:27 -08:00
immber 76c959f790 add google to auth options 2018-11-30 17:11:42 -08:00
immber 4659392ad3 updated cusotm plugin docker onbuild instructions 2018-11-30 15:10:00 -08:00
immber e9cec73966 added asset mgmt to prelaunch checklist 2018-11-30 14:44:35 -08:00
immber bac0ee9c3b revert git ignore 2018-11-28 14:00:07 -08:00
immber 5ae293af89 revert generateIntrospectionResult.json 2018-11-28 13:58:56 -08:00
immber cce464b82a merging in typo fixes from docs (rtd) branch 2018-11-28 13:56:31 -08:00
Mendel Konikov 63400a455f Merge https://github.com/coralproject/talk 2018-05-10 20:53:41 -04:00
Mendel Konikov b7d8fb950b Merge master 2018-05-01 20:25:05 -04:00
Mendel Konikov 3915ffc4e0 Merge master 2018-04-10 20:10:21 -04:00
Mendel Konikov ac9899406a Merge 2018-03-07 12:02:01 -05:00
Mendel Konikov 13e1b288b3 Add more translations 2018-03-07 12:00:38 -05:00
Mendel Konikov 8c8f5d8fee Fix apostrophes 2018-03-07 12:00:38 -05:00
Mendel Konikov 35cf694b60 Add More hebrew translations 2018-03-07 12:00:38 -05:00
Mendel Konikov fa4dbfe9d0 Trigger build 2018-03-07 12:00:38 -05:00
Mendel Konikov 0ea08ccc9c Remove extra space 2018-03-07 12:00:38 -05:00
Mendel Konikov fd5bbe0234 Viewing options Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov b43f657c8d Toxic comments Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 9456c79bad Most respected Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 0cd18a458d More Hebrew translations 2018-03-07 12:00:38 -05:00
Mendel Konikov fdcd1404ef Most loved Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 741d5000dd Most liked Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 8f413852d9 Settings Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 420fc6daa7 Off-top Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov ad69086888 Moderation actions Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov f18c51dc68 Member since Hebrew Translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 8e1822af21 Google Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 5d3e8f3fb6 Flag Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 71288f543e Featured comments Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov a1890a0bfe Facebook Auth translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 1f17d71a76 Ignore user hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov fc8b9c3547 Like hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov 8ebb90c160 Subscriber hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov b0c230a1df Akismet Hebrew translations 2018-03-07 12:00:38 -05:00
Mendel Konikov be15734c77 Respect hebrew translations 2018-03-07 12:00:38 -05:00
Mendel Konikov 4478ced205 Auth Hebrew translations 2018-03-07 12:00:38 -05:00
Mendel Konikov 769437c8fb Add more translations 2018-03-07 12:00:38 -05:00
Mendel Konikov 5fde3235ad Add Hebrew template to all translation files 2018-03-07 12:00:38 -05:00
Mendel Konikov 2a8975711a Begin adding Hebrew translation 2018-03-07 12:00:38 -05:00
Mendel Konikov da56cd5b18 Add more translations 2018-03-06 13:02:03 -05:00
Mendel Konikov 90424b4766 Fix apostrophes 2018-02-26 16:39:28 -05:00
Mendel Konikov cf23bdba12 Add More hebrew translations 2018-02-26 16:37:40 -05:00
Mendel Konikov 0f983109e1 Trigger build 2018-02-26 16:14:16 -05:00
Mendel Konikov 8f7edde93f Remove extra space 2018-02-24 21:38:07 -05:00
Mendel Konikov 1946d023cd Viewing options Hebrew translation 2018-02-24 21:27:31 -05:00
Mendel Konikov 1e5f5848b2 Toxic comments Hebrew translation 2018-02-24 21:25:58 -05:00
Mendel Konikov 3cbf5fef07 Most respected Hebrew translation 2018-02-24 21:21:02 -05:00
Mendel Konikov 4f276ed944 More Hebrew translations 2018-02-24 21:19:43 -05:00
Mendel Konikov e69afc4e76 Most loved Hebrew translation 2018-02-24 21:17:02 -05:00
Mendel Konikov c4b46c28fe Most liked Hebrew translation 2018-02-24 21:15:51 -05:00
Mendel Konikov dac823f9c4 Settings Hebrew translation 2018-02-24 21:14:00 -05:00
Mendel Konikov 65deb1cfbe Off-top Hebrew translation 2018-02-24 21:12:04 -05:00
Mendel Konikov 828f200eb6 Moderation actions Hebrew translation 2018-02-24 21:10:23 -05:00
Mendel Konikov fe5c95c27a Member since Hebrew Translation 2018-02-24 21:06:24 -05:00
Mendel Konikov c7fad00b66 Google Hebrew translation 2018-02-24 21:05:14 -05:00
Mendel Konikov 5165d06d64 Flag Hebrew translation 2018-02-24 21:03:10 -05:00
Mendel Konikov e15275666e Featured comments Hebrew translation 2018-02-24 21:01:25 -05:00
Mendel Konikov 1dc8df4189 Facebook Auth translation 2018-02-24 20:43:18 -05:00
Mendel Konikov ffe83452cd Ignore user hebrew translation 2018-02-24 20:41:22 -05:00
Mendel Konikov 3c3603ee08 Like hebrew translation 2018-02-24 20:34:22 -05:00
Mendel Konikov f6e94b0cc2 Subscriber hebrew translation 2018-02-24 20:30:34 -05:00
Mendel Konikov c3f92e8c8d Akismet Hebrew translations 2018-02-24 20:27:52 -05:00
Mendel Konikov 84a121a1d9 Respect hebrew translations 2018-02-24 20:22:12 -05:00
Mendel Konikov f55f1341e1 Auth Hebrew translations 2018-02-24 20:19:20 -05:00
Mendel Konikov cccac6a8bc Add more translations 2018-02-23 12:17:13 -05:00
Mendel Konikov e775a9f092 Add Hebrew template to all translation files 2018-02-23 12:00:01 -05:00
Mendel Konikov 76237f42da Begin adding Hebrew translation 2018-02-23 11:29:28 -05:00
Mendel Konikov 6e942a1121 Fix typos in comments 2018-02-23 11:11:41 -05:00
371 changed files with 28881 additions and 2629 deletions
+26 -28
View File
@@ -1,4 +1,3 @@
# job_environment will setup the environment for any job being executed.
job_environment: &job_environment
NODE_ENV: test
@@ -54,7 +53,6 @@ integration_job: &integration_job
when: always
path: /tmp/circleci-test-results
version: 2
jobs:
# npm_dependencies will install the dependencies used by all other steps.
@@ -69,7 +67,7 @@ jobs:
- run:
name: Install dependencies
command: |
yarn global add node-gyp &&
yarn global add node-gyp@6.1.0 &&
yarn install --frozen-lockfile
- save_cache:
key: dependency-cache-{{ checksum "yarn.lock" }}
@@ -223,20 +221,17 @@ jobs:
# only execute on a deploy related job.
filter_deploy: &filter_deploy
filters:
branches:
only:
- master
- next
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/
# filter_develop will add the filters for a development related commit.
filter_develop: &filter_develop
filters:
branches:
ignore:
- master
- next
- /release\/.*/
workflows:
version: 2
@@ -258,10 +253,11 @@ workflows:
<<: *filter_develop
requires:
- npm_dependencies
- test_integration_chrome_local:
<<: *filter_develop
requires:
- build_assets
# TODO: uncomment when we revisit browserstack
# - test_integration_chrome_local:
# <<: *filter_develop
# requires:
# - build_assets
# TODO: uncomment when more reliable
# - test_integration_firefox_local:
# <<: *filter_develop
@@ -283,18 +279,19 @@ workflows:
<<: *filter_deploy
requires:
- npm_dependencies
- test_integration_chrome:
<<: *filter_deploy
requires:
- build_assets
- test_integration_firefox:
<<: *filter_deploy
requires:
- build_assets
- test_integration_edge:
<<: *filter_deploy
requires:
- build_assets
# TODO: uncomment when we revisit browserstack
# - test_integration_chrome:
# <<: *filter_deploy
# requires:
# - build_assets
# - test_integration_firefox:
# <<: *filter_deploy
# requires:
# - build_assets
# - test_integration_edge:
# <<: *filter_deploy
# requires:
# - build_assets
# TODO: uncomment when more reliable
# - test_integration_ie:
# <<: *filter_deploy
@@ -309,9 +306,10 @@ workflows:
requires:
- lint
- test_unit
- test_integration_chrome
- test_integration_firefox
- test_integration_edge
# TODO: uncomment when we revisit browserstack
# - test_integration_chrome
# - test_integration_firefox
# - test_integration_edge
# TODO: uncomment when more reliable
# - test_integration_ie
# - test_integration_safari
+1
View File
@@ -45,6 +45,7 @@ plugins/*
!plugins/talk-plugin-moderation-actions
!plugins/talk-plugin-notifications
!plugins/talk-plugin-notifications-category-featured
!plugins/talk-plugin-notifications-category-moderation-actions
!plugins/talk-plugin-notifications-category-reply
!plugins/talk-plugin-notifications-category-staff
!plugins/talk-plugin-notifications-digest-daily
+5 -4
View File
@@ -1,6 +1,7 @@
# Mozilla Community Participation Guidelines
# Vox Media Code of Conduct
## Version 2.3 - Updated May 16, 2017
## Introduction
The heart of Mozilla is people. We put people first and do our best to recognize, appreciate and respect the diversity of our global contributors. The Mozilla Project welcomes contributions from everyone who shares our goals and wants to contribute in a healthy and constructive manner within our community. As such, we have adopted this code of conduct and require all those who participate to agree and adhere to these Community Participation Guidelines in order to help us create a safe and positive community experience for all.
https://www.mozilla.org/en-US/about/governance/policies/participation/
This code of conduct governs the environment of the Vox Product team. We created it not because we anticipate bad behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team and because having a code provides us with clear avenues to correct our culture should it ever stray from that course. We make this code public in the hopes of contributing to the ongoing conversation about inclusion in the tech, design, and media communities and encourage other teams to fork it and make it their own. To our team, we commit to enforce and evolve this code as our team grows.
Read the rest here: http://code-of-conduct.voxmedia.com/
+3 -23
View File
@@ -18,7 +18,7 @@ There are at least three ways to contribute to Talk:
Conversation surrounding contributions begins in [issues](https://github.com/coralproject/talk/issues).
### When should I Create an Issue?
### When should I create an issue?
File an issue as soon as you have an idea of something you'd like to contribute. We would love to hear what you're thinking and help refine the idea to make it into the Talk ecosystem.
@@ -30,9 +30,9 @@ Please file issues if:
### What should I include?
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a story or this form:
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a user story in this form:
> As a [type of person] I'd like to be able to [do something] so that I can [get some result].
> As a [type of Coral user] I'd like to [do something] so that I can [get some result/value].
This exercise does two things:
@@ -71,23 +71,3 @@ If you'd like to discuss a contribution, please [file an issue](https://github.c
Talk's translations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repository.
## I want to contribute but I'm not sure what to do!
If you want to contribute but don't have a clear idea of exactly what that may be, here are some resources that may help:
### Product Roadmap
Please visit our product roadmap here: https://www.pivotaltracker.com/n/projects/1863625. If you'd like to take on any of our scheduled tasks we'd be forever grateful!
### Discussion Forum
If you'd like to discuss what we're up to, please visit or [community](https://community.coralproject.net/) or [contact us](https://coralproject.net/contact).
### Integrations
Have a favorite analytics engine? Data science service? CMS? Auth platform? Deployment platform or pipeline? Pet project? Consider building a plugin to integrate them!
### Favorite Features?
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
+2 -1
View File
@@ -20,7 +20,8 @@ ARG REVISION_HASH
ENV REVISION_HASH=${REVISION_HASH}
# Install app dependencies and build static assets.
RUN yarn global add node-gyp && \
# Pin node-gyp@6.1.0 as newer versions do not build on Node 8.
RUN yarn global add node-gyp@6.1.0 && \
yarn install --frozen-lockfile && \
yarn build && \
yarn cache clean
+1
View File
@@ -4,6 +4,7 @@ FROM coralproject/talk:latest
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
ONBUILD ARG TALK_ADDTL_REPLIES_ON_LOAD_MORE=999999
ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DISABLE_EMBED_POLYFILL=FALSE
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2018 Mozilla Foundation
Copyright 2019 Vox Media, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+9 -5
View File
@@ -1,8 +1,14 @@
# Talk &middot; [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) &middot; [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests)
---
> :construction: **NOTE:** We're currently building v5 of this platform, check it out in our [next branch](https://github.com/coralproject/talk/tree/next) :construction_worker_man: :construction_worker_woman:
---
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/talk).
Built with <3 by The Coral Project.
Built with <3 by The Coral Project, a part of [Vox Media](https://www.voxmedia.com/).
## Getting Started
@@ -22,13 +28,11 @@ For advanced configuration and usage of Talk, check out our [Configuration](http
## Versions & Upgrading
The current recommended release version is v4.5.0. ^4.5 (and all future even-numbered versions) are considered stable LTS versions. We recommend ^4.5 for use in production environments.
Check our Releases page for the latest recommended release version. [Releases](https://github.com/coralproject/talk/releases) All future even-numbered versions are considered stable LTS versions. We recommend the latest verified release for use in production environments.
## More Resources
- [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
- [Our Blog](https://blog.coralproject.net/)
- [Community Forums](https://community.coralproject.net/)
- [Our Blog](https://coralproject.net/blog)
- [Community Guides for Journalism](https://guides.coralproject.net/)
- [More About Us](https://coralproject.net/)
+26 -2
View File
@@ -11,7 +11,12 @@ const { HELMET_CONFIGURATION } = require('./config');
const { MOUNT_PATH } = require('./url');
const routes = require('./routes');
const debug = require('debug')('talk:app');
const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
const {
ENABLE_TRACING,
APOLLO_ENGINE_KEY,
PORT,
TRUST_PROXY,
} = require('./config');
const app = express();
@@ -58,7 +63,26 @@ if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
// Trust the first proxy in front of us, this will enable us to trust the fact
// that SSL was terminated correctly.
app.set('trust proxy', 1);
app.set(
'trust proxy',
(function() {
if (!TRUST_PROXY) {
return null;
}
const lowercase = TRUST_PROXY.toLowerCase();
if (lowercase === 'true' || lowercase === 'false') {
return lowercase === 'true';
}
const parsed = Number(TRUST_PROXY);
if (!isNaN(parsed)) {
return parsed;
}
return TRUST_PROXY;
})()
);
// Enable a suite of security good practices through helmet. We disable
// frameguard to allow crossdomain injection of the embed.
+17 -5
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env node
const throng = require('throng');
const { CONCURRENCY } = require('../config');
const { logger } = require('../services/logging');
const util = require('./util');
const program = require('commander');
const serve = require('../serve');
//==============================================================================
// Setting up the program command line arguments.
@@ -22,8 +24,18 @@ program
)
.parse(process.argv);
// Start serving.
serve(program).catch(err => {
console.error(err);
util.shutdown(1);
throng({
workers: CONCURRENCY,
start: i => {
logger.info({ workerID: i }, 'started worker');
// Load in the serve.
const serve = require('../serve');
// Start serving.
serve(program).catch(err => {
console.error(err);
util.shutdown(1);
});
},
});
+2 -1
View File
@@ -105,7 +105,8 @@ function printUserAsTable(user) {
Suspension: user.suspended
? `Until ${user.status.suspension.until}`
: false,
}
},
{ 'Always premod comments': user.alwaysPremod }
);
console.log(table.toString());
@@ -0,0 +1,14 @@
import {
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
} from '../constants/alwaysPremodUserDialog.js';
export const showAlwaysPremodUserDialog = ({ userId, username }) => ({
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
userId,
username,
});
export const hideAlwaysPremodUserDialog = () => ({
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
});
@@ -9,6 +9,8 @@ import {
SET_SEARCH_VALUE,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG,
SET_INDICATOR_TRACK,
@@ -61,6 +63,15 @@ export const setSearchValue = value => ({
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
// Always premod User Dialog
export const showAlwaysPremodUserDialog = user => ({
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
user,
});
export const hideAlwaysPremodUserDialog = () => ({
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
});
// Reject Username Dialog
export const showRejectUsernameDialog = user => ({
type: SHOW_REJECT_USERNAME_DIALOG,
+29 -15
View File
@@ -4,13 +4,15 @@ import {
FETCH_ASSETS_REQUEST,
FETCH_ASSETS_SUCCESS,
FETCH_ASSETS_FAILURE,
LOAD_MORE_ASSETS_REQUEST,
LOAD_MORE_ASSETS_SUCCESS,
LOAD_MORE_ASSETS_FAILURE,
SET_PAGE,
SET_SEARCH_VALUE,
SET_CRITERIA,
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE,
UPDATE_ASSETS,
} from '../constants/stories';
import t from 'coral-framework/services/i18n';
@@ -21,17 +23,14 @@ import t from 'coral-framework/services/i18n';
// Fetch a page of assets
// Get comments to fill each of the three lists on the mod queue
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
export const fetchAssets = (query = {}) => (dispatch, _, { rest2 }) => {
dispatch({ type: FETCH_ASSETS_REQUEST });
return rest(`/assets?${queryString.stringify(query)}`)
.then(({ result, page, count, limit, totalPages }) =>
return rest2(`/stories?${queryString.stringify(query)}`)
.then(({ edges, pageInfo }) =>
dispatch({
type: FETCH_ASSETS_SUCCESS,
assets: result,
page,
count,
limit,
totalPages,
edges,
pageInfo,
})
)
.catch(error => {
@@ -43,12 +42,31 @@ export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
});
};
export const loadMoreAssets = (query = {}) => (dispatch, _l, { rest2 }) => {
dispatch({ type: LOAD_MORE_ASSETS_REQUEST });
return rest2(`/stories?${queryString.stringify(query)}`)
.then(({ edges, pageInfo }) =>
dispatch({
type: LOAD_MORE_ASSETS_SUCCESS,
edges,
pageInfo,
})
)
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: LOAD_MORE_ASSETS_FAILURE, error: errorMessage });
});
};
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
dispatch({ type: UPDATE_ASSET_STATE_REQUEST });
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS, id, closedAt }))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
@@ -58,10 +76,6 @@ export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
});
};
export const updateAssets = assets => dispatch => {
dispatch({ type: UPDATE_ASSETS, assets });
};
export const setPage = page => ({
type: SET_PAGE,
page,
@@ -73,7 +73,7 @@ class AdminLogin extends React.Component {
this.setState({ requestPassword: true });
}}
>
{t('login.request_passowrd')}
{t('login.request_password')}
</a>
</p>
{loginMaxExceeded && (
@@ -0,0 +1,29 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 400px;
top: 50%;
transform: translateY(-50%);
padding: 20px;
border-radius: 4px;
}
.header {
color: black;
font-size: 1.5em;
font-weight: 500;
margin: 0 0 8px 0;
}
.subheader {
color: black;
font-size: 1.3em;
font-weight: 500;
margin: 0 0 8px 0;
}
.buttons {
margin-top: 8px;
margin-bottom: 6px;
text-align: right;
}
@@ -0,0 +1,68 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import { Dialog } from 'coral-ui';
import styles from './AlwaysPremodUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
class AlwaysPremodUserDialog extends React.Component {
handlePerform = () => {
this.props.onPerform();
};
render() {
const { open, onCancel, username, info } = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-always-premod-user-dialog')}
id="alwaysPremodUserDialog"
open={open}
onCancel={onCancel}
title={t('alwayspremoddialog.always_premod_user')}
>
<span className={styles.close} onClick={onCancel}>
×
</span>
<section>
<h2 className={styles.header}>
{t('alwayspremoddialog.always_premod_user')}
</h2>
<h3 className={styles.subheader}>
{t('alwayspremoddialog.are_you_sure', username)}
</h3>
<p className={styles.description}>{info}</p>
<div className={styles.buttons}>
<Button
className={cn('talk-always-premod-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised
>
{t('alwayspremoddialog.cancel')}
</Button>
<Button
className={cn('talk-always-premod-user-dialog-button-confirm')}
cStyle="black"
onClick={this.handlePerform}
raised
>
{t('alwayspremoddialog.yes_always_premod_user')}
</Button>
</div>
</section>
</Dialog>
);
}
}
AlwaysPremodUserDialog.propTypes = {
open: PropTypes.bool,
onPerform: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
username: PropTypes.string,
info: PropTypes.string,
};
export default AlwaysPremodUserDialog;
+4 -3
View File
@@ -26,7 +26,8 @@
max-width: 1280px;
margin: 0 auto;
background-color: #696969;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.header > div {
@@ -41,7 +42,7 @@
position: absolute;
top: 0;
right: 0;
width: 170px;
width: 200px;
height: 100%;
}
@@ -85,7 +86,7 @@
background-color: transparent;
transition: background-color 200ms;
font-weight: 100;
letter-spacing: .8;
letter-spacing: 0.8;
&:hover {
background-color: #404040;
+1 -1
View File
@@ -79,7 +79,7 @@ class SignIn extends React.Component {
className={styles.forgotPasswordLink}
onClick={this.handleForgotPasswordLink}
>
{t('login.request_passowrd')}
{t('login.request_password')}
</a>
</p>
</form>
@@ -137,6 +137,12 @@
display: inline-block;
}
.actionsMenuAlwaysPremod {
background-color: #F0B50B;
border-color: #F0B50B;
color: white;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
@@ -12,6 +12,7 @@ import {
isUsernameRejected,
isUsernameChanged,
isBanned,
isAlwaysPremod,
getKarma,
} from 'coral-framework/utils/user';
@@ -45,6 +46,7 @@ function getUserStatusArray(user) {
const statusMap = {
suspended: isSuspended,
banned: isBanned,
alwaysPremod: isAlwaysPremod,
usernameRejected: isUsernameRejected,
usernameChanged: isUsernameChanged,
};
@@ -68,6 +70,12 @@ class UserDetail extends React.Component {
username: this.props.root.user.username,
});
showAlwaysPremodUserDialog = () =>
this.props.showAlwaysPremodUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showRejectUsernameDialog = () =>
this.props.showRejectUsernameDialog({
userId: this.props.root.user.id,
@@ -107,6 +115,8 @@ class UserDetail extends React.Component {
return t('user_detail.suspended');
case 'banned':
return t('user_detail.banned');
case 'alwaysPremod':
return t('user_detail.always_premoded');
case 'usernameRejected':
return (
<span>
@@ -153,6 +163,7 @@ class UserDetail extends React.Component {
toggleSelectAll,
unbanUser,
unsuspendUser,
removeAlwaysPremodUser,
modal,
acceptComment,
rejectComment,
@@ -169,6 +180,7 @@ class UserDetail extends React.Component {
const banned = isBanned(user);
const suspended = isSuspended(user);
const alwaysPremod = isAlwaysPremod(user);
const usernameRejected = isUsernameRejected(user);
const usernameChanged = isUsernameChanged(user);
@@ -200,6 +212,7 @@ class UserDetail extends React.Component {
{
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
[styles.actionsMenuAlwaysPremod]: alwaysPremod,
},
'talk-admin-user-detail-actions-button'
)}
@@ -231,6 +244,21 @@ class UserDetail extends React.Component {
</ActionsMenuItem>
)}
{alwaysPremod ? (
<ActionsMenuItem
onClick={() => removeAlwaysPremodUser({ id: user.id })}
>
{t('user_detail.remove_always_premod')}
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showAlwaysPremodUserDialog}
>
{t('user_detail.always_premod')}
</ActionsMenuItem>
)}
{usernameChanged && (
<ActionsMenuItem onClick={this.goToReportedUsernames}>
{t('user_detail.username_needs_approval')}
@@ -476,8 +504,10 @@ UserDetail.propTypes = {
showRejectUsernameDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
showAlwaysPremodUserDialog: PropTypes.func,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
modal: PropTypes.bool,
rejectUsername: PropTypes.func.isRequired,
};
@@ -34,7 +34,7 @@
color: #063b9a;
text-decoration: none;
font-weight: 500;
letter-spacing: .5px;
letter-spacing: 0.5px;
margin-left: 10px;
font-size: 13px;
margin-left: 5px;
@@ -109,7 +109,7 @@
}
.external {
font-size: .7em;
font-size: 0.7em;
text-decoration: none;
color: #063b9a;
cursor: pointer;
@@ -119,7 +119,7 @@
&:hover {
text-decoration: underline;
opacity: .9;
opacity: 0.9;
}
> i {
@@ -139,3 +139,25 @@
margin-right: 5px;
}
}
.bodyHistoryToggle {
color: #666;
font-size: 12px;
line-height: 1px;
font-weight: 300;
text-decoration: underline;
&:hover {
cursor: pointer;
}
}
.editedCommentBody {
padding: 0 5px 5px 5px;
}
.editedComment {
margin-top: 6px;
font-weight: 300;
font-size: 16px;
overflow-wrap: break-word;
}
@@ -19,6 +19,8 @@ import TimeAgo from 'coral-framework/components/TimeAgo';
import t from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
state = { showingEditHistory: false };
approve = () =>
this.props.comment.status === 'ACCEPTED'
? null
@@ -29,6 +31,29 @@ class UserDetailComment extends React.Component {
? null
: this.props.rejectComment({ commentId: this.props.comment.id });
getBodyHistory = () => {
const bodyHistory = [];
const comment = this.props.comment;
for (let i = 0; i < comment.body_history.length - 1; i++) {
bodyHistory.push(
<div key={i} className={styles.editedComment}>
<div>
<TimeAgo className={styles.created} datetime={comment.created_at} />
</div>
<div className={styles.editedCommentBody}>
{comment.body_history[i].body}
</div>
</div>
);
}
return bodyHistory;
};
toggleEditHistory = () => {
this.setState({ showingEditHistory: !this.state.showingEditHistory });
};
render() {
const {
comment,
@@ -81,6 +106,14 @@ class UserDetailComment extends React.Component {
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
&nbsp;<span
className={styles.bodyHistoryToggle}
onClick={this.toggleEditHistory}
>
{this.state.showingEditHistory
? t('comment.hide_edit_history')
: t('comment.show_edit_history')}
</span>
</span>
) : null}
@@ -142,6 +175,14 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
{this.state.showingEditHistory ? (
<div className={styles.container}>
{t('comment.edit_history')}
{this.getBodyHistory()}
</div>
) : null}
<CommentDetails root={root} comment={comment} />
</li>
);
@@ -48,6 +48,10 @@ const buildActionResponse = (typename, created_at, until, status) => {
return status
? t('user_history.user_banned')
: t('user_history.ban_removed');
case 'AlwaysPremodStatusHistory':
return status
? t('user_history.user_always_premoded')
: t('user_history.always_premod_removed');
case 'SuspensionStatusHistory':
return until
? t('user_history.suspended', readableDuration(created_at, until))
@@ -0,0 +1,2 @@
export const SHOW_ALWAYS_PREMOD_USER_DIALOG = 'SHOW_ALWAYS_PREMOD_USER_DIALOG';
export const HIDE_ALWAYS_PREMOD_USER_DIALOG = 'HIDE_ALWAYS_PREMOD_USER_DIALOG';
@@ -14,6 +14,9 @@ export const FETCH_FLAGGED_COMMENTERS_FAILURE = `${prefix}_FETCH_FLAGGED_COMMENT
export const SHOW_BANUSER_DIALOG = `${prefix}_SHOW_BANUSER_DIALOG`;
export const HIDE_BANUSER_DIALOG = `${prefix}_HIDE_BANUSER_DIALOG`;
export const SHOW_ALWAYS_PREMOD_USER_DIALOG = `${prefix}_SHOW_ALWAYS_PREMOD_USER_DIALOG`;
export const HIDE_ALWAYS_PREMOD_USER_DIALOG = `${prefix}_HIDE_ALWAYS_PREMOD_USER_DIALOG`;
export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALOG`;
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
+4 -2
View File
@@ -4,12 +4,14 @@ export const FETCH_ASSETS_REQUEST = `${prefix}_FETCH_ASSETS_REQUEST`;
export const FETCH_ASSETS_SUCCESS = `${prefix}_FETCH_ASSETS_SUCCESS`;
export const FETCH_ASSETS_FAILURE = `${prefix}_FETCH_ASSETS_FAILURE`;
export const LOAD_MORE_ASSETS_REQUEST = `${prefix}_LOAD_MORE_ASSETS_REQUEST`;
export const LOAD_MORE_ASSETS_SUCCESS = `${prefix}_LOAD_MORE_ASSETS_SUCCESS`;
export const LOAD_MORE_ASSETS_FAILURE = `${prefix}_LOAD_MORE_ASSETS_FAILURE`;
export const UPDATE_ASSET_STATE_REQUEST = `${prefix}_UPDATE_ASSET_STATE_REQUEST`;
export const UPDATE_ASSET_STATE_SUCCESS = `${prefix}_UPDATE_ASSET_STATE_SUCCESS`;
export const UPDATE_ASSET_STATE_FAILURE = `${prefix}_UPDATE_ASSET_STATE_FAILURE`;
export const UPDATE_ASSETS = `${prefix}_UPDATE_ASSETS`;
export const SET_PAGE = `${prefix}_SET_PAGE`;
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
export const SET_CRITERIA = `${prefix}_SET_CRITERIA`;
@@ -0,0 +1,66 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import AlwaysPremodUserDialog from '../components/AlwaysPremodUserDialog';
import { hideAlwaysPremodUserDialog } from '../actions/alwaysPremodUserDialog';
import { withAlwaysPremodUser } from 'coral-framework/graphql/mutations';
import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
class AlwaysPremodUserDialogContainer extends Component {
alwaysPremodUser = async () => {
const { userId, alwaysPremodUser, hideAlwaysPremodUserDialog } = this.props;
await alwaysPremodUser({ id: userId });
hideAlwaysPremodUserDialog();
};
getInfo() {
let note = t('alwayspremoddialog.note_always_premod_user');
return t('alwayspremoddialog.note', note);
}
render() {
return (
<AlwaysPremodUserDialog
open={this.props.open}
onPerform={this.alwaysPremodUser}
onCancel={this.props.hideAlwaysPremodUserDialog}
username={this.props.username}
info={this.getInfo()}
/>
);
}
}
AlwaysPremodUserDialogContainer.propTypes = {
alwaysPremodUser: PropTypes.func.isRequired,
hideAlwaysPremodUserDialog: PropTypes.func,
open: PropTypes.bool,
username: PropTypes.string,
};
const mapStateToProps = ({
alwaysPremodUserDialog: { open, userId, username },
}) => ({
open,
userId,
username,
});
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideAlwaysPremodUserDialog,
},
dispatch
),
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
withAlwaysPremodUser
)(AlwaysPremodUserDialogContainer);
@@ -5,6 +5,7 @@ import Layout from '../components/Layout';
import Login from '../containers/Login';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import AlwaysPremodUserDialog from './AlwaysPremodUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import RejectUsernameDialog from './RejectUsernameDialog';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
@@ -40,6 +41,7 @@ class LayoutContainer extends React.Component {
toggleShortcutModal={toggleShortcutModal}
currentUser={this.props.currentUser}
>
<AlwaysPremodUserDialog />
<BanUserDialog />
<SuspendUserDialog />
<RejectUsernameDialog />
@@ -22,12 +22,14 @@ import {
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
withRemoveAlwaysPremodUser,
withRejectUsername,
withPostFlag,
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showAlwaysPremodUserDialog } from 'actions/alwaysPremodUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
import { showRejectUsernameDialog } from 'actions/rejectUsernameDialog';
@@ -153,6 +155,7 @@ UserDetailContainer.propTypes = {
selectedCommentIds: PropTypes.array,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired,
userId: PropTypes.string,
};
@@ -218,6 +221,17 @@ export const withUserDetailQuery = withQuery(
created_at
}
}
alwaysPremod {
status
history {
status
assigned_by {
id
username
}
created_at
}
},
username {
status
history {
@@ -280,6 +294,7 @@ const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
showBanUserDialog,
showAlwaysPremodUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
changeTab,
@@ -302,6 +317,7 @@ export default compose(
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
withRemoveAlwaysPremodUser,
withRejectUsername,
withPostFlag,
withRouter
@@ -42,6 +42,10 @@ export default withFragments({
status_history {
type
}
body_history {
body
created_at
}
${getSlotFragmentSpreads(slots, 'comment')}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
+78
View File
@@ -10,6 +10,9 @@ const userStatusFragment = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -125,6 +128,64 @@ export default {
});
},
}),
AlwaysPremodUser: ({
variables: {
input: { id },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
alwaysPremod: {
status: { $set: true },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
RemoveAlwaysPremodUser: ({
variables: {
input: { id },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
alwaysPremod: {
status: { $set: false },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
BanUser: ({
variables: {
input: { id },
@@ -154,6 +215,23 @@ export default {
});
},
}),
SetUserAlwaysPremodStatus: ({ variables: { status, id } }) => ({
updateQueries: {
TalkAdmin_Community: prev => {
if (!status) {
return prev;
}
const updated = update(prev, {
users: {
nodes: {
$apply: nodes => nodes.filter(node => node.id !== id),
},
},
});
return updated;
},
},
}),
UnbanUser: ({
variables: {
input: { id },
@@ -0,0 +1,29 @@
import {
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
} from '../constants/alwaysPremodUserDialog';
const initialState = {
open: false,
userId: null,
username: '',
};
export default function alwaysPremodUserDialog(state = initialState, action) {
switch (action.type) {
case SHOW_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
};
case HIDE_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
}
}
+2
View File
@@ -5,6 +5,7 @@ import moderation from './moderation';
import install from './install';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
import alwaysPremodUserDialog from './alwaysPremodUserDialog';
import rejectUsernameDialog from './rejectUsernameDialog';
import userDetail from './userDetail';
import ui from './ui';
@@ -14,6 +15,7 @@ export default {
banUserDialog,
configure,
suspendUserDialog,
alwaysPremodUserDialog,
userDetail,
stories,
community,
+53 -30
View File
@@ -3,56 +3,79 @@ import update from 'immutability-helper';
const initialState = {
assets: {
byId: {},
ids: [],
assets: [],
edges: [],
pageInfo: {},
},
searchValue: '',
criteria: {
asc: 'false',
filter: 'all',
},
loading: true,
loadingMore: false,
};
export default function assets(state = initialState, action) {
switch (action.type) {
case actions.FETCH_ASSETS_SUCCESS: {
const assets = action.assets.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {});
case actions.FETCH_ASSETS_REQUEST: {
return update(state, {
loading: { $set: true },
});
}
case actions.FETCH_ASSETS_FAILURE: {
return update(state, {
loading: { $set: false },
});
}
case actions.FETCH_ASSETS_SUCCESS: {
return update(state, {
loading: { $set: false },
assets: {
totalPages: { $set: action.totalPages },
page: { $set: action.page },
byId: { $set: assets },
count: { $set: action.count },
ids: { $set: Object.keys(assets) },
edges: { $set: action.edges },
pageInfo: { $set: action.pageInfo },
},
});
}
case actions.UPDATE_ASSET_STATE_REQUEST:
case actions.LOAD_MORE_ASSETS_REQUEST: {
return update(state, {
loadingMore: { $set: true },
});
}
case actions.LOAD_MORE_ASSETS_FAILURE: {
return update(state, {
loadingMore: { $set: false },
});
}
case actions.LOAD_MORE_ASSETS_SUCCESS: {
return update(state, {
loadingMore: { $set: false },
assets: {
edges: { $push: action.edges },
pageInfo: {
endCursor: { $set: action.pageInfo.endCursor },
hasNextPage: { $set: action.pageInfo.hasNextPage },
},
},
});
}
case actions.UPDATE_ASSET_STATE_SUCCESS:
const index = state.assets.edges.findIndex(
({ node: { id } }) => id === action.id
);
if (index < 0) {
return state;
}
return update(state, {
assets: {
byId: {
[action.id]: {
closedAt: { $set: action.closedAt },
edges: {
[index]: {
node: {
closedAt: { $set: action.closedAt },
},
},
},
},
});
case actions.UPDATE_ASSETS:
return update(state, {
assets: {
assets: { $set: action.assets },
},
});
case actions.SET_PAGE:
return {
...state,
page: action.page,
};
case actions.SET_SEARCH_VALUE:
return {
...state,
+14
View File
@@ -6,6 +6,10 @@ import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog';
import {
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
} from '../constants/alwaysPremodUserDialog';
const initialState = {
modal: false,
@@ -23,6 +27,11 @@ export default function config(state = initialState, action) {
...state,
modal: true,
};
case SHOW_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
modal: true,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
@@ -33,6 +42,11 @@ export default function config(state = initialState, action) {
...state,
modal: false,
};
case HIDE_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
modal: false,
};
default:
return state;
}
@@ -78,9 +78,10 @@
}
.flaggedByReason {
font-size: 1tpx;
font-size: 14px;
margin: 0px;
line-height: 1.4;
word-break: break-word;
}
@@ -130,6 +130,12 @@ th.header:nth-child(2), th.header:nth-child(3) {
font-size: 0.98em;
}
.actionsMenuAlwaysPremod {
background-color: #F0B50B;
border-color: #F0B50B;
color: white;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
@@ -8,7 +8,11 @@ import LoadMore from '../../../components/LoadMore';
import PropTypes from 'prop-types';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import { isSuspended, isBanned } from 'coral-framework/utils/user';
import {
isSuspended,
isBanned,
isAlwaysPremod,
} from 'coral-framework/utils/user';
import { RadioGroup, Radio } from 'react-mdl';
import moment from 'moment';
import {
@@ -43,6 +47,8 @@ class People extends React.Component {
return 'Banned';
} else if (isSuspended(user)) {
return 'Suspended';
} else if (isAlwaysPremod(user)) {
return 'Always premoderated';
}
return '';
};
@@ -55,6 +61,10 @@ class People extends React.Component {
this.props.unbanUser(input);
};
removeAlwaysPremodUser = input => {
this.props.removeAlwaysPremodUser(input);
};
showBanUserDialog = input => {
this.props.showBanUserDialog(input);
};
@@ -63,6 +73,10 @@ class People extends React.Component {
this.props.showSuspendUserDialog(input);
};
showAlwaysPremodUserDialog = input => {
this.props.showAlwaysPremodUserDialog(input);
};
render() {
const {
onFilterChange,
@@ -109,6 +123,7 @@ class People extends React.Component {
<Radio value="active">{t('community.active')}</Radio>
<Radio value="suspended">{t('community.suspended')}</Radio>
<Radio value="banned">{t('community.banned')}</Radio>
<Radio value="alwaysPremod">{t('community.always_premod')}</Radio>
</RadioGroup>
<div className={styles.filterDetail}>
{t('community.filter_role')}
@@ -188,6 +203,9 @@ class People extends React.Component {
user
),
[styles.actionsMenuBanned]: isBanned(user),
[styles.actionsMenuAlwaysPremod]: isAlwaysPremod(
user
),
},
'talk-admin-user-detail-actions-button'
)}
@@ -232,6 +250,27 @@ class People extends React.Component {
{t('modqueue.ban_user_actions')}
</ActionsMenuItem>
)}
{isAlwaysPremod(user) ? (
<ActionsMenuItem
onClick={() =>
this.removeAlwaysPremodUser({ id: user.id })
}
>
Remove Always Premoderate
</ActionsMenuItem>
) : (
<ActionsMenuItem
onClick={() =>
this.showAlwaysPremodUserDialog({
userId: user.id,
username: user.username,
})
}
>
{t('modqueue.always_premod_user_actions')}
</ActionsMenuItem>
)}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
@@ -294,8 +333,10 @@ People.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
showAlwaysPremodUserDialog: PropTypes.func,
loadMore: PropTypes.func.isRequired,
};
@@ -8,9 +8,11 @@ import {
withUnbanUser,
withUnsuspendUser,
withSetUserRole,
withRemoveAlwaysPremodUser,
} from 'coral-framework/graphql/mutations';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
import { showAlwaysPremodUserDialog } from 'actions/alwaysPremodUserDialog';
import { viewUserDetail } from '../../../actions/userDetail';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
@@ -30,6 +32,7 @@ class PeopleContainer extends React.PureComponent {
active: { suspended: false, banned: false },
suspended: { suspended: true },
banned: { banned: true },
alwaysPremod: { alwaysPremod: true },
};
onFilterChange = filter => e =>
@@ -40,7 +43,6 @@ class PeopleContainer extends React.PureComponent {
getFilterState = () => {
const { role, status } = this.state;
return {
status: this.statusToQuery[status] || null,
role: role || null,
@@ -106,8 +108,10 @@ class PeopleContainer extends React.PureComponent {
setUserRole={this.props.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
showAlwaysPremodUserDialog={this.props.showAlwaysPremodUserDialog}
unbanUser={this.props.unbanUser}
unsuspendUser={this.props.unsuspendUser}
removeAlwaysPremodUser={this.props.removeAlwaysPremodUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
@@ -122,9 +126,11 @@ PeopleContainer.propTypes = {
setUserRole: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
showAlwaysPremodUserDialog: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
};
@@ -134,8 +140,11 @@ const LOAD_MORE_QUERY = gql`
$limit: Int
$cursor: Cursor
$value: String
$state: UserStateInput
) {
users(query: { value: $value, limit: $limit, cursor: $cursor }) {
users(
query: { value: $value, limit: $limit, cursor: $cursor, state: $state }
) {
hasNextPage
endCursor
nodes {
@@ -153,6 +162,9 @@ const LOAD_MORE_QUERY = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -187,6 +199,9 @@ const FILTER_QUERY = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -203,6 +218,7 @@ const mapDispatchToProps = dispatch =>
viewUserDetail,
showSuspendUserDialog,
showBanUserDialog,
showAlwaysPremodUserDialog,
},
dispatch
);
@@ -215,6 +231,7 @@ export default compose(
withSetUserRole,
withUnsuspendUser,
withUnbanUser,
withRemoveAlwaysPremodUser,
withQuery(
gql`
query TalkAdmin_Community_People {
@@ -236,6 +253,9 @@ export default compose(
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -14,6 +14,11 @@ class TechSettings extends React.Component {
this.props.updatePending({ updater });
};
updateCustomAdminCssUrl = event => {
const updater = { customAdminCssUrl: { $set: event.target.value } };
this.props.updatePending({ updater });
};
updateDomainlist = (listName, list) => {
this.props.updatePending({
updater: {
@@ -50,6 +55,14 @@ class TechSettings extends React.Component {
onChange={this.updateCustomCssUrl}
/>
</ConfigureCard>
<ConfigureCard title={t('configure.custom_admin_css_url')}>
<p>{t('configure.custom_admin_css_url_desc')}</p>
<input
className={styles.customCSSInput}
value={settings.customAdminCssUrl}
onChange={this.updateCustomAdminCssUrl}
/>
</ConfigureCard>
<Slot fill="adminTechSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
@@ -32,6 +32,7 @@ export default compose(
settings: gql`
fragment TalkAdmin_TechSettings_settings on Settings {
customCssUrl
customAdminCssUrl
domains {
whitelist
}
@@ -62,6 +62,7 @@
font-weight: 300;
margin-bottom: 8px;
overflow-wrap: break-word;
word-break: break-word;
}
.body {
@@ -102,7 +103,7 @@
color: #063b9a;
text-decoration: none;
font-weight: 500;
letter-spacing: .5px;
letter-spacing: 0.5px;
margin-left: 10px;
font-size: 13px;
margin-left: 5px;
@@ -110,14 +111,14 @@
border-bottom: solid 1px;
line-height: 16px;
&:hover {
opacity: .9;
opacity: 0.9;
cursor: pointer;
}
}
}
.username {
color: #393B44;
color: #393b44;
text-decoration: none;
cursor: pointer;
font-weight: 600;
@@ -126,12 +127,12 @@
margin-left: -5px;
transition: background-color 200ms ease;
&:hover {
background-color: #E0E0E0;
background-color: #e0e0e0;
}
}
.external {
font-size: .7em;
font-size: 0.7em;
text-decoration: none;
color: #063b9a;
cursor: pointer;
@@ -139,7 +140,7 @@
white-space: nowrap;
&:hover {
text-decoration: underline;
opacity: .9;
opacity: 0.9;
}
i {
font-size: 12px;
@@ -195,3 +196,25 @@
.commentContentFooter {
flex: 1;
}
.bodyHistoryToggle {
color: #666;
font-size: 12px;
line-height: 1px;
font-weight: 300;
text-decoration: underline;
&:hover {
cursor: pointer;
}
}
.editedCommentBody {
padding: 0 5px 5px 5px;
}
.editedComment {
margin-top: 6px;
font-weight: 300;
font-size: 16px;
overflow-wrap: break-word;
}
@@ -22,6 +22,8 @@ import t from 'coral-framework/services/i18n';
class Comment extends React.Component {
ref = null;
state = { showingEditHistory: false };
handleRef = ref => (this.ref = ref);
handleFocusOrClick = () => {
@@ -45,6 +47,30 @@ class Comment extends React.Component {
? null
: this.props.rejectComment({ commentId: this.props.comment.id });
getBodyHistory = () => {
const bodyHistory = [];
const comment = this.props.comment;
for (let i = 0; i < comment.body_history.length - 1; i++) {
bodyHistory.push(
<div key={i} className={styles.editedComment}>
<div>
<TimeAgo className={styles.created} datetime={comment.created_at} />
</div>
<div className={styles.editedCommentBody}>
{comment.body_history[i].body}
</div>
</div>
);
}
return bodyHistory;
};
toggleEditHistory = () => {
this.setState({ showingEditHistory: !this.state.showingEditHistory });
this.props.clearHeightCache && this.props.clearHeightCache();
};
componentDidUpdate(prev) {
if (!prev.selected && this.props.selected) {
this.ref.focus();
@@ -137,6 +163,14 @@ class Comment extends React.Component {
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
&nbsp;<span
className={styles.bodyHistoryToggle}
onClick={this.toggleEditHistory}
>
{this.state.showingEditHistory
? t('comment.hide_edit_history')
: t('comment.show_edit_history')}
</span>
</span>
) : null}
<div className={styles.adminCommentInfoBar}>
@@ -201,6 +235,14 @@ class Comment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
{this.state.showingEditHistory ? (
<div className={styles.container}>
{t('comment.edit_history')}
{this.getBodyHistory()}
</div>
) : null}
<CommentDetails
root={root}
comment={comment}
@@ -52,6 +52,10 @@ export default withFragments({
status_history {
type
}
body_history {
body
created_at
}
hasParent
${getSlotFragmentSpreads(slots, 'comment')}
...${getDefinitionName(CommentLabels.fragments.comment)}
@@ -77,7 +77,6 @@
display: block;
}
.hidden {
display: none;
}
@@ -95,3 +94,11 @@
text-align: left;
}
.loadMore {
margin: 20px auto;
text-align: center;
}
.loadMoreSpinner {
margin: 20px auto 50px;
}
@@ -2,11 +2,14 @@ import React, { Component } from 'react';
import cn from 'classnames';
import { Link } from 'react-router';
import PropTypes from 'prop-types';
import { Dropdown, Option, Paginate, Icon } from 'coral-ui';
import { DataTable, TableHeader, RadioGroup, Radio } from 'react-mdl';
import t from 'coral-framework/services/i18n';
import styles from './Stories.css';
import { Dropdown, Option, Icon, Spinner } from 'coral-ui';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import LoadMore from 'coral-admin/src/components/LoadMore';
import styles from './Stories.css';
class Stories extends Component {
renderDate = date => {
@@ -39,10 +42,11 @@ class Stories extends Component {
filter,
onSearchChange,
onSettingChange,
onPageChange,
asc,
onLoadMore,
loading,
loadingMore,
} = this.props;
const rows = assets.ids.map(id => assets.byId[id]);
const rows = assets.edges.map(({ node }) => node);
return (
<div className={cn('talk-admin-stories', styles.container)}>
@@ -74,60 +78,58 @@ class Stories extends Component {
<Radio value="open">{t('streams.open')}</Radio>
<Radio value="closed">{t('streams.closed')}</Radio>
</RadioGroup>
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
<RadioGroup
name="sortBy"
value={asc}
childContainer="div"
onChange={onSettingChange('asc')}
className={styles.radioGroup}
>
<Radio value="false">{t('streams.newest')}</Radio>
<Radio value="true">{t('streams.oldest')}</Radio>
</RadioGroup>
</div>
{rows.length ? (
<div className={styles.mainContent}>
<DataTable className={styles.streamsTable} rows={rows}>
<TableHeader name="title" cellFormatter={this.renderTitle}>
{t('streams.article')}
</TableHeader>
<TableHeader
name="publication_date"
cellFormatter={this.renderDate}
>
{t('streams.pubdate')}
</TableHeader>
<TableHeader
name="closedAt"
cellFormatter={this.renderStatus}
className={styles.status}
>
{t('streams.status')}
</TableHeader>
</DataTable>
<Paginate
pageCount={assets.totalPages}
page={assets.page - 1}
onPageChange={onPageChange}
/>
</div>
) : (
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
)}
<div className={styles.mainContent}>
{loading ? (
<Spinner />
) : rows.length ? (
<div>
<DataTable className={styles.streamsTable} rows={rows}>
<TableHeader name="title" cellFormatter={this.renderTitle}>
{t('streams.article')}
</TableHeader>
<TableHeader
name="publication_date"
cellFormatter={this.renderDate}
>
{t('streams.pubdate')}
</TableHeader>
<TableHeader
name="closedAt"
cellFormatter={this.renderStatus}
className={styles.status}
>
{t('streams.status')}
</TableHeader>
</DataTable>
{loadingMore ? (
<Spinner className={styles.loadMoreSpinner} />
) : (
<LoadMore
showLoadMore={assets.pageInfo.hasNextPage}
loadMore={onLoadMore}
className={styles.loadMore}
/>
)}
</div>
) : (
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
)}
</div>
</div>
);
}
}
Stories.propTypes = {
loading: PropTypes.bool,
loadingMore: PropTypes.bool,
assets: PropTypes.object,
searchValue: PropTypes.string,
asc: PropTypes.string,
filter: PropTypes.string,
onLoadMore: PropTypes.func.isRequired,
onStatusChange: PropTypes.func.isRequired,
onSearchChange: PropTypes.func.isRequired,
onPageChange: PropTypes.func.isRequired,
onSettingChange: PropTypes.func.isRequired,
};
@@ -5,9 +5,9 @@ import { bindActionCreators } from 'redux';
import {
fetchAssets,
updateAssetState,
setPage,
setSearchValue,
setCriteria,
loadMoreAssets,
} from 'coral-admin/src/actions/stories';
import Stories from '../components/Stories';
@@ -35,13 +35,11 @@ class StoriesContainer extends Component {
};
fetchAssets = query => {
const { searchValue, asc, filter, limit } = this.props;
const { searchValue: value, filter } = this.props;
this.props.fetchAssets({
value: searchValue,
asc,
value,
filter,
limit,
...query,
});
};
@@ -51,30 +49,39 @@ class StoriesContainer extends Component {
try {
await updateAssetState(id, closeStream ? Date.now() : null);
this.fetchAssets();
} catch (err) {
console.error(err);
}
};
onPageChange = ({ selected }) => {
const page = selected + 1;
this.props.setPage(page);
this.fetchAssets({ page });
onLoadMore = async () => {
const {
searchValue: value,
filter,
assets: {
pageInfo: { endCursor: cursor },
},
loadMoreAssets,
} = this.props;
try {
await loadMoreAssets({ cursor, value, filter });
} catch (err) {
console.error(err);
}
};
render() {
return (
<Stories
loading={this.props.loading}
loadingMore={this.props.loadingMore}
assets={this.props.assets}
searchValue={this.props.searchValue}
asc={this.props.asc}
filter={this.props.filter}
limit={this.props.limit}
onPageChange={this.onPageChange}
onStatusChange={this.onStatusChange}
onSettingChange={this.onSettingChange}
onSearchChange={this.onSearchChange}
onLoadMore={this.onLoadMore}
/>
);
}
@@ -82,34 +89,34 @@ class StoriesContainer extends Component {
const mapStateToProps = ({ stories }) => ({
assets: stories.assets,
loading: stories.loading,
loadingMore: stories.loadingMore,
searchValue: stories.searchValue,
asc: stories.criteria.asc,
filter: stories.criteria.filter,
limit: stories.criteria.limit,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
setPage,
setCriteria,
setSearchValue,
fetchAssets,
updateAssetState,
loadMoreAssets,
},
dispatch
);
StoriesContainer.propTypes = {
loading: PropTypes.bool,
loadingMore: PropTypes.bool,
assets: PropTypes.object,
searchValue: PropTypes.string,
asc: PropTypes.string,
filter: PropTypes.string,
limit: PropTypes.number,
setPage: PropTypes.func.isRequired,
setCriteria: PropTypes.func.isRequired,
setSearchValue: PropTypes.func.isRequired,
fetchAssets: PropTypes.func.isRequired,
loadMoreAssets: PropTypes.func.isRequired,
updateAssetState: PropTypes.func.isRequired,
};
@@ -1,8 +0,0 @@
import LinkifyIt from 'linkify-it';
import tlds from 'tlds';
export function createLinkify() {
const linkify = new LinkifyIt();
linkify.tlds(tlds);
return linkify;
}
@@ -5,6 +5,9 @@ const prefix = 'TALK_EMBED_STREAM';
export const ADDTL_COMMENTS_ON_LOAD_MORE = parseInt(
defaultTo(process.env.TALK_ADDTL_COMMENTS_ON_LOAD_MORE, '10')
);
export const ADDTL_REPLIES_ON_LOAD_MORE = parseInt(
defaultTo(process.env.TALK_ADDTL_REPLIES_ON_LOAD_MORE, '999999')
);
export const ASSET_COMMENTS_LOAD_DEPTH = parseInt(
defaultTo(process.env.TALK_ASSET_COMMENTS_LOAD_DEPTH, '10')
);
@@ -81,7 +81,7 @@ class EmbedContainer extends React.Component {
},
}
) => {
notify('info', t('your_username_has_been_rejected'));
notify('info', t('your_username_has_been_rejected_not_in_line'));
props.updateStatus(state.status);
},
},
@@ -175,6 +175,9 @@ const USER_BANNED_SUBSCRIPTION = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -196,6 +199,9 @@ const USER_SUSPENDED_SUBSCRIPTION = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -217,6 +223,9 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -253,6 +262,9 @@ const EMBED_QUERY = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -1,6 +1,7 @@
import React from 'react';
import QuestionBox from '../../../components/QuestionBox';
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './QuestionBoxBuilder.css';
import { Icon } from 'coral-ui';
@@ -12,6 +13,7 @@ const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote'];
class QuestionBoxBuilder extends React.Component {
render() {
const {
title,
questionBoxIcon,
questionBoxContent,
onContentChange,
@@ -20,7 +22,7 @@ class QuestionBoxBuilder extends React.Component {
return (
<div className={styles.root}>
<h4>Include an Icon</h4>
<h4>{title}</h4>
<ul className={styles.iconList}>
{icons.map(item => {
@@ -53,4 +55,12 @@ class QuestionBoxBuilder extends React.Component {
}
}
QuestionBoxBuilder.propTypes = {
title: PropTypes.string,
questionBoxIcon: PropTypes.string,
questionBoxContent: PropTypes.string,
onContentChange: PropTypes.func,
onIconChange: PropTypes.func,
};
export default QuestionBoxBuilder;
@@ -69,6 +69,7 @@ class Settings extends React.Component {
{questionBoxEnable && (
<div className={styles.questionBoxContainer}>
<QuestionBoxBuilder
title={t('configure.include_an_icon')}
questionBoxIcon={questionBoxIcon}
questionBoxContent={questionBoxContent}
onIconChange={onQuestionBoxIconChange}
@@ -48,7 +48,7 @@ class ChangeUsername extends Component {
return (
<RestrictedMessageBox>
<div className="talk-change-username">
<span>{t('framework.edit_name.msg')}</span>
<span>{t('framework.edit_name.rejected')}</span>
<div className={styles.alert}>{alert}</div>
<label htmlFor="username" className="screen-reader-text">
{t('framework.edit_name.label')}
@@ -6,6 +6,7 @@ import { bindActionCreators } from 'redux';
import {
ADDTL_COMMENTS_ON_LOAD_MORE,
ASSET_COMMENTS_LOAD_DEPTH,
ADDTL_REPLIES_ON_LOAD_MORE,
THREADING_LEVEL,
} from '../../../constants/stream';
import {
@@ -152,7 +153,9 @@ class StreamContainer extends React.Component {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: parent_id ? 999999 : ADDTL_COMMENTS_ON_LOAD_MORE,
limit: parent_id
? ADDTL_REPLIES_ON_LOAD_MORE
: ADDTL_COMMENTS_ON_LOAD_MORE,
cursor: comment.replies.endCursor,
parent_id,
asset_id: this.props.asset.id,
@@ -398,6 +401,9 @@ const fragments = {
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
+359
View File
@@ -0,0 +1,359 @@
import queryString from 'querystringify';
import URLSearchParams from '@ungap/url-search-params';
import pym from 'pym.js';
import EventEmitter from 'eventemitter2';
import { buildUrl } from 'coral-framework/utils/url';
import SnackBar from './SnackBar';
import onIntersect from './onIntersect';
import {
createStorage,
connectStorageToPym,
} from 'coral-framework/services/storage';
// Rebuild the origin if it isn't defined. This is our poor-mans polyfill
// for the location APIs.
if (!window.location.origin) {
window.location.origin = `${window.location.protocol}//${
window.location.hostname
}${window.location.port ? `:${window.location.port}` : ''}`;
}
const NOTIFICATION_OFFSET = 200;
// Ensure there is a trailing slash.
function ensureEndSlash(p) {
return p.match(/\/$/) ? p : `${p}/`;
}
// Build the URL to load in the pym iframe.
function buildStreamIframeUrl(talkBaseUrl, query) {
let url = talkBaseUrl + 'embed/stream?';
url += queryString.stringify(query);
return url;
}
// detectAssetURL will try to grab the canonical url from the head, if it isn't
// available, source the url from the current one.
function detectAssetURL() {
try {
// Try to get the url from the canonical tag on the page.
return document.querySelector('link[rel="canonical"]').href;
} catch (e) {
window.console.warn(
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
);
return window.location.origin + window.location.pathname;
}
}
function buildQuery({ asset_id, asset_url }) {
// Compose the query to send down to the Talk API so it knows what to load.
const query = {};
// Parse the url parameters to extract some of the information.
const search = new URLSearchParams(window.location.search);
// Pull the Comment ID out of the query string.
const commentID = search.get('commentId') || search.get('commentID');
if (commentID) {
query.comment_id = commentID;
}
// Insert the asset_id into the query.
if (asset_id) {
query.asset_id = asset_id;
}
// If the asset_url is defined, use it, otherwise, detect it.
if (asset_url) {
query.asset_url = asset_url;
} else {
query.asset_url = detectAssetURL();
}
return query;
}
// Get dimensions of viewport.
function viewportDimensions() {
let target = window;
let prefix = 'inner';
if (!('innerWidth' in window)) {
prefix = 'client';
target = document.documentElement || document.body;
}
return {
width: target[`${prefix}Width`],
height: target[`${prefix}Height`],
};
}
function parseAMPHash(opts) {
const result = { ...opts };
const query = window.location.hash.length && window.location.hash.substr(1);
if (query) {
const parsed = queryString.parse(query);
if (parsed.asset_url && !result.asset_url) {
result.asset_url = parsed.asset_url;
}
if (parsed.asset_id && !result.asset_id) {
result.asset_id = parsed.asset_id;
}
}
return result;
}
export default class Bridge {
constructor(
element,
{
// Pull out the URLs used to setup Talk.
talk: talkBaseUrl,
talkStaticUrl = talkBaseUrl,
// Default the following to null.
events = null,
snackBarStyles = null,
onAuthChanged = null,
// Determine if we're in lazy mode or not. By default, the build argument
// will determine the lazy render status. This default is primarily used
// when the embed code cannot be changed, but control of the Talk serving
// domain is available.
lazy = process.env.TALK_DEFAULT_LAZY_RENDER === 'TRUE',
// Any additional options are extracted to be sent to the embed via the
// pym bridge.
amp,
...opts
}
) {
this.pym = null;
this.element = element;
this.amp = amp;
this.lazy = !amp && lazy;
// Parse amp hash.
this.opts = amp ? parseAMPHash(opts) : opts;
this.query = buildQuery(this.opts);
this.emitter = new EventEmitter({ wildcard: true });
this.snackBar = amp ? null : new SnackBar(snackBarStyles || {});
this.onAuthChanged = onAuthChanged;
this.talkBaseUrl = ensureEndSlash(talkBaseUrl);
this.talkStaticUrl = ensureEndSlash(talkStaticUrl);
// Store queued operations in a queue that can be processed once the stream
// is rendered.
this.queued = [];
// Attach to the events emitted by the pym parent.
if (events) {
events(this.emitter);
}
// Start the embed loading process.
if (this.lazy) {
// When the dom element containing the talk embed container is in view,
// render the stream with force turned on so that it skips this portion.
onIntersect(this.element, () => this.load());
} else {
// We aren't being lazy, load it now!
this.load();
}
}
ensureRendered() {
// Check to see if the pym bridge is created, and the embed is loaded.
if (this.pym === null) {
throw new Error('Stream Embed must be rendered first');
}
}
queueWhenRendered(callback) {
// Check to see if the queue is alive, if it isn't, run the callback now,
// otherwise, push the callback to be processed when the stream has loaded.
if (this.queued !== null) {
this.queued.push(callback);
} else {
callback();
}
}
setupPym() {
const url = buildStreamIframeUrl(this.talkBaseUrl, this.query);
this.pym = new pym.Parent(this.element.id, url, {
title: this.opts.title,
id: `${this.element.id}_iframe`,
name: `${this.element.id}_iframe`,
});
// NOTE: Workaround for iOS Safari which ignores `width` but respects `min-width` value.
this.pym.el.firstChild.style.width = '1px';
this.pym.el.firstChild.style.minWidth = '100%';
// Resize parent iframe height when child height changes
let cachedHeight;
this.pym.onMessage('height', height => {
if (height !== cachedHeight) {
this.pym.el.firstChild.style.height = `${height}px`;
cachedHeight = height;
if (this.amp) {
window.parent.postMessage(
{
sentinel: 'amp',
type: 'embed-size',
height,
},
'*'
);
}
}
});
// Send the config back over the pym bridge if requested.
this.pym.onMessage('getConfig', () => {
this.pym.sendMessage('config', JSON.stringify(this.opts));
});
// If the auth changes, and someone is listening for it, then re-emit it.
if (this.onAuthChanged) {
this.pym.onMessage('coral-auth-changed', message => {
this.onAuthChanged(message ? JSON.parse(message) : null);
});
}
// Remove the permalink comment id from the search.
this.pym.onMessage('coral-view-all-comments', () => {
const query = queryString.parse(location.search);
// Remove the commentId/commentID url param.
delete query.commentId;
delete query.commentID;
// Rebuild the search field without the commentId in it.
const search = queryString.stringify(query);
// Change the url.
window.history.replaceState(
{},
document.title,
buildUrl({ ...location, search })
);
});
// Remove the permalink comment id from the hash.
this.pym.onMessage('coral-view-comment', id => {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: id,
});
// Change the url to the permalink url.
window.history.replaceState(
{},
document.title,
buildUrl({ ...location, search })
);
});
// Helps child show notifications at the right scrollTop.
this.pym.onMessage('getPosition', () => {
const { height } = viewportDimensions();
let position = height + document.body.scrollTop;
if (position > NOTIFICATION_OFFSET) {
position = position - NOTIFICATION_OFFSET;
}
this.pym.sendMessage('position', position);
});
// When end-user clicks link in iframe, open it in parent context
this.pym.onMessage('navigate', url => {
// Open the new window, detach the opener, and focus on it.
const w = window.open(url, '_blank');
w.opener = null;
w.focus();
});
// Pass events from iframe to the event emitter.
this.pym.onMessage('event', raw => {
const { eventName, value } = JSON.parse(raw);
this.emitter.emit(eventName, value);
});
}
/**
* load will configure the pym parent if it hasn't already been, and setup the
* snackBar. If there are any queued operations, it will run them first.
*/
load() {
if (this.pym !== null) {
throw new Error('Stream Embed already rendered');
}
// Setup Pym.
this.setupPym();
if (this.snackBar) {
// Attach the snackBar to the pym parent and to the body of the page.
this.snackBar.attach(window.document.body, this.pym);
}
// If the user clicks outside the embed, then tell the embed.
document.addEventListener('click', this.handleClick.bind(this), true);
// Listens to ${name}Storage requests on pym and relay it to
// ${name}Storage.
['local', 'session'].forEach(name => {
connectStorageToPym(
createStorage(`${name}Storage`),
this.pym,
`${name}Storage`
);
});
// Process any queued operations.
const queued = this.queued;
this.queued = null;
queued.forEach(callback => callback());
}
handleClick() {
this.pym.sendMessage('click');
}
enablePluginsDebug() {
this.pym.sendMessage('enablePluginsDebug');
}
disablePluginsDebug() {
this.pym.sendMessage('disablePluginsDebug');
}
login(token) {
this.pym.sendMessage('login', token);
}
logout() {
this.pym.sendMessage('logout');
}
remove() {
// Remove the event listeners.
document.removeEventListener('click', this.handleClick.bind(this));
this.emitter.removeAllListeners();
// Remove the snackbar.
if (this.snackBar) {
this.snackBar.remove();
this.snackBar = null;
}
// Remove the pym parent.
this.pym.remove();
this.pym = null;
}
}
@@ -7,17 +7,25 @@ const DEFAULT_STYLE = {
willChange: 'transform, opacity',
transition: 'transform .35s cubic-bezier(.55,0,.1,1), opacity .35s',
pointerEvents: 'none',
padding: '12px 18px',
padding: '12px 18px 0',
color: '#fff',
borderRadius: '3px 3px 0 0',
textAlign: 'center',
borderRadius: '4px',
maxWidth: '400px',
left: '50%',
opacity: 0,
transform: 'translate(-50%, 20px)',
bottom: 0,
bottom: '20px',
boxSizing: 'border-box',
fontFamily: 'Helvetica, "Helvetica Neue", Verdana, sans-serif',
display: 'flex',
};
const CLOSE_STYLE = {
fontSize: '20px',
cursor: 'pointer',
marginLeft: '10px',
position: 'relative',
top: '-4px',
};
export default class Snackbar {
@@ -26,6 +34,20 @@ export default class Snackbar {
this.el = document.createElement('div');
this.el.id = 'coral-notif';
const closeButton = document.createElement('div');
closeButton.className = 'coral-notif-close';
for (let key in CLOSE_STYLE) {
closeButton.style[key] = CLOSE_STYLE[key];
}
closeButton.textContent = '×';
closeButton.onclick = () => this.clear();
this.snackbarText = document.createElement('div');
this.snackbarText.className = 'coral-notif-text';
this.snackbarText.style.paddingBottom = '12px';
this.el.appendChild(this.snackbarText);
this.el.appendChild(closeButton);
// Apply custom styles to the snackbar.
const style = Object.assign({}, DEFAULT_STYLE, customStyle);
for (let key in style) {
@@ -35,6 +57,7 @@ export default class Snackbar {
clear() {
this.el.style.opacity = 0;
this.el.style.pointerEvents = 'none';
}
alert(message) {
@@ -42,7 +65,7 @@ export default class Snackbar {
this.el.style.transform = 'translate(-50%, 20px)';
this.el.style.opacity = 0;
this.el.className = `coral-notif-${type}`;
this.el.textContent = text;
this.snackbarText.textContent = text;
if (this.timeout) {
clearTimeout(this.timeout);
@@ -51,10 +74,12 @@ export default class Snackbar {
this.timeout = setTimeout(() => {
this.el.style.transform = 'translate(-50%, 0)';
this.el.style.opacity = 1;
this.el.style.pointerEvents = 'auto';
this.timeout = setTimeout(() => {
this.el.style.opacity = 0;
}, 7000);
this.el.style.pointerEvents = 'none';
}, 15000);
}, 0);
}
-259
View File
@@ -1,259 +0,0 @@
/* global __webpack_public_path__ */ // eslint-disable-line no-unused-vars
import queryString from 'querystringify';
import pym from 'pym.js';
import EventEmitter from 'eventemitter2';
import { buildUrl } from 'coral-framework/utils/url';
import Snackbar from './Snackbar';
import onIntersect from './onIntersect';
import {
createStorage,
connectStorageToPym,
} from 'coral-framework/services/storage';
const NOTIFICATION_OFFSET = 200;
// Ensure there is a trailing slash.
function ensureEndSlash(p) {
return p.match(/\/$/) ? p : `${p}/`;
}
// Build the URL to load in the pym iframe.
function buildStreamIframeUrl(talkBaseUrl, query) {
let url = talkBaseUrl + 'embed/stream?';
url += queryString.stringify(query);
return url;
}
// Get dimensions of viewport.
function viewportDimensions() {
let e = window,
a = 'inner';
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
return {
width: e[`${a}Width`],
height: e[`${a}Height`],
};
}
export default class Stream {
constructor(el, talkBaseUrl, query, config) {
this.query = query;
// Extract the non-opts opts from the object.
const {
events = null,
snackBarStyles = null,
onAuthChanged = null,
talkStaticUrl = talkBaseUrl,
...opts
} = config;
this.onAuthChanged = onAuthChanged;
this.el = el;
this.talkBaseUrl = ensureEndSlash(talkBaseUrl);
this.talkStaticUrl = ensureEndSlash(talkStaticUrl);
this.opts = opts;
this.snackBar = new Snackbar(snackBarStyles || {});
this.emitter = new EventEmitter({ wildcard: true });
// Because we're loading chunks dynamically below, we need to point to the
// static URL.
//
// The __webpack_public_path__ can be referenced:
// https://webpack.js.org/configuration/output/#output-publicpath
//
__webpack_public_path__ = this.talkStaticUrl + 'static/';
// Attach to the events emitted by the pym parent.
if (events) {
events(this.emitter);
}
if (config.lazy || process.env.TALK_DEFAULT_LAZY_RENDER === 'TRUE') {
const renderOnIntersect = () => onIntersect(this.el, () => this.render());
if (!window.IntersectionObserver) {
// Include a polyfill for the intersection observer.
import(/* webpackChunkName: "intersection-observer" */ 'intersection-observer')
.then(() => {
// Polyfill applied.
renderOnIntersect();
})
.catch(e => {
console.error(e);
// Loading polyfill failed, just render it directly.
this.render();
});
} else {
// No need for polyfill.
renderOnIntersect();
}
} else {
this.render();
}
}
assertRendered() {
if (!this.pym) {
throw new Error('Stream Embed must be rendered first');
}
}
isRendered() {
return !!this.pym;
}
render() {
if (this.pym) {
throw new Error('Stream Embed already rendered');
}
this.pym = new pym.Parent(
this.el.id,
buildStreamIframeUrl(this.talkBaseUrl, this.query),
{
title: this.opts.title,
id: `${this.el.id}_iframe`,
name: `${this.el.id}_iframe`,
}
);
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
this.pym.el.firstChild.style.width = '1px';
this.pym.el.firstChild.style.minWidth = '100%';
// Resize parent iframe height when child height changes
let cachedHeight;
this.pym.onMessage('height', height => {
if (height !== cachedHeight) {
this.pym.el.firstChild.style.height = `${height}px`;
cachedHeight = height;
}
});
this.pym.onMessage('getConfig', () => {
this.pym.sendMessage('config', JSON.stringify(this.opts));
});
// If the auth changes, and someone is listening for it, then re-emit it.
if (this.onAuthChanged) {
this.pym.onMessage('coral-auth-changed', message => {
this.onAuthChanged(message ? JSON.parse(message) : null);
});
}
// Attach the snackbar to the pym parent and to the body of the page.
this.snackBar.attach(window.document.body, this.pym);
// Remove the permalink comment id from the hash.
this.pym.onMessage('coral-view-all-comments', () => {
const query = queryString.parse(location.search);
// Remove the commentId url param.
delete query.commentId;
const search = queryString.stringify(query);
const url = buildUrl({ ...location, search });
// Change the url.
window.history.replaceState({}, document.title, url);
});
// Remove the permalink comment id from the hash.
this.pym.onMessage('coral-view-comment', id => {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: id,
});
// Remove the commentId url param.
const url = buildUrl({ ...location, search });
// Change the url.
window.history.replaceState({}, document.title, url);
});
// Helps child show notifications at the right scrollTop.
this.pym.onMessage('getPosition', () => {
const { height } = viewportDimensions();
let position = height + document.body.scrollTop;
if (position > NOTIFICATION_OFFSET) {
position = position - NOTIFICATION_OFFSET;
}
this.pym.sendMessage('position', position);
});
// When end-user clicks link in iframe, open it in parent context
this.pym.onMessage('navigate', url => {
window.open(url, '_blank').focus();
});
// Pass events from iframe to the event emitter.
this.pym.onMessage('event', raw => {
const { eventName, value } = JSON.parse(raw);
this.emitter.emit(eventName, value);
});
// If the user clicks outside the embed, then tell the embed.
document.addEventListener('click', this.handleClick.bind(this), true);
// Listens to local storage requests on pym and relay it to local storage.
connectStorageToPym(
createStorage('localStorage'),
this.pym,
'localStorage'
);
// Listens to session storage requests on pym and relay it to session storage.
connectStorageToPym(
createStorage('sessionStorage'),
this.pym,
'sessionStorage'
);
}
enablePluginsDebug() {
this.assertRendered();
this.pym.sendMessage('enablePluginsDebug');
}
disablePluginsDebug() {
this.assertRendered();
this.pym.sendMessage('disablePluginsDebug');
}
login(token) {
this.assertRendered();
this.pym.sendMessage('login', token);
}
logout() {
this.assertRendered();
this.pym.sendMessage('logout');
}
remove() {
this.assertRendered();
// Remove the event listeners.
document.removeEventListener('click', this.handleClick.bind(this));
this.emitter.removeAllListeners();
// Remove the snackbar.
this.snackBar.remove();
// Remove the pym parent.
this.pym.remove();
}
handleClick() {
this.assertRendered();
this.pym.sendMessage('click');
}
}
-33
View File
@@ -1,33 +0,0 @@
export default class StreamInterface {
constructor(stream) {
this._stream = stream;
}
on(eventName, callback) {
return this._stream.emitter.on(eventName, callback);
}
off(eventName, callback) {
return this._stream.emitter.off(eventName, callback);
}
login(token) {
return this._stream.login(token);
}
logout() {
return this._stream.logout();
}
remove() {
return this._stream.remove();
}
enablePluginsDebug() {
return this._stream.enablePluginsDebug();
}
disablePluginsDebug() {
return this._stream.disablePluginsDebug();
}
}
+56 -91
View File
@@ -1,107 +1,72 @@
import URLSearchParams from 'url-search-params';
import Stream from './Stream';
import StreamInterface from './StreamInterface';
// Polyfill IntersectionObserver always, the alternative is that we have to also
// polyfill for Promise, which itself adds 1KB gziped, which means that the
// 4KB that the intersection observer really doesn't take up that much in terms
// of size.
import 'intersection-observer';
// Rebuild the origin if it isn't defined. This is our poor-mans polyfill
// for the location API's.
if (!window.location.origin) {
window.location.origin = `${window.location.protocol}//${
window.location.hostname
}${window.location.port ? `:${window.location.port}` : ''}`;
}
import Bridge from './Bridge';
import wrapBridge from './wrapBridge';
// parses the Asset URL from the config variable
function parseAssetURL() {
try {
// Try to get the url from the canonical tag on the page.
return document.querySelector('link[rel="canonical"]').href;
} catch (e) {
window.console.warn(
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
/**
* validateElement will throw an error when the element is not configured
* correctly or is not an HTMLElement at all.
*
* @param {HTMLElement} element the HTMLElement where the stream will be rendered
*/
function validateElement(element) {
if (!element) {
throw new Error(
'Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'
);
}
return window.location.origin + window.location.pathname;
if (typeof element !== 'object') {
throw new Error(
`Coral.Talk.render() expected HTMLElement but got ${element} (${typeof element})`
);
}
// Ensure element has an id, as pym can't directly accept the HTMLElement.
if (!element.id) {
element.id = `_${Math.random()}`;
}
}
export class Talk {
/**
* validateConfig is the configuration validation tool.
*
* @param {Object} config the configuration that will be used to setup Talk.
*/
function validateConfig(config) {
if (!config || typeof config !== 'object' || !config.talk) {
throw new Error(
'Coral.Talk.render() expected configuration with at least opts.talk as the Talk Base URL, none found'
);
}
}
export const Talk = {
/**
* Render a Talk stream
* @param {HTMLElement} element - Element to render the stream in
* @param {Object} config - Configuration options for talk
* @param {String} config.talk - Talk base URL
* @param {String} [config.title] - Title of Stream (rendered in iframe)
* @param {String} [config.asset_url] - Asset URL
* @param {String} [config.asset_id] - Asset ID
* @param {String} config.talk - URL to the Talk installation
* @param {String} [config.asset_id] - (optional) ID for the Asset
* @param {String} [config.asset_url] - (optional) URL where the Asset is located
* @param {String} [config.auth_token] - (optional) A jwt representing the session
* @param {String} [config.lazy] - (optional) If set the stream will only render lazily.
* @param {String} [config.lazy] - (optional) If set the stream will only render lazily
* @param {String} [config.talkStaticUrl] - (optional) Static URL used to serve Talk
* @param {Boolean} [config.amp] - (optional) Run Talk in AMP mode
* @return {Object}
*
* Example:
* ```
* const embed = Talk.render(document.getElementById('talkStreamEmbed'), config);
*
* // trigger a login with optional token.
* embed.login(token);
*
* // trigger a logout.
* embed.logout();
*
* // listen to events (in this case all events).
* embed.on('**', function(value) {
* console.log(this.event, value);
* });
* ```
*/
static render(element, config) {
if (!element) {
throw new Error(
'Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'
);
}
if (typeof element !== 'object') {
throw new Error(
`Coral.Talk.render() expected HTMLElement but got ${element} (${typeof element})`
);
}
if (!config || typeof config !== 'object' || !config.talk) {
throw new Error(
'Coral.Talk.render() expected configuration with at least opts.talk as the Talk Base URL, none found'
);
}
render: (element, config) => {
// Validate the element.
validateElement(element);
// Ensure el has an id, as pym can't directly accept the HTMLElement.
if (!element.id) {
element.id = `_${Math.random()}`;
}
// Validate the configuration.
validateConfig(config);
// Compose the query to send down to the Talk API so it knows what to load.
const query = {};
// Parse the url parameters to extract some of the information.
const search = new URLSearchParams(window.location.search);
// Pull the commentID out from the query params.
const commentID = search.get('commentId') || search.get('commentID');
if (commentID) {
query.comment_id = commentID;
}
// Extract the asset id from the options.
if (config.asset_id) {
query.asset_id = config.asset_id;
}
// Parse the Asset URL.
query.asset_url = config.asset_url;
if (!query.asset_url) {
query.asset_url = parseAssetURL();
}
// Create the new Stream.
const stream = new Stream(element, config.talk, query, config);
// Return the public interface for the stream.
return new StreamInterface(stream);
}
}
// Create the new Bridge, and wrap it up.
return wrapBridge(new Bridge(element, config));
},
};
+20 -10
View File
@@ -1,20 +1,30 @@
export default function onIntersect(el, callback) {
// Ensure that the intersection observer is available.
if (!IntersectionObserver) {
// tslint:disable-next-line:no-console
console.warn('IntersectionObserver not available');
window.console.warn('IntersectionObserver not available, rendering now');
callback();
return;
}
const options = {
rootMargin: '100px',
threshold: 1.0,
};
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
observer.disconnect();
callback();
// Create the Intersection Observer that will wait till the embed is within
// view and will then call the callback.
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
// Stop receiving intersection events.
observer.disconnect();
// Fire the callback.
callback();
}
},
{
rootMargin: '100px',
threshold: 1.0,
}
}, options);
);
// Start observing the element for visibility.
observer.observe(el);
}
+31
View File
@@ -0,0 +1,31 @@
export default bridge => {
// whenRendered will call the callback when the stream has rendered (or now if
// the stream is already rendered).
const whenRendered = callback => bridge.queueWhenRendered(callback);
// onlyWhenRendered will guard the callback unless the stream is rendered.
const onlyWhenRendered = callback => {
bridge.ensureRendered();
callback();
};
// Return the limited stream interface.
return {
// Directly map the on/off from the event emitter to the stream.
on: (eventName, callback) => bridge.emitter.on(eventName, callback),
off: (eventName, callback) => bridge.emitter.off(eventName, callback),
// Queue up the login operation until the stream has been rendered.
login: token => whenRendered(() => bridge.login(token)),
// Queue up the logout operation until the stream has been rendered.
logout: () => whenRendered(() => bridge.logout()),
// Remove the stream if it's already been rendered.
remove: () => onlyWhenRendered(() => bridge.remove()),
// Queue up the plugin config until the embed has rendered.
enablePluginsDebug: () => whenRendered(() => bridge.enablePluginsDebug()),
disablePluginsDebug: () => whenRendered(() => bridge.disablePluginsDebug()),
};
};
@@ -3,6 +3,7 @@ import { createDefaultResponseFragments } from '../utils';
// fragments defined here are automatically registered.
export default {
...createDefaultResponseFragments(
'AlwaysPremodUserResponse',
'BanUsersResponse',
'ChangeUsernameResponse',
'CloseAssetResponse',
@@ -14,6 +15,7 @@ export default {
'IgnoreUserResponse',
'ModifyTagResponse',
'PostFlagResponse',
'RemoveAlwaysPremodUserResponse',
'SetCommentStatusResponse',
'SetUsernameResponse',
'SetUsernameStatusResponse',
@@ -412,6 +412,48 @@ export const withSetUsername = withMutation(
}
);
export const withAlwaysPremodUser = withMutation(
gql`
mutation AlwaysPremodUser($input: AlwaysPremodUserInput!) {
alwaysPremodUser(input: $input) {
...AlwaysPremodUserResponse
}
}
`,
{
props: ({ mutate }) => ({
alwaysPremodUser: input => {
return mutate({
variables: {
input,
},
});
},
}),
}
);
export const withRemoveAlwaysPremodUser = withMutation(
gql`
mutation RemoveAlwaysPremodUser($input: RemoveAlwaysPremodUserInput!) {
removeAlwaysPremodUser(input: $input) {
...RemoveAlwaysPremodUserResponse
}
}
`,
{
props: ({ mutate }) => ({
removeAlwaysPremodUser: input => {
return mutate({
variables: {
input,
},
});
},
}),
}
);
export const withBanUser = withMutation(
gql`
mutation BanUser($input: BanUserInput!) {
+7 -7
View File
@@ -7,7 +7,6 @@ import { createReduxEmitter } from './events';
import { createRestClient } from './rest';
import thunk from 'redux-thunk';
import { loadTranslations } from './i18n';
import bowser from 'bowser';
import noop from 'lodash/noop';
import { BASE_PATH } from 'coral-framework/constants/url';
import { createPluginsService } from './plugins';
@@ -65,12 +64,7 @@ const getAuthToken = (store, storage) => {
}
return token;
} else if (
!bowser.safari &&
!bowser.ios &&
storage &&
storage.getItem('token')
) {
} else if (storage && storage.getItem('token')) {
// Use local storage auth tokens where there's a stable api.
return storage.getItem('token');
}
@@ -159,6 +153,11 @@ export async function createContext({
token,
});
const rest2 = createRestClient({
uri: `${BASE_PATH}api/v2`,
token,
});
const staticConfig = getStaticConfiguration();
let { LIVE_URI: liveUri, BASE_ORIGIN: origin } = staticConfig;
if (liveUri == null) {
@@ -193,6 +192,7 @@ export async function createContext({
plugins,
eventEmitter,
rest,
rest2,
graphql,
notification,
localStorage,
+5
View File
@@ -12,6 +12,7 @@ import 'moment/locale/da';
import 'moment/locale/de';
import 'moment/locale/es';
import 'moment/locale/fr';
import 'moment/locale/he';
import 'moment/locale/it';
import 'moment/locale/nl';
import 'moment/locale/pt-br';
@@ -23,6 +24,7 @@ import daTA from 'timeago.js/locales/da';
import deTA from 'timeago.js/locales/de';
import esTA from 'timeago.js/locales/es';
import frTA from 'timeago.js/locales/fr';
import heTA from 'timeago.js/locales/he';
import itTA from 'timeago.js/locales/it';
import nlTA from 'timeago.js/locales/nl';
import pt_BRTA from 'timeago.js/locales/pt_BR';
@@ -36,6 +38,7 @@ import da from '../../../locales/da.yml';
import de from '../../../locales/de.yml';
import es from '../../../locales/es.yml';
import fr from '../../../locales/fr.yml';
import he from '../../../locales/he.yml';
import it from '../../../locales/it.yml';
import nl_NL from '../../../locales/nl_NL.yml';
import pt_BR from '../../../locales/pt_BR.yml';
@@ -64,6 +67,7 @@ export const translations = {
...de,
...es,
...fr,
...he,
...it,
...nl_NL,
...pt_BR,
@@ -110,6 +114,7 @@ export function setupTranslations() {
ta.register('da', daTA);
ta.register('de', deTA);
ta.register('fr', frTA);
ta.register('he', heTA);
ta.register('it', itTA);
ta.register('nl_NL', nlTA);
ta.register('pt_BR', pt_BRTA);
+2 -2
View File
@@ -14,9 +14,9 @@ import {
export function createStore(reducers, middlewares = []) {
const enhancers = [applyMiddleware(...middlewares)];
if (window.devToolsExtension) {
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
// we can't have the last argument of compose() be undefined
enhancers.push(window.devToolsExtension());
enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__());
}
return reduxCreateStore(combineReducers(reducers), {}, compose(...enhancers));
+9
View File
@@ -35,6 +35,15 @@ export const isBanned = user => {
return get(user, 'state.status.banned.status');
};
/**
* isAlwaysPremod
* retrieves boolean based on the user premod status
*/
export const isAlwaysPremod = user => {
return get(user, 'state.status.alwaysPremod.status');
};
/**
* isUsernameRejected
* retrieves boolean based on the username status
+10 -2
View File
@@ -16,7 +16,8 @@
text-align: center;
vertical-align: middle;
line-height: 30px;
width: 30px;
min-width: 30px;
padding: 5px;
user-select: none;
}
@@ -24,7 +25,14 @@
font-size: 1.8em;
}
.active {
.page:hover {
background-color: #ababab;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
a {
color: white;
}
}
.active, .active:hover {
background-color: #696969;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
a {
+40
View File
@@ -43,6 +43,32 @@ const CONFIG = {
process.env.TALK_WHITELISTED_LANGUAGES &&
process.env.TALK_WHITELISTED_LANGUAGES.split(',').map(l => l.trim()),
// USERNAME_CAST_REGEXP defiles the regex expression that will be used to
// strip characters from a username during a username cast operation.
USERNAME_CAST_REGEXP: new RegExp(
process.env.USERNAME_CAST_REGEXP || '[^a-zA-Z_]',
'g'
),
// USERNAME_REPLACEMENT_CAST_REGEXP defiles the regex expression that will be
// used to replace characters with the replacement character during a username
// cast operation. First duplicates will be replaced, then
USERNAME_REPLACEMENT_CAST_REGEXP: new RegExp(
process.env.USERNAME_REPLACEMENT_CAST_REGEXP || ' +',
'g'
),
// USERNAME_REPLACEMENT_CHARACTER is the character used to replace other
// characters matching the USERNAME_REPLACEMENT_CAST_REGEXP.
USERNAME_REPLACEMENT_CHARACTER:
process.env.USERNAME_REPLACEMENT_CHARACTER || '_',
// USERNAME_VALIDATION_REGEX defines the allowed characters for a username in
// Talk.
USERNAME_VALIDATION_REGEX: new RegExp(
process.env.USERNAME_VALIDATION_REGEX || '^[A-Za-z0-9_]+$'
),
// When TRUE, it ensures that database indexes created in core will not add
// indexes.
CREATE_MONGO_INDEXES: process.env.DISABLE_CREATE_MONGO_INDEXES !== 'TRUE',
@@ -59,6 +85,9 @@ const CONFIG = {
// as report CSP violations.
ENABLE_STRICT_CSP: process.env.TALK_ENABLE_STRICT_CSP === 'TRUE',
// TRUST_PROXY allows control over the `trust proxy` configuration on express.
TRUST_PROXY: process.env.TALK_TRUST_PROXY || '1',
// LOGGING_LEVEL specifies the logging level used by the bunyan logger.
LOGGING_LEVEL: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes(
process.env.TALK_LOGGING_LEVEL
@@ -217,6 +246,9 @@ const CONFIG = {
// messages through the websocket to keep the socket alive.
KEEP_ALIVE: process.env.TALK_KEEP_ALIVE || '30s',
// CONCURRENCY is the number of workers that will serve traffic.
CONCURRENCY: parseInt(process.env.TALK_CONCURRENCY || '1'),
//------------------------------------------------------------------------------
// Cache configuration
//------------------------------------------------------------------------------
@@ -224,6 +256,14 @@ const CONFIG = {
CACHE_EXPIRY_COMMENT_COUNT:
process.env.TALK_CACHE_EXPIRY_COMMENT_COUNT || '1hr',
// EMBED_EXPIRY_TIME is the time that the embed will be cacheable for, sent as
// the max-age= directive on the Cache-Control header.
EMBED_EXPIRY_TIME: ms(process.env.TALK_EMBED_EXPIRY || '24hr'),
// EMBED_EXPIRY_TIME is the time that the rest of the static files will be
// cacheable for, sent as the max-age= directive on the Cache-Control header.
STATIC_EXPIRY_TIME: ms(process.env.TALK_STATIC_EXPIRY || '1w'),
//------------------------------------------------------------------------------
// Recaptcha configuration
//------------------------------------------------------------------------------
+150 -99
View File
@@ -6,7 +6,7 @@
title: Talk Documentation
subtitle:
description: >
Documentation and guides for Talk from The Coral Project.
Documentation and guides for Coral by Vox Media
google_analytics: UA-73335347-5
author:
language: en
@@ -47,7 +47,7 @@ highlight:
# per_page: Posts displayed per page. (0 = disable pagination)
# order_by: Posts order. (Order by date descending by default)
index_generator:
path: ''
path: ""
# Category & Tag
default_category: uncategorized
@@ -76,110 +76,161 @@ sidebar:
- title: GitHub
url: https://github.com/coralproject/
- title: Docker
url: https://hub.docker.com/r/coralproject/
- title: Roadmap
url: https://www.pivotaltracker.com/n/projects/1863625
url: https://hub.docker.com/r/coralproject/talk/
side:
- title: Installation
- title: Version 5+
children:
- title: Talk Quickstart
url: /
- title: Installation from Docker
url: /installation-from-docker/
- title: Installation from Source
url: /installation-from-source/
- title: Planning your Talk Architecture
url: /planning-architecture/
- title: Pre-Launch Checklist
url: /pre-launch-checklist/
- title: Configuration
- title: Getting started
children:
- title: Installing
url: /
- title: Configuration
url: /v5/configuration/
- title: Admin settings
url: /v5/administration/
- title: Developing
url: /v5/developing/
- title: CLI Tools
url: /v5/configuration-cli-tools/
- title: Integration
children:
- title: CMS integration
url: /v5/integrating/cms/
- title: SSO
url: /v5/integrating/sso/
- title: Other auth options
url: /v5/integration/auth/
- title: Custom CSS styles
url: /v5/css/
- title: Emails & notifications
url: /v5/notifications/
- title: GDPR
url: /v5/integrating/gdpr/
- title: Comment count
url: /v5/integrating/counts/
- title: Moderating via Slack
url: /v5/integrating/slack/
- title: Event tracking
url: /v5/integrating/events/
- title: SEO & iframes
url: /v5/seo/
- title: API
children:
- title: GraphQL Overview
url: /v5/api/overview/
- title: Version 4
children:
- title: Required Configuration
url: /configuration/
- title: Advanced Configuration
url: /advanced-configuration/
- title: Configuration with CLI tools
url: /configuration-cli-tools/
- title: Integrating
children:
- title: Authentication
url: /integrating/authentication/
- title: Asset Management
url: /integrating/asset-management/
- title: Asset Scraping
url: /integrating/asset-scraping/
- title: Configuring the Comment Stream
url: /integrating/configuring-comment-stream/
- title: Configuring the Admin
url: /integrating/configuring-admin/
- title: Notifications
url: /integrating/notifications/
- title: Event Tracking and Metrics
url: /integrating/event-tracking-metrics/
- title: Styling and CSS
url: /integrating/styling-css/
- title: Translations and i18n
url: /integrating/translations-i18n/
- title: GDPR Compliance
url: /integrating/gdpr/
- title: Product Guide
children:
- title: How Talk Works
url: /how-talk-works/
- title: Commenter Features
url: /commenter-features/
- title: Moderator Features
url: /moderator-features/
- title: User Roles in Talk
url: /roles/
- title: Trust
url: /trust/
- title: Toxic Comments
url: /toxic-comments/
- title: Configuring Talk
url: /configuring-talk/
- title: Plugins
children:
- title: Plugins Overview
url: /plugins/
- title: Plugin Directory
url: /plugins-directory/
- title: Plugin Recipes
url: /plugin-recipes/
- title: API
children:
- title: GraphQL Overview
url: /api/overview/
- title: GraphQL Reference
url: /api/graphql/
- title: Server Plugin API
url: /api/server/
- title: Client Plugin API
url: /api/client/
- title: Plugin Slots API
url: /api/slots/
- title: Tutorials
children:
- title: Creating a Basic Plugin
url: /building-basic-plugin/
- title: Customizing Plugins with Coral UI
url: /customizing-plugins-coral-ui/
- title: When You've Installed Talk
url: /when-youve-installed-talk/
- title: Installation
children:
- title: Talk Quickstart
url: /installation-quickstart
- title: Installation from Docker
url: /installation-from-docker/
- title: Installation from Source
url: /installation-from-source/
- title: Planning your Talk Architecture
url: /planning-architecture/
- title: Pre-Launch Checklist
url: /pre-launch-checklist/
- title: Configuration
children:
- title: Required Configuration
url: /configuration/
- title: Advanced Configuration
url: /advanced-configuration/
- title: Configuration with CLI tools
url: /configuration-cli-tools/
- title: Integrating
children:
- title: Authentication
url: /integrating/authentication/
- title: CMS Integration
url: /integrating/cms-integration/
- title: Asset Scraping
url: /integrating/asset-scraping/
- title: Configuring the Comment Stream
url: /integrating/configuring-comment-stream/
- title: Configuring the Admin
url: /integrating/configuring-admin/
- title: Notifications
url: /integrating/notifications/
- title: Event Tracking and Metrics
url: /integrating/event-tracking-metrics/
- title: Styling and CSS
url: /integrating/styling-css/
- title: Translations and i18n
url: /integrating/translations-i18n/
- title: GDPR Compliance
url: /integrating/gdpr/
- title: Accelerated Mobile Page
url: /integrating/amp/
- title: Product Guide
children:
- title: How Talk Works
url: /how-talk-works/
- title: Commenter Features
url: /commenter-features/
- title: Moderator Features
url: /moderator-features/
- title: User Roles in Talk
url: /roles/
- title: Trust
url: /trust/
- title: Toxic Comments
url: /toxic-comments/
- title: Configuring Talk
url: /configuring-talk/
- title: Plugins
children:
- title: Plugins Overview
url: /plugins/
- title: Plugin Directory
url: /plugins-directory/
- title: Plugin Recipes
url: /plugin-recipes/
- title: API
children:
- title: GraphQL Overview
url: /api/overview/
- title: GraphQL Reference
url: /api/graphql/
- title: Server Plugin API
url: /api/server/
- title: Client Plugin API
url: /api/client/
- title: Plugin Slots API
url: /api/slots/
- title: Tutorials
children:
- title: Creating a Basic Plugin
url: /building-basic-plugin/
- title: Customizing Plugins with Coral UI
url: /customizing-plugins-coral-ui/
- title: When You've Installed Talk
url: /when-youve-installed-talk/
- title: Embedding Talk in Native Mobile Apps
url: /embedding-in-native-mobile-apps/
- title: Migrating
children:
- title: Migrating from v3.x.x
url: /migration/3/
- title: Migrating to v4.0.0
url: /migration/4/
- title: Migrating to v4.1.0
url: /migration/4.1/
- title:
children:
- title: Migrating from v3.x.x
url: /migration/3/
- title: Migrating to v4.0.0
url: /migration/4/
- title: Migrating to v4.1.0
url: /migration/4.1/
- title: Migrating to v5.0.0
url: /migration/5.0/
- title: Migrating to v6.0.0
url: /migration/6.0/
- title: FAQ & Troubleshooting
children:
- title: FAQ
url: /faq/
- title: Troubleshooting Tips
url: /troubleshooting-tips/
- children:
- title: FAQ
url: /faq/
- title: Troubleshooting Tips
url: /troubleshooting-tips/
- title: Contact
url: /contact/
-3
View File
@@ -1,3 +0,0 @@
/ /talk/
/talk/reference/server/ /talk/api/server/
/talk/reference/graphql/ /talk/api/graphql/
+136
View File
@@ -0,0 +1,136 @@
---
title: Installing Coral
permalink: /
---
Online comments are broken. Our open-source commenting platform, Coral, reimagines
moderation, comment display, and conversation. Use Coral to add smarter, safer discussions to your site without giving away your data.
More than 130 newsrooms in 16 countries trust Coral to power their on-site communities, including The Washington Post, The Wall Street Journal, and Der Spiegel. [Read more about Coral here](https://coralproject.net/).
<div class="callout">
We offer hosting and support packages for Coral, as well as exclusive, customer-only features. [Contact us for more information.](https://coralproject.net/pricing/)
</div>
Built with ❤️ by Coral by [Vox Media](https://product.voxmedia.com/).
Try a test version of Coral by running it via a Heroku App:
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/coralproject/talk)
## Requirements
- MongoDB >=4.2
- Redis >=3.2
- NodeJS >=12
- NPM >=6.7
## Running
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:6](https://hub.docker.com/r/coralproject/talk) image.
### Docker
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:6
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:4.2
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 >=12, 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 >=4.2 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:4.2
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
```
Then start Coral with:
```bash
# Start the server in development mode.
npm run start:development
```
Then head on over to http://localhost:3000 to install Coral!
Note that if you want to run Coral in production from source, you'll need to
create a secret for signing that differs from the unsafe default. You can do
this by using something like `openssl rand -base64 32`. You can then place this
secret in a `.env` file in your `talk` directory as such:
```bash
SIGNING_SECRET=<replace me with something secret>
```
You can then run Coral with the production command instead:
```bash
# Start the server in production mode.
npm run start
```
+193
View File
@@ -0,0 +1,193 @@
---
title: Configuring Version 5+
permalink: /v5/configuration/
toc: true
---
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.
## Required Configuration Variables
### `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`)
### `SIGNING_SECRET`
The shared secret to use to sign JSON Web Tokens (JWT) with the selected signing
algorithm. (Default: `keyboard cat`)
Note: While there is a default for this so development can be simplified, Coral
will throw a runtime error in the event it's started with `NODE_ENV=production`
and the `SIGNING_SECRET="keyboard cat"` to prevent insecure installations. This
_must_ be set in production to something long and secure. You can use `openssl`
to help with that:
```sh
openssl rand -base64 45
```
## Advanced Configuration Variables
### `NODE_ENV`
Can be one of `production` or `development`. All production deployments should
use `production`. Defaults to `production` when ran with `npm run start`, or
with Docker deployments. Defaults to `development` when run with
`npm run start:development`.
### `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_ALGORITHM`
The signing algorithm to use for signing tokens. (Default `HS256`).
Supported algorithms are:
- `HS256`
- `HS384`
- `HS512`
- `RS256`
- `RS384`
- `RS512`
- `ES256`
- `ES384`
- `ES512`
### `LOCALE`
Specify the default locale to use for all requests without a locale specified. (Default `en-US`)
### `LOGGING_LEVEL`
The logging level that can be set to one of `fatal`,
`error`, `warn`, `info`, `debug`, or `trace`. (Default `info`)
### `FORCE_SSL`
Forces SSL in production by redirecting all HTTP requests to HTTPS, and sending
[HSTS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) headers. (Default `false`)
Coral does not provide or manage HTTPS certificates. If you want to enable
HTTPS, you must configure a proxy in front of Coral such as
[Caddy](https://caddyserver.com/).
**Troubleshooting**: If you are seeing redirect loops when trying to access
pages like the admin, you may need to configure [`TRUST_PROXY`](#trust-proxy) to
tell Coral which upstream proxies to trust.
**Warning**: When `FORCE_SSL=true`, Coral will send [HSTS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)
headers that will force web browsers to connect via HTTPS for the next 60 days.
By forcing SSL use you'll need to provide a secure connection to your Coral
instance for at least the next 60 days.
### `DISABLE_LIVE_UPDATES`
When `true`, the comment stream will not create a websocket connection to get
live comment updates. This applies across all tenants on the installation, and
cannot be turned back on via the interface. (Default `false`)
### `DISABLE_LIVE_UPDATES_TIMEOUT`
Stories that have not received a comment within this time frame will pause live
updates automatically. Once a single comment is received on these stories, live
updates will be re-enabled until the story sits idle for the timeout value,
parsed by [ms](https://www.npmjs.com/package/ms). (Default `2 weeks`)
### `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`)
### `ENABLE_GRAPHIQL`
When `true`, it will enable the interactive GraphQL developer environment at the
`/graphiql` route. This will also disable persisted (Default `false`)
Note: We do not recommend using this in production environments as it disables
many safety features used by the application to provide it.
### `METRICS_USERNAME`
The username for _Basic Authentication_ at the `/metrics` route. If not
provided with `METRICS_PASSWORD`, no authentication will be added to this route.
### `METRICS_PASSWORD`
The password for _Basic Authentication_ at the `/metrics` route. If not
provided with `METRICS_USERNAME`, no authentication will be added to this route.
### `METRICS_PORT`
[Prometheus](https://prometheus.io/docs/introduction/overview/) metrics are
provided at this port under `/metrics` route. (Default `9000`)
### `SCRAPE_TIMEOUT`
The request timeout for scraping operations, parsed by [ms](https://www.npmjs.com/package/ms).
(Default `10 seconds`)
### `SCRAPE_MAX_RESPONSE_SIZE`
The maximum size (in bytes) to allow for scraping responses. (Default `10e6`)
### `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.
### `TRUST_PROXY`
When provided, it configures the "trust proxy" settings for Express. If you are
encountering issues where urls in the administration are showing with a `http`
instead of `https`, you may need to set the `TRUST_PROXY` setting. Refer to
https://expressjs.com/en/guide/behind-proxies.html for possible values of this
configuration variable as it pertains to your setup.
### `WEBSOCKET_KEEP_ALIVE_TIMEOUT`
The interval that should be used to send keep alive messages over websocket to
keep the socket open, parsed by [ms](https://www.npmjs.com/package/ms).
(Default `30s`)
### `WORD_LIST_TIMEOUT`
The length of time that a given request to test a comment against a given word
list, parsed by [ms](https://www.npmjs.com/package/ms). (Default `100ms`)
### `PERSPECTIVE_TIMEOUT`
The length of time that a given request should wait for a response when
interacting with the Perspective API, parsed by [ms](https://www.npmjs.com/package/ms).
(Default `800ms`)
## Development Configuration Variables
The following configuration variables are only enabled when the server has been
started in development mode (where `NODE_ENV=development`).
### `DEV_PORT`
The port where the Webpack Development server is running on. (Default `8080`)
### `DISABLE_CLIENT_ROUTES`
Disables mounting of client routes for developing with Webpack Dev Server.
(Default `false`)
### `DISABLE_RATE_LIMITERS`
Used to disable the rate limiters used in Coral. (Default `false`)
+166
View File
@@ -0,0 +1,166 @@
---
title: Admin Settings
permalink: /v5/administration/
---
Upon completing the installation setup wizard, you will be able to login to Coral with the ADMIN user you created during setup.
## Moderation queues
All moderation queues are sorted by Most Recent at the top.
### Reported
This is the most urgent queue  these are comments that are still on the page, might be a problem, and you haven't made a moderation decision about them yet. These comments have either been Reported by a user, or reported by the system because they contain suspect words or phrases.
### Pending
These are comments that have been held back by the system for some reason. These are the reasons that comments are held back in Pending:
* The article or site is set to pre-moderate all comments (except for Staff/Moderator/Admin/Expert comments)
* The commenter has been manually set to 'Always Pre-moderate' by a moderator
* The comment has been ruled as 'Likely to be toxic' by the AI (optional)
* The commenter's recent behavior has included a higher percentage of rejected comment than the threshhold you set in Configure ('Recent History') (optional)
* The commenter has entered an identical comment twice in a row, anywhere on the site
* The site is set to hold back for review the first comments by new commenters (optional)
* The comment is marked as likely to spam by our spam detection system (optional)
Users who login with the ADMIN role are able to access the Configure tab, and from here change settings that control the functionality of your Coral instance.
### Unmoderated
These are comments of any kind - Reported, Published, Pending - that a moderator hasn't yet accepted or rejected.
### Approved
These are comments that a moderator has already approved. The name of the moderator is included on the card.
### Rejected
These are comments that a moderator has already rejected. The name of the moderator is included on the card.
There is also a clock icon in the top right of the page. When you click the icon, you will see a list of the comments that you have recently moderated, with links to review your decision.
## Configure Menu Settings
### Language
You can set the default language Coral uses in **Admin** > **Configure** > **General**.
You can see what languages Coral currently supports here: https://github.com/coralproject/talk/tree/master/src/locales
### Comment Stream Settings
Also located in **Admin** > **Configure** > **General** are the setting to control the comment stream. These settings are site-wide and will affect all of your comment streams.
#### **Community guidelines summary**
This will appear above the comments sitewide. You can format the text using Markdown.
#### **Sitewide closed comments message**
A message that will be displayed when comment streams are closed sitewide.
#### **Comment length**
Set minimum and maximum comment length requirements. Blank spaces at the beginning and the end of a comment will be trimmed. Some newsrooms weve worked with prefer a limit between 2000 and 5000
characters. Commenters will be alerted that they have gone over that number and wont be able to submit their comment until theyve edited it. This can be a useful tool to ensure commenters are concise with their comments.
#### **Comment editing**
Set a limit on how long commenters have to edit their comments sitewide. Edited comments are marked as (Edited) on the comment stream and the moderation panel.
#### **Closing comment streams**
Set comment streams to close after a defined period of time after a storys publication. When this feature is enabled, existing stories older than the defined period that have not been manually opened or closed will also be updated and set as closed. Manually opening or closing a story will permantenly override the auto-close functionality.
#### **Closed comment stream message**
A message to appear when a story is closed for commenting.
### Reactions
Coral comes with a `Respect` button out of the box. Why a “Respect” button, you
ask?
[Read more here](https://mediaengagement.org/research/engagement-buttons/).
You can also create your own custom reaction
buttons by modifying the Reaction label.
### Badges & Staff Member Badge
Badges differentiate users on the comment stream. By default, the `Staff` user badge displays when a commenter has an Admin, Moderator, or Staff role.
You can customize the badge label used for Staff roles by providing your own custom badge text. *i.e.: "NewsSite Team"*
Custom user badges are also available with SSO integrations. See [Single Sign On](/talk/v5/integrating/sso/)
## Moderation
These options can be found under **Admin** > **Configure** > **Moderation**.
### Pre-moderation
When pre-moderation is turned on, comments will not be published unless approved by a moderator.
### Recent History
Prevents repeat offenders from publishing comments without approval. When a commenter's rejection rate is above the threshold, their comments are sent to the Pending queue for moderation. This does not apply to Staff comments.
Rejection rate time period determines the look back period of commenter activity that will be considered in performing the calculation.
Rejection rate threshold is calculated by dividing
number of Rejected comments by (rejected comments + published comments) over the time period specified, and is displayed as a percentage. It does not include comments pending for toxicity, spam or pre-moderation, only moderated comments are considred.
### Toxic Comment Filter
Using Google's Perspective API, the Toxic Comment Filter warns users when comments exceed the predefined toxicity threshold. You can read more about Googles Perspective API and/or request an API key here: http://perspectiveapi.com/.
Comments with a toxicity score above the threshold will not be published and are placed in the Pending Queue for review by a moderator. Only if approved by a moderator, then the comment will be published.
If a comment exceeds the threshold, the commenter is warned that their comment may be toxic, and are given the chance to modify their comment before posting. If the revised comment is below the Toxicity Threshold, it is posted and displayed normally; however if the revised comment still exceeds the Toxicity Threshold, it is not displayed on the stream and instead is sent to the Pending queue for moderation.
Perspective API uses machine learning based on existing databases of accepted/rejected comments to guess the probability that a comment is abusive and/or toxic. English is the default language, but experimental models are available in multiple languages. Sites can also work with Jigsaw to create an individualized data set specifically trained on their own comment history. Read more about Corals take on toxicity [on our blog](https://coralproject.net/blog/toxic-avenging/).
### Spam Detection Filter
Enables spam detection from [Akismet](https://akismet.com/). Comments will be passed to the Akismet API for spam detection. If a comment is determined to be spam, it will prompt the user, indicating that the comment might be considered spam. If the user continues after this point with the still spam-like comment, the comment will be reported as containing spam, and sent for moderator approval.
*Note: Akismet is a premium service, charges may apply.*
## Banned & Suspect Words and Phrases
Located in **Admin** > **Configure** > **Banned words and phrases**.
**Banned words** are a list of words or phrases that will trigger a comment to be automatically **Rejected**.
Comments containing a word or phrase in the **Suspect Words** List are placed into the **Reported** queue for moderator review and are published (if comments are not pre-moderated).
Lists of words/phrases are not case sensitive, and are separated by new lines.
## Email SMTP Settings
These settings can be found under **Admin** > **Configure** > **Email**.
You can configure a **From Name** as well as a **From Email Address** to appear on all outgoing emails.
Coral will use your SMTP provider for all outgoing mail including user invites, email verifications, password resets, and notifications. Specify your SMTP provider's settings for Host, Port, and authentication to send emails from Coral.
The `Invite` (users) button on the Community tab will only be available if Email settings have been configured.
## Advanced
Located in **Admin** > **Configure** > **Advanced**.
### Embed Code
This is the unique script that is used to embed Coral comment streams on your website. For more information about using your Embed code see [CMS Integration](/talk/v5/integrating/cms/).
### Custom CSS
The link to your custom stylesheet. This will override any default styles, so you can style the comment embed to match your site.
### Live Updates
When enabled, there will be real-time loading and updating of comments via subscriptions
(specifically GraphQL Subscriptions). When disabled, users will have to refresh the page to see new comments.
Coral enables this via “Load More” buttons for both top-level comments (this
button appears at the top of the stream), and within conversation threads (this
button appears in situ for replies).
Weve decided to go this route in order to make the viewing experience as smooth
as possible, so that the feed of comments doesnt change as youre reading just
because new comments are coming in. This could be especially disruptive on
breaking news and/or controversial stories with very active discussions.
This option can be disabled by setting the environment variable: [DISABLE_LIVE_UPDATES](/talk/v5/configuration/#disable-live-updates)
### Permitted Domains
List of domains where your Coral instance is allowed to be embedded. List each domain and subdomain in use separately and include the scheme for example: `http://localhost:3000`, `https://staging.yourdomain.com`, `http://yourdomain.com`, `https://yourdomain.com`, etc.
### Story Creation Settings
Advanced settings for how stories are created within Coral; such as if stories are to be automatically created when they are published from your CMS.
See CMS Integration secions [Story Creation](/talk/v5/integrating/cms/#story-creation) and [Story Scraping](/talk/v5/integrating/cms/#story-scraping) for more details.
+149
View File
@@ -0,0 +1,149 @@
---
title: Developing
permalink: /v5/developing/
---
Running Coral for development is very similar to installing Coral via Source as
described above.
Coral requires NodeJS >=12, 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 >=4.2 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:4.2
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
```
We recommend installing [watchman](https://facebook.github.io/watchman/docs/install.html) for better watch
performance.
```bash
# On macOS, you can run the following with Homebrew.
brew update
brew install watchman
```
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
```
## 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/.
## Contributing
We welcome contributions from the open source community!
To get started contributing, check out our [Contribution Guidelines](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md).
### Contributing a Translation
Were so proud to have received submissions from a lot of 3rd party contributors translating Coral into their own languages.
You can see what languages Coral currently supports here: https://github.com/coralproject/talk/tree/master/src/locales
You can set the default language Coral uses in **Admin** > **Configure** > **General**.
We use the fluent library and store our translations in FTL files in `src/locales/` and `src/core/server/locales/`. To add a new Coral translation, strings are added or removed from localization bundles in the translation files as needed.
Strings MUST NOT be changed after they've been committed and pushed to master. Changing a string requires creating a new ID with a new name (preferably descriptive instead of incremented) and deletion of the obsolete ID. It's often useful to add a comment above the string with info about how and where the string is used.
If you are a developer contributing a new language, youll need to add the required i18n support in the i18n files (or you can leave that to us if you like). If youre a non-developer, you can submit the translation via GitHub if you feel comfortable doing that, or feel free to email it to us via our Support: support@coralproject.net
If you want to suggest a new language or put a placeholder for a translation youre working on, feel free to create a GitHub issue: https://github.com/coralproject/talk/issues/new
## Compatibility with IE11
We strive to make the Comment Stream Embed usable in IE11 without being pixel-perfect. If you develop on the Comment Stream you need to be aware of the following:
### CSS Variables
In order to support IE11 on the Comment Stream, every time new CSS is loaded, you need to call `polyfillCSSVarsForIE11()`.
```ts
import { polyfillCSSVarsForIE11 } from "coral-framework/helpers";
const loadProfileContainer = () =>
import("./ProfileContainer" /* webpackChunkName: "profile" */).then(x => {
// New css is loaded, take care of polyfilling those css vars for IE11.
polyfillCSSVarsForIE11();
return x;
});
```
### CSS Calc
Various bugs exists around `calc` support in IE11 (see https://caniuse.com/#feat=calc). We work around most of them by pre-transforming `calc` values using `postcss-calc-function`. Some css attributes might have an issue if you use `css-variables` inside `calc`.
@@ -1,21 +0,0 @@
---
title: Planning your Talk Architecture
permalink: /planning-architecture/
---
Talk is architected to be able to run on as little as 500MB of RAM. To do this however you will need to use the pre-compiled Docker container, as compiling the code and dependencies will cause a memory spike.
For the average small blog or newsroom, these are our recommended machines:
- **Digital Ocean**: ~$5/month for their 1GB droplet
- **Google Cloud**: ~$14/month for a g1 small
- **AWS**: ~$16/month for a t2small
From there, youre free to separate app servers and DB servers, and scale up as much as you need.
One larger newsrooms setup, as an example of Talk performing at scale, is:
Application servers: c4.xlarge (16 VM nginx + Talk VM machine pairs)
Mongo nodes: 3x c3.medium (large db cluster, 1 master, 2 read replicas)
If you need help with Talk performance or want custom scaling help or recommendations, let us know by logging a ticket and one of our engineers will get in touch with you: https://support.coralproject.net
+230
View File
@@ -0,0 +1,230 @@
---
title: Configuration with CLI tools in Version 5
permalink: /v5/configuration-cli-tools/
toc: true
---
Corals CLI tooling has been reinvented in version 5. Weve replaced the former `./bin/cli` tools with a completely new CLI application which is available on GitHub at: https://github.com/coralproject/coral-cli
The new CLI was built on `oclif` ([View on GitHub](https://github.com/oclif)), the open source CLI Framework originally built by Heroku, and offers an extensible platform for interacting with Coral via CLI.
## Installation
To get started with the new CLI run:
```
npm install -g @coralproject/coral-cli
```
## Usage
Once installed, run:
```
coral-cli [COMMAND]
```
Below is a list of commands available:
```
CLI to interact with Coral
VERSION
@coralproject/coral-cli/0.2.0 darwin-x64 node-v11.9.0
USAGE
$ coral-cli [COMMAND]
COMMANDS
help display help for coral-cli
login grabs a token for interacting with Coral
logout removes credentials for logging in with Coral
plugins list installed plugins
story manage stories on Coral
tenant creates a new tenant
token creates tokens on the current user
```
### `coral-cli help [COMMAND]`
Displays help for `coral-cli`, or for the command specified.
```
USAGE
$ coral-cli help [COMMAND]
ARGUMENTS
COMMAND command to show help for
OPTIONS
--all see all commands in CLI
```
### `coral-cli login`
Grabs a session token for interacting with a specific Coral installation. Can be used to create a session prior to running other commands.
```
USAGE
$ coral-cli login
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
```
### `coral-cli logout`
Removes credentials for logging in with Coral and terminates current login session.
```
USAGE
$ coral-cli logout
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
```
### `coral-cli plugins`
Not to be confused with "Talk Plugins" that were available in prior versions of Coral, `coral-cli plugins` are extensions to the `coral-cli` application. See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins). Use this command to the list installed plugins.
```
USAGE
$ coral-cli plugins
OPTIONS
--core show core plugins
EXAMPLE
$ coral-cli plugins
```
### `coral-cli plugins:install [PLUGIN]`
Installs a plugin into the CLI. Can be from npm or a git url. See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins)
*NOTE: Installation of a user-installed plugin will override a core plugin. e.g. If you have a core plugin that has a `hello` command, installing a user-installed plugin with a `hello` command will override the core plugin implementation.*
```bash
USAGE
$ coral-cli plugins:install PLUGIN
ARGUMENTS
PLUGIN plugin to install
OPTIONS
-f, --force yarn install with force flag
-h, --help show CLI help
-v, --verbose
ALIASES
$ coral-cli plugins:add
EXAMPLES
$ coral-cli plugins:install myplugin
$ coral-cli plugins:install https://github.com/someuser/someplugin
$ coral-cli plugins:install someuser/someplugin
```
### `coral-cli plugins:link PLUGIN`
Links a plugin into the CLI for development.
See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins)
```
USAGE
$ coral-cli plugins:link PLUGIN
ARGUMENTS
PATH [default: .] path to plugin
OPTIONS
-h, --help show CLI help
-v, --verbose
EXAMPLE
$ coral-cli plugins:link myplugin
```
### `coral-cli plugins:uninstall PLUGIN`
Removes a plugin from the CLI. See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins)
```
USAGE
$ coral-cli plugins:uninstall PLUGIN
ARGUMENTS
PLUGIN plugin to uninstall
OPTIONS
-h, --help show CLI help
-v, --verbose
ALIASES
$ coral-cli plugins:unlink
$ coral-cli plugins:remove
```
### `coral-cli plugins:update`
Update installed plugins. See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins).
```
USAGE
$ coral-cli plugins:update
OPTIONS
-h, --help show CLI help
-v, --verbose
```
### `coral-cli story:get`
Fetches a story by `id` or `url`, useful to view Story metadata obtained by the scraper.
```USAGE
$ coral-cli story:get
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
--id=id find a story by ID
--url=url find a story by URL
```
### `coral-cli story:merge`
Merge duplicate stories and their comments into a single story. Story metadata & url are maintained on destination.
```
USAGE
$ coral-cli story:merge
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
--from=from (required) source Story ID that will be merged from
--into=into (required) destination Story ID that will be merged into
```
### `coral-cli story:update`
Target a story by `id` to update its `url`.
```
USAGE
$ coral-cli story:update
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
--id=id (required) the ID of the story to update
--url=url (required) the new URL to update the story to
```
### `coral-cli token:create`
Creates a persistent token for the current user (based on running `coral-cli login` above). The token generated can be used to authenticate API requests as the associated user.
```
USAGE
$ coral-cli token:create
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
--name=name (required) name of the token
```
### `coral-cli token:list`
Lists tokens on the current user (based on running `coral-cli login` above).
```
USAGE
$ coral-cli token:list
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
```
### `coral-cli token:revoke`
Revokes token by `id`. Token must be on the current user (based on running `coral-cli login` above).
```
USAGE
$ coral-cli token:revoke
OPTIONS
-d, --domain=domain (required) domain for tenant to run command against
--id=id (required) id of the token to revoke
```
+85
View File
@@ -0,0 +1,85 @@
---
title: Integrating on your site
permalink: /v5/integrating/cms/
---
With Coral setup and running locally you can find your **Embed code** under **Configure** > **Organization** > **Site Details** (when logged in as an ADMIN). It should look something like this, but with your domain in place of `CORAL_DOMAIN_NAME`. You can test placing the comment stream embed on your page with this sample embed script:
```
<div id="coral_thread"></div>
<script type="text/javascript">
(function() {
var d = document, s = d.createElement('script');
var url = '{{ CORAL_DOMAIN_NAME }}';
s.src = '//' + url + '/assets/js/embed.js';
s.async = false;
s.defer = true;
s.onload = function() {
Coral.createStreamEmbed({
id: "coral_thread",
autoRender: true,
rootURL: '//' + url,
});
};
(d.head || d.body).appendChild(s);
})();
</script>`;
```
> **NOTE:** Replace the value of `{% raw %}{{ CORAL_DOMAIN_NAME }}{% endraw %}` with the location of your running instance of Coral.
## Story Creation
Lazy `Story` Creation enables stories to be automatically created when they are published from your CMS. Triggering the embed script above renders the comment stream iFrame on your page. By default that script dynamically generates `Stories` in Coral for seamless integration.
### storyURL
If you do not specify a `storyURL` when rendering the embed, the `storyURL` is first inferred from the Canonical link element, which takes the form of a <link> element in your <head> of the page:
```html
<!DOCTYPE html>
<html>
<head>
<link rel="canonical" href="https://example.com/page" />
</head>
<body>
...
</body>
</html>
```
The url must reference an existing Permitted Domain. If your articles/stories always have unique urls, then you will not need to modify the default behavior.
If this tag is not present, or if the canonical URL references a different url than your site such as a wire service, you can specify the `storyURL` parameter in the render function.
The url will be used by Coral to build user facing links, and should reference the location where you would direct a user back to this particular story or article.
### storyID
To more tightly couple Coral with your CMS you can provide your CMS's unique identifier to Coral by including a `storyID` parameter in the render function. Doing so will allow you to target the `Story` for later updates via Coral's Graphql API, such as updating the URL if it changes.
## Integration via API
Story creation can also be controlled by direct calls to Coral's API. When Lazy Story Creation is disabled embed streams can only be created by data migration or API POST request.
See [GraphQL API Overview](/talk/v5/api/overview/) for help with the API.
## Story Scraping
By default, stories have their metadata scraped when they are loaded. This provides the easiest way for newsrooms to integrate their CMSs into Coral in a simple way. We use the following meta tags on the target pages that allow us to extract some properties.
Metadata scraping is performed by the `scraper` job which is enabled by default.
If your production site is behind a paywall or otherwise prevents scraping, you might need to confiugre a **Scraper Proxy URL**. When specified it allows scraping requests to use the provided proxy. All requests are then passed through the appropriote proxy as parsed by the npm proxy-agent package.
| Asset Property | Selector |
|--------------------|----------|
| `title` | See [`metascraper-title`](https://github.com/microlinkhq/metascraper/blob/dc664c37ea1b238b1e3e9d5342edfacc9027892c/packages/metascraper-title/index.js) |
| `description` | See [`metascraper-description`](https://github.com/microlinkhq/metascraper/blob/dc664c37ea1b238b1e3e9d5342edfacc9027892c/packages/metascraper-description/index.js) |
| `image` | See [`metascraper-image`](https://github.com/microlinkhq/metascraper/blob/dc664c37ea1b238b1e3e9d5342edfacc9027892c/packages/metascraper-image/index.js) |
| `author` | See [`metascraper-author`](https://github.com/microlinkhq/metascraper/blob/dc664c37ea1b238b1e3e9d5342edfacc9027892c/packages/metascraper-author/index.js) |
| `publication_date` | See [`metascraper-date`](https://github.com/microlinkhq/metascraper/blob/dc664c37ea1b238b1e3e9d5342edfacc9027892c/packages/metascraper-date/index.js) |
| `modified_date` | `meta[property="article:modified"]` |
| `section` | `meta[property="article:section"]` |
+132
View File
@@ -0,0 +1,132 @@
---
title: Single Sign On
permalink: /v5/integrating/sso/
---
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://{% raw %}{{ CORAL_DOMAIN_NAME }}{% endraw %} /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 `Secret` box
6. Click Save
> **NOTE:** Replace the value of `{% raw %}{{ CORAL_DOMAIN_NAME }}{% endraw %}` 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. This claim controls the logout functionality on the embed stream. To disable the “Sign Out” links in the embed, remove this claim, and disable “Login with email authentication” on the Comment Stream. You can then call `logout()` on the embed, or expire the token when the SSO user should be signed out of Coral.
- `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 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. Any claims not present on the jwt will be ignored on update, and will not overwrite existing values.
- `user.id` **(required)** - the unique 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 unique 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. There are no username validations or restrictions enforced by Coral when you're using SSO.
- `user.badges` _(optional)_ - array of strings to be displayed as badges beside
username inside Coral, visible to other users and moderators. For example, to indicate
a user's subscription status. If you include the claim, but you are not passing a badge value, then use an empty array instead of null.
- `user.role` _(optional)_ - one of "COMMENTER", "STAFF", "MODERATOR", "ADMIN". Will create/update
Coral user with this permission level. When users have both an assigned role greather than COMMENTER and a badge, both will be displayed.
- `user.url` _(optional)_ - url for user account management, where a user will
be able to perform account management tasks such as changing password or
deleting data. If provided, user will be able to access this URL by clicking
on the "account" tab from the stream.
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 `Secret` 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();
```
## External Integrations
You can integrate directly with the Coral GraphQL API in order to facilitate
account updates for your users when using Coral SSO. The relevant mutations are
as follows:
- `updateUserUsername` lets you update a given user with a new username using
an admin token.
- `updateUserEmail` lets you update a given user with a new email address
using an admin token.
- `deleteUser` lets you delete a given account using an admin token.
Note that even with an admin token, you may not delete yourself via
this method, and instead must use the `requestAccountDeletion`
mutation instead. This differs from the `requestAccountDeletion` as
it does the operation immediately instead of scheduling it as
`requestAccountDeletion` does.
- `requestUserCommentsDownload` lets you retrieve a given account's comments download. This mutation will provide you with a `archiveURL` that can be used to download a ZIP file containing the user's comment export.
If you're unsure on how to call GraphQL API's, refer to the section here on [Making your first GraphQL request](/talk/v5/api/overview/#making-your-first-request).
## Login Prompts
In order to handle login prompts (e.g. a user clicks on the sign in button) you can listen to the `loginPrompt` event.
```js
var embed = Coral.createStreamEmbed({
// Don't forget to include the parameters from the
// "Embed On Your Site" section.
events: function(events) {
events.on("loginPrompt", function() {
// Redirect user to a login page.
location.href = "http://example.com/login";
});
}
});
```
## Troubleshooting JWT Validation Errors
In addition to the uniqueness constraints on User `id` and `email` values, each `user.id`/`user.email` combination must also be unique inside Coral. This is true for both Single Sign On users created by JWT tokens, as well as users that register/login using Corals built in “Login with email authentication”. You can not share authentication strategies for a single user; thus if a user logs in with SSO, they can not also login with email and vice versa. When you attempt to authenticate a JWT token with an email address that already exists in Coral with a different `user.id` than was passed on the token Coral will throw a Duplicate User error.
Any JWT validation errors thrown can be found in Coral's server logs.
+57
View File
@@ -0,0 +1,57 @@
---
title: Social and Email Authentication
permalink: /v5/integration/auth/
---
## Authentication Strategies
There are several strategies to choose from when it comes to user authentication. Login methods can be enabled or disabled for both the **Coral Admin** and the **Comment Stream** separately thus allowing you to use different methods for different user groups.
You can also use "Allow Registration" to restrict or allow users that have not signed up before to register and create new user accounts.
To configure authentication for your instance of Coral, go to **Configure** > **Authentication**.
## Login with email authentication
Best when you want Coral to manage all user registration, creation and authentication. Requires the least amount of setup and configuration. Users register by creating a Username (must be unique across users, no spaces or special chars), Email, and Password. All user details are stored in Coral's database.
By Default - users are sent a verification email and are prompted to verify email addresss, however completing the email verification step is not required to submit comments.
## Login with OpenID Connect
Allows integration with your existing OpenID Connect authentication provider. When enabled this strategy adds a "Login with ___", button to Coral's login modal with the value you've specified for `Provider Name`.
With OpenID Connect authentication is handled by passing an access token parameter to the comment stream embed when placing the call to render it. To learn more please visit: https://openid.net/connect/
## Login with Single Sign On
When you want users to authenticate via your existing identity system, and be automatically registered/logged into Coral. Users register, login, and manage their user details on your existing site, your user database maintains the source of truth for all user information. Users are created and updated in Coral via jwt tokens passed from your auth service to Coral.
Requires advanced configuration, please see: [Single Sign On](/talk/v5/integrating/sso/)
## Login with Google & Facebook
Allows users to use existing social media account to sign in. Requires you to setup API keys with social sign on provider, and configure your API keys in Coral.
Users register and login via a *“Sign in with ….”*, and are not required to set a password if Email based authentication is not enabled. Users with social accounts are prompted to associate an email address with their social profile to enable notifications.
## Commenter Account Management Features
Optional features you can enable or disable to allows users to:
* change their usernames (once every 14 days)
* download their comments
* delete their account details.
## Session settings
Determines length of user sessions when not using SSO or when `exp` claim is not provided on jwt. Default: `90 Days`.
## Username Restrictions
Coral usernames are subject to the following validations:
* USERNAME_REGEX =
`(/^[a-zA-Z0-9_.]+$/)`
* USERNAME_MAX_LENGTH = `30`
* USERNAME_MIN_LENGTH = `3`
*NOTE: SSO users are not subject to username restricitons*
+78
View File
@@ -0,0 +1,78 @@
---
title: Customizing Styles with CSS
permalink: /v5/css/
---
You can add your own stylesheet in **Admin** > **Configure** > **Advanced** > **Custom CSS**.
### Use CSS Variables
We recommend using CSS Variables to broadly apply changes to certain styling aspects like color or fonts.
Here is a CSS example that modifies some of the CSS Variables.
```css
:root {
/* Change primary font */
--font-family-primary: 'Verdana';
/* Remove round corners */
--round-corners: 0px;
}
```
You can find all CSS Variables using the Web Inspector or navigate to https://github.com/coralproject/talk/blob/master/CSS_VARIABLES.md for a comprehensive list and additional information.
> **NOTE:** Before 6.3.0 Coral uses a different set of CSS Variables. Navigate to the link above to get information on upgrading.
### Use stable CSS Class Names
If you would like to change the styling of any elements of the comment embed, we provide global classnames. Most elements will be tagged with either `.coral` or `.coral-stream`.
The easiest way to find the classname for the element you're looking for is to use the web inspector, and then update your stylesheet accordingly.
You can also navigate to https://github.com/coralproject/talk/blob/master/src/core/client/stream/classes.ts to see available stable class names.
### Custom body class for theming
You can set the class name of the `<body>` tag inside the embed by using the `bodyClassName` parameter when calling `Coral.createStreamEmbed`:
```js
Coral.createStreamEmbed({
bodyClassName: "pink"
});
```
This will allow your styles to include variations:
```css
.pink button.coral {
background: pink;
}
```
### Reaction styling
As of Coral 6.3.0, Coral has support for styling based on the number of
reactions that a given comment has received. It does so via the:
```sh
.coral-reacted-{{ n }}
```
Where `{{ n }}` is the number of reactions the comment has received. You can
invert this when creating CSS to allow you to highlight comments that have at
least `{{ n }}` reactions. For example, if you wanted to add a coral color to
comments with at least 3 reactions, you could write:
```css
.coral-comment .coral-indent {
background-color: coral;
}
.coral-reacted-0 .coral-indent,
.coral-reacted-1 .coral-indent,
.coral-reacted-2 .coral-indent {
background-color: transparent;
}
```
+65
View File
@@ -0,0 +1,65 @@
---
title: Emails & Notifications
permalink: /v5/notifications/
---
There are several email notifications included with Coral by default that will be sent to users based on user activity.
_NOTE: Notifications are only supported for users that have a valid email address! If you are using authenticate via Facebook, Google, or OpenIDConnect, user emails are not automatically required._
### Configuring SMTP
You must setup SMTP to send email notifications.
See our documentation on [Email SMTP Settings](/talk/v5/administration/#email-smtp-settings) for reference.
When running in production we recommend using a 3rd party mail service provider like SendGrid or MailGun.
### Notification Types
In addition to the comment activity notifications, Coral also provides users with several transactional notifications about their user profile.
Comment activity notifications are set to OFF for individual users by default, and commenters must opt-in to enable activity notifications. Commenters cannot enable notifications until they have verified their email, and are not able to opt-out of transactional notifications such as when a user has been Banned or Suspended.
#### Type: User Profile Notifications
* When a new Staff, Moderator, or Admin user is invited, sends invitation
* When any user registers, sends confirmation and email verification link
* When user requests password reset, sends reset link
* When user changes username, sends change confirmation
* When user changes their email address, sends change confirmation and email verification link
* When a user is banned or suspended, sends notice
* When comment history is ready for download (only one link can be generated every 7 days, and the link is valid for 24 hours), sends download link
* When user requests account deletion, cancels account deletion, or account deletion has been completed, sends confirmation
_NOTE: Users can not opt-out of User Profile Notifications_
#### Type: Comment Activity Notifications
Commenter users can subscribe to the following comment activity notificaitons:
* When one of my comments receives a reply
* When a Staff, Moderator, or Admin user replies to one of my comments
* When a Moderator Features one of my comments
* When one of my pending comments has been APPROVED or REJECTED
Commenters have 3 options for activity notification frequency:
* Immediately
* Hourly
* Daily
Immediately will send one email notification per occurance, versus sending an hourly or daily summary with all occurances listed.
### Email Templates
Email templates are text based and support translations. Many of the transactional emails can be modified by Moderators at the time of sending to provide additional context to users.
Coral does not currently support customizing email templates beyond the default templates.
To view the current email text strings for US English visit: https://github.com/coralproject/talk/blob/master/src/core/server/locales/en-US/email.ftl
+39
View File
@@ -0,0 +1,39 @@
---
title: GDPR Compliance
permalink: /v5/integrating/gdpr/
---
In order to facilitate compliance with the
[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), Coral
provides features so your users can change and manage their own data.
Even if GDPR will not apply to you, it is recommended to enable these
features as a best practice to provide your users with control over their own
data.
## GDPR Feature Overview
Integrating our GDPR tools will give your users and organizations the following benefits:
- **Download my comment data**: Users can request a download of their comments. An email with a link is emailed to them to download a CSV with each comment they've made, what story it was made on, and the comment's ID and timestamp.
- **Delete my account**: Users can request deletion of their account. Deleted account requests are pending for 24 hours to allow the user to download their comments, or to change their mind and reactivate their account before the expiry. Account deletions remove all of their comments from the site, all their comments and actions from the database, and their account info from our system.
- **Add an email to an OAuth/external account**: Users are prompted to add an email to their non-Coral account (Facebook, Google, external, etc) so that they can take part in GDPR and other features requiring email communication.
- **Change my username**: Users can update their username. This is capped at once every 2 weeks.
- **Change my email**: Users can change their email.
- **Change my password**: Users can change their password.
## GDPR with SSO
As many newsrooms often implement their own [SSO solutions](/talk/v5/integrating/sso/),
we also provide API support to manage GDPR features directly from your own Account or My Profile page.
We provide the following GraphQL mutations designed to allow you to integrate it into your existing user
interfaces or exports.
- `requestUserCommentsDownload` - lets you grab the direct link to download a users
account in a zip format. From there, you can integrate it into your existing
data export or simply proxy it to the user to allow them to download it
elsewhere in your UI.
- `deleteUserAccount` - lets you delete the specified user
**Note: These mutations require an administrative token**
+40
View File
@@ -0,0 +1,40 @@
---
title: Comment Count Integration
permalink: /v5/integrating/counts/
---
Add the `count.js` script to your `html` tree. On a page that includes the _Stream Embed_ this is done for you automatically, however for best performance we recommend to include it into the `<head>` tag.
```html
<script class="coral-script" src="//{{ CORAL_DOMAIN_NAME }}/assets/js/count.js" defer></script>
```
> **NOTE:** Replace the value of `{% raw %}{{ CORAL_DOMAIN_NAME }}{% endraw %}` with the location of your running instance of Coral.
> **NOTE:** `class="coral-script"` is needed to support IE11.
Insert an html element with the class `coral-count` and configure it using `data-coral-*` attributes.
```html
<span class="coral-count" data-coral-url="http://example.com/blog-entry-1/"></span>
```
After successful injection it will become:
```html
<span class="coral-count" data-coral-url="http://example.com/blog-entry-1/">
<span class="coral-count-number">5</span>
<span class="coral-count-text">Comments</span>
</span>
```
### Available `data-coral-*` attributes
Set the class of your html element to `coral-count` in order to get story counts. The following `data-coral-*` attributes will configure the output:
- `data-coral-id` The id of the story of which counts should be injected.
- `data-coral-url` The url of the story of which counts should be injected.
- `data-coral-notext` If set to `"true"`, only the count number will be injected
Either `data-coral-id` or `data-coral-url` should be set. If none are provided the story url
will be retrieved from the [canonical url reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs#Using_%3Clink_relcanonical%3E) `<link rel="canonical" href="...">` or inferred using the current page url.
+28
View File
@@ -0,0 +1,28 @@
---
title: Slack
permalink: /v5/integrating/slack/
---
Coral version 5.4+ supports built-in Slack integration to help you forward comments from your moderation queues into appropriate Slack channels.
## Creating a Slack App
To enable web hooks that we will use to forward comments, you'll need to create an App and give it permissions over a channel.
For details on how to create a Slack app with webhooks, please go to:
https://slack.com/intl/en-ca/help/articles/115005265063-incoming-webhooks-for-slack
After you have created a Slack app with a webhook, you can use it in your Coral configuration.
1. Sign into the administration side of your Coral deployment.
2. Select _Configure_ from the top navigation.
3. Select _Slack_ from the side navigation for the configuration area.
4. Here you can configure a Slack channel.
5. Paste in the webhook URL you created for your Slack app and select which comment categories you want to receive notifications for.
## I need to find the webhook URL again, where is it?
Webhooks options are tied to a Slack app and can be found under your app settings at:
https://api.slack.com/apps
+32
View File
@@ -0,0 +1,32 @@
---
title: Event Tracking
permalink: /v5/integrating/events/
---
The Coral embed script can emit events to the analytics tool of your choice. Common user actions, such as successfully posting a comment, posting a reaction, or changing a setting are all capturable with events.
## Analytics Tool Integration
To enable event tracking, modify your embed script to subscribe to the events hook. You will also need to add code to your page that sends the events to your analytics system. The particular way you send this will depend on what tool youre using. Refer to your tools API and docs to determine this.
In this example, were logging all events to the console as well as sending the `createComment.success` event.
```js
<script>
const CoralStreamEmbed = Coral.createStreamEmbed({
events: function(events) {
events.onAny(function(eventName, data) {
console.log(eventName, data);
if (eventName === 'createComment.success') {
my_event_tracker.send('createComment', data);
});
},
});
</script>
```
## Available Events
A complete list of trackable events is available on GitHub: https://github.com/coralproject/talk/blob/master/CLIENT_EVENTS.md
+62
View File
@@ -0,0 +1,62 @@
---
title: SEO & iframes
permalink: /v5/seo/
---
Since Coral is embedded in an article page using an iframe, we often get asked what affect,
if any, iframes have on a publishers SEO. We asked Hart Van Santvoord, Vox Medias resident
SEO expert to give us some insight to help answer these questions.
We can confidently say that today's modern crawlers are able to crawl and index content in
iframes and that they do their best to associate the two and identify which version is preferred.
This isnt as relevant however for content that is complementary to an article, such as comments
here in Corals case, or with other embedded content like documents or live blogs. Its is more
important for products or people who are iframing entire pages, such as for syndication.
[Here's commentary on that from John Mueller of Google, probably the most trusted
Google technical SEO resource we have.](https://www.seroundtable.com/google-iframes-seo-24098.html )
When newsrooms and organizations come to us with SEO questions, it usually falls into
2 use cases.
1. We need our comments to be indexed to help our SEO!
For this case, we are very confident that the way Coral is currently handling comments
via iframe provides the SEO benefit publishers are looking for:
* Google can crawl it and associate it with the container article/page
* They're certainly seeing commenting activity as a positive sign of engagement which
Google cares about A LOT
* Ongoing commenting also indicates to google that the page is evolving/updating,
another strong signal
* Serving via iframe typically improves performance as opposed to including the content
directly on the parent page, and faster/better speed and UX is yet another strong positive
signal to Google
For an example of how the Vox Media SEO team has analyzed iframe content outside of Coral,
we can can look The Verge. The Verge leverages live blogging for certain types of events,
[such as iPhone events](https://www.theverge.com/2019/9/10/20840715/apple-iphone-11-event-live-blog-announcement-photos).
In this case, similar to Coral, the live blog content itself is embedded on The Verge article
via iframe but no doubt leads to a more engaged audience who stick around longer, refreshes,
clicks load more, etc. We see these articles perform well in search in a super competitive
space on event days.
That brings us to our 2nd use case:
2. Please, make sure my comments are NOT indexed!
In this request, the use case is slightly more complicated due to the fact we know
Google can crawl and index comment content. But, given that Coral does not also
create creating a page just with the comments, we dont see a chance Google would
Surface just comment content in search results outside of the article in which theyre
embedded.
If youre a publisher in this scenario and are worried about the content in comments
reflecting poorly on your site/brand then this seems to be as much as moderation issue
As a Google one, and again, Coral does not provide a way for Google to crawl, index,
and serve up commenting content outside the context of the article regardless.
If you are a newsroom doing creative things with your comments and perhaps would
Have the need to create a separate page with only comments, then you can certainly explore
Using tools like a canonical tag to redirect that page back to the container page, and other
SEO best practices for this and other edge cases.
+215
View File
@@ -0,0 +1,215 @@
---
title: GraphQL API Overview
permalink: /v5/api/overview/
---
## GraphQL API
Our API is generally served via GraphQL at `/api/graphql` on your Coral installation. If you're running Coral locally, this would be https://localhost:8080/api/graphql.
## Enabling & Using `/graphiql`
It is not required to enable the `/graphiql` playground to use Corals GraphQL API. The playground simply provides an easy way to explore and interact with Corals GraphQL schema. You can enable the GraphiQL interface at https://localhost:3000/graphiql (Note the port number here is not 8080, this is because this route is directly served by the server, and not the webpack development server) when running in development. You can do this by setting `ENABLE_GRAPHIQL=true`.
**(🚨 Note 🚨) we do not recommend using this in production environments as it disables many safety features used by the application**.
When submitting requests in the `/graphiql` playground, it is necessary to include an ADMIN level bearer token to authenticate your requests. When signed into Coral as an ADMIN level user, you can grab your current access token from your browsers local storage. Add the following to the HTTP Headers section of your GraphiQL call.
```sh
{“authorization”:”Bearer <token>”}
```
## Making your first request
To learn a bit about how to interact with Coral, we'll query for comments on a
page of Coral.
The GraphQL endpoint we have can be used with any HTTP client available, but our
examples below will use the common `curl` tool:
```sh
curl --request POST \
--url "http://localhost:8080/api/graphql" \
--header "content-type: application/json" \
--data '{"query":"query GetComments($url: String!) {story(url: $url) { id metadata { title } url comments { nodes { id body author { id username } } } } }","variables":{"url":"http://localhost:8080/"},"operationName":"GetComments"}'
```
When you unpack that, it's really quite simple. We're executing a `POST` request
to the `/api/graphql` route of the local Talk server with the GraphQL
request we want to make. It's composed of the `query`, `variables`, and
`operationName`.
```graphql
query GetComments($url: String!) {
story(url: $url) {
metadata {
title
}
url
comments {
nodes {
body
author {
username
}
}
}
}
}
```
We are grabbing the asset with the specified `$url`, and grabbing it's title and the comments under it.
We can then also specify our variables to the query being executed (in this
case, the url for the page where we have comments on our local install of Coral):
```json
{
"url": "http://localhost:8080/"
}
```
It's also sometimes common to have multiple queries within a query, which is
where the `operationName` comes into play, where we simply specify the named
query that we want to execute (in this case, `GetComments`).
To get a deeper understanding of GraphQL queries, read up on
[GraphQL Queries and Mutations](http://graphql.org/learn/queries/).
## Understanding the response
Once you completed the above GraphQL query with `curl`, you'll get a response
sort of like this:
```json
{
"data": {
"story": {
"metadata": {
"title": "Coral 5.0 Embed Stream"
},
"url": "http://localhost:8080/",
"comments": {
"nodes": [
{
"body": "First comment!",
"author": {
"username": "wyatt.johnson"
}
}
]
}
}
}
}
```
All of the parameters you requested should be available under the `data`
property. Any errors that you get would appear in a `errors` array at the top
level, like this:
```json
{
"errors": [
{
"message": "The specified story URL does not exist in the permitted domains list.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["story"],
"extensions": {
"code": "STORY_URL_NOT_PERMITTED",
"id": "e255e860-d3ab-11e9-acdf-b9e9700f06fa",
"type": "INVALID_REQUEST_ERROR",
"message": "The specified story URL does not exist in the permitted domains list."
}
}
],
"data": {
"story": null
}
}
```
You should know that any property that is marked with a `!` is considered
required, and non-nullable, which means you can always guarantee on it being
there in your request if there were no errors.
## Authorizing a request
Some queries you may notice seem to return an error of
`USER_NOT_ENTITLED`. It's likely the case that you are making a request to a
route that requires authorization. You can perform authorization a few ways in
Talk.
Essentially, you need to get access to a JWT token that you can use to authorize
your requests.
```sh
curl --request POST \
--url http://localhost:3000/api/auth/local \
--header 'content-type: application/json' \
--data '{ "email": "${EMAIL}", "password": "${PASSWORD}"}'
```
Which returns a response similar to:
```json
{
"token": "${TOKEN}"
}
```
Where `${EMAIL}` is the email address of an admin user, and `${PASSWORD}` is the password for that admin user. This will generate a short term token (valid for 90 days). To generate a long lived access token (or Personal Access Token), you have to exchange the token generated above to create a new long lived token:
```sh
curl --request POST \
--url "http://localhost:3000/api/graphql" \
--header 'authorization: Bearer ${TOKEN}' \
--header 'content-type: application/json' \
--data '{"query":"mutation CreateAccessToken { createToken(input: { clientMutationId: \"\", name: \"My PAT\" }) { signedToken }}","operationName":"CreateAccessToken"}'
```
Which returns a response similar to:
```json
{
"data": {
"createToken": {
"signedToken": "${TOKEN}"
}
}
}
```
Where `${TOKEN}` in the request being from the previous set of login steps and returning a new `signedToken` as `${TOKEN}` which can be used instead of the previous `${TOKEN}` value in the below examples.
Once you have your access token, you can substitute it as `${TOKEN}` in your
`curl` request as follows:
### Bearer Token
```sh
curl --request POST \
--url "http://localhost:8080/api/graphql" \
--header "content-type: application/json" \
--header "Authorization: Bearer ${TOKEN}" \
--data '{"query":"query GetComments($url: String!) {story(url: $url) { id metadata { title } url comments { nodes { id body author { id username } } } } }","variables":{"url":"http://localhost:8080/"},"operationName":"GetComments"}'
```
### Cookie
```sh
curl --request POST \
--url "http://localhost:8080/api/graphql" \
--header "content-type: application/json" \
--cookie "authorization=${TOKEN}"
--data '{"query":"query GetComments($url: String!) {story(url: $url) { id metadata { title } url comments { nodes { id body author { id username } } } } }","variables":{"url":"http://localhost:8080/"},"operationName":"GetComments"}'
```
## Persisted Queries
You might see an error like `RAW_QUERY_NOT_AUTHORIZED`. This means that you attempted to use either no token, or a token from a user without admin privileges. In Coral, we whitelist the GraphQL mutations and queries that are performed by the applications for security reasons meaning that only admin users can make arbitrary queries against the GraphQL API.
-69
View File
@@ -1,69 +0,0 @@
---
title: FAQ
permalink: /faq/
---
## How can I get help, submit a bug, or suggest a feature?
There are a few avenues to get in touch with us and others in the community for help.
To log a bug or request a feature, submit a Support ticket ([support@coralproject.net](mailto:support@coralproject.net)) and someone from our team will get back to you.
You can also request help on Github by [submitting an issue](https://github.com/coralproject/talk/issues). This also increases your chances of having someone from the community respond to help.
And you can also search our [Coral Community](https://community.coralproject.net) to see if your issue has been solved, or to get tips from the community.
## How can our dev team contribute to Talk?
We are lucky to work with newsroom dev teams and individual contributors who span the world, and come from newsrooms of all sizes. You can read our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) to get started, but feel free to reach out to us via Github too.
## What if we want to add a feature you don't have?
Talk is open source, so you're free to develop additional functionality and [submit a pull request](https://github.com/coralproject.net/talk).
## Do you have GDPR features?
Yes! Please read our [GDPR documentation](/talk/integrating/gdpr/) for more information and instructions to get started.
## Can I import my existing comments?
Yes! We have a community-supported [import framework](https://github.com/coralproject/talk-importer) that you can use to migrate your existing comments.
## What support is available?
Our team is small, so it's difficult for us to provide support packages. However, you can always email us at [support@coralproject.net](mailto:support@coralproject.net), and we can help answer your questions. In some cases, we can provide premium support packages either with our team, or through partners. You can inquire about this via the support email address above.
## Is there a hosted version I can purchase by monthly subscription?
Yes! We are happy to announce that as of July 2018, we provide a SaaS version of Talk, called the Coral Cloud. For a monthly subscription, you get your own hosted Talk instance to embed on your news and blog articles. [Reach out to us](mailto:support@coralproject.net) if you're interested in this option.
## Where is our data when we use Talk?
If you are hosting Talk on your own:
* Your data is stored in a MongoDB database that you provide
* The Coral Team doesnt have any access to your data
If you are using Coral Cloud Hosting for Talk:
* Your data is stored in a dedicated MongDB database that is provisioned for your in the Cloud
* Your data is completely isolated from other customers data
* The Coral Team and its third party database hosting providers use strict access controls and auditing to protected your data from unauthorized access by team members
## Does Talk have any automated moderation features to protect against spam and trolling?
Talk features a couple of plugins that provide advanced moderation:
* The [Toxic Comments plugin](/talk/plugin/talk-plugin-toxic-comments) integrates with the [Perspective API from Google](https://www.perspectiveapi.com/) to detect the likelihood of toxicity of comments in real-time
* The [Akismet plugin](/talk/plugin/talk-plugin-akismet) detects and blocks spam comments
## How much can I customize Talk?
* The CSS of the Talk comment stream can be customized by [adding your own CSS via an external stylesheet](/talk/integrating/styling-css/)
* The functionality of Talk can be [extended through the plugin framework](/talk/plugins/)
## How much does Talk cost?
* The Talk software is freely available under the Apache 2.0 open source license
* Associated costs are those for the infrastructure required to run Talk (i.e. cloud hosting fee or bare-metal server costs)
* The Coral Project offers a SaaS version of Talk. Please [get in touch with us](mailto:support@coralproject.net) to discuss pricing for your custom requirements.
+5
View File
@@ -146,3 +146,8 @@
tags:
- default
- sorting
- name: talk-plugin-comment-count
description: This plugin enables adding comment counts to Talks embed or in other locations on your site.
link: https://github.com/coralproject/talk-plugin-comment-count
tags:
- counts
+3 -3
View File
@@ -3,10 +3,10 @@ title: Contact
permalink: /contact/
---
## How can I get help integrating Talk into my newsroom?
## How can I get help integrating Coral into my newsroom?
We're here to help with newsrooms of all sizes. Connect with us [here](https://coralproject.net/contact/) to see how we can help you!
We're here to help with newsrooms of all sizes. Email our Support Team
([support@coralproject.net](mailto:support@coralproject.net)) to set up a meeting.
## How do I request a feature or submit a bug?
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

-106
View File
@@ -1,106 +0,0 @@
---
title: Authenticating with Talk
permalink: /integrating/authentication/
---
Out of the box Talk supports account registration with username and password, as well as features like forgot password.
You can also integrate Talk with any external authentication service that will enable
seamless single sign-on (SSO) for users within your organization. There are a few
methods of doing so:
1. Passport Middleware
2. Custom Token Integration
Both methods work, but there are product decisions that will affect the overall
choice.
## Passport Middleware
You would choose the **Passport Middleware** route when you are OK using an auth
that is triggered from inside Talk that is not connected to an external auth
state (you don't use the auth anywhere else now).
Plugins are available for the following 3rd party authentication providers:
* [Facebook](/talk/plugin/talk-plugin-facebook-auth/)
* [Google](/talk/plugin/talk-plugin-google-auth/)
## Custom Token Integration
You can integrate Talk with any authentication service to enable single sign-on
for users. The steps to do that are:
1. Create a service that generates [JWT tokens](https://jwt.io/introduction/).
2. Push the token into the embed.
3. Implement the [`tokenUserNotFound`](#implement-tokenusernotfound) hook to
process the token.
### Create JWT Token
You should create an external service that is responsible for generating a JWT
for use with Talk. The token can be generated as easy as checking out the
following node app: https://github.com/coralproject/talk-token-example
Using that demo application, you'll see how you can:
1. Create a node application that can issue JWT's that are compatible with Talk.
2. Provide a validation endpoint that can be used by Talk to validate the token
and get the user via the [`tokenUserNotFound`](#implement-tokenusernotfound)
hook.
It's also important to note a few requirements for proper integration with Talk.
The generated JWT must contain the following claims:
- [`jti`](https://tools.ietf.org/html/rfc7519#section-4.1.7): a unique identifier for the token (like a uuid/v4)
- [`exp`](https://tools.ietf.org/html/rfc7519#section-4.1.4): the expiry date of the token as a unix timestamp
- [`sub`](https://tools.ietf.org/html/rfc7519#section-4.1.2): the user identifier that can be used to lookup the user in the mongo
database
- The user may not yet exist in the database, but that's the responsibility
of the [`tokenUserNotFound`](#implement-tokenusernotfound) hook.
- [`iss`](https://tools.ietf.org/html/rfc7519#section-4.1.1): the issuer for the token must match the value of `TALK_JWT_ISSUER`
- [`aud`](https://tools.ietf.org/html/rfc7519#section-4.1.3): the audience for the token must match the value of `TALK_JWT_AUDIENCE`
### Push token into embed
We're assuming that your CMS is capable of authenticating a user account, or
at least having the user's details available to send off to the token creation
service we created/used in the previous step.
Using the token that was created for the user, you simply have to amend the
template where Talk is rendering to read as the following:
```js
Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
// ...
auth_token: '<your generated JWT token issued for this user>',
});
```
Which will pass down the token to Talk and will fire the next steps
`tokenUserNotFound` hook to complete the auth flow.
### Implement `tokenUserNotFound`
This is the only piece of code you'll have to write that lives inside Talk.
The role of this code is to live as a plugin and provide Talk with a way of
taking the token that you gave it, and turning into a user.
Using the example application we were working with in the JWT issuing step
above, we'll need to ensure that the configuration is consistent in-between both
Talk and the JWT issuer. Namely, the following environment variables from our
example issuer and Talk must match:
| Talk | Token Issuer Example |
|------|----------------------|
|[`TALK_JWT_ISSUER`](/talk/advanced-configuration/#talk-jwt-issuer)|`JWT_ISSUER`|
|[`TALK_JWT_AUDIENCE`](/talk/advanced-configuration/#talk-jwt-audience)|`JWT_AUDIENCE`|
|[`TALK_JWT_SECRET`](/talk/advanced-configuration/#talk-jwt-secret)|`JWT_SECRET`*|
\* Note that secrets is a pretty complex topic, refer to the
[TALK_JWT_SECRET](/talk/advanced-configuration/#talk-jwt-secret) configuration
reference, the basic takeaway is that the secret used to sign the tokens issued
by the issuer must be able to be verified by Talk.
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound)
reference.
@@ -1,33 +0,0 @@
---
title: Tracking Talk Events and Metrics
permalink: /integrating/event-tracking-metrics/
---
Talk supports event emitting via Redux, Apollo and GraphQL. This means that common actions taken within Talk, such as successfully posting a comment, posting a reaction, or changing a setting, are automatically emitted from Talk. To send these events to your analytics tool of choice, however, will require some integration on your part.
First, we want to uncomment the tracking code in `article.ejs` (https://github.com/coralproject/talk/blob/93bda87ad061a2dc5eb8dc5b65a579a20efb76f7/views/article.ejs#L34). This will enable events to be sent via the Talk embed that is on your article pages. This will start a stream of events to the browser console, so that you can see which events are available.
```
events: function(events) {
events.onAny(function(eventName, data) {
// logs all available events.
console.log(eventName, data);
});
},
```
Now, we want to add our code that sends the events to our analytics system. In this case, we're sending the `PostComment.success` event. The particular way you send this will depend on what tool you're using. Refer to your tool's API and docs to determine this.
```
events: function(events) {
events.onAny(function(eventName, data) {
console.log(eventName, data);
if (eventName === 'mutation.PostComment.success') {
my_event_tracker.send('postComment', data);
}
});
},
```
You can continue this process for any specific events you'd like to track. You can also remove the `console.log` to stop events being emitted to the browser and instead only send the events to your analytics tool.
PR for Reference: https://github.com/coralproject/talk/pull/785
-69
View File
@@ -1,69 +0,0 @@
---
title: Notifications
permalink: /integrating/notifications/
---
Talk currently supports 3 types of email notifications.
1. When someone replies to my comment
2. When a staff member replies to my comment
3. When my comment gets featured
Talk support 3 options for notification frequency: immediately, hourly or daily. Commenters can also opt-out of email notifications. Notifications are set to OFF by default.
Commenters cannot enable notifications until they have verified their email.
Note: Notifications are not currently supported for users that sign-up via Facebook or Google auth, or don't have an email attached to their account for any other reason.
### Configuring SMTP
You must setup SMTP to use notifications. The following ENV variables must be set:
```
TALK_SMTP_FROM_ADDRESS=email@email.com
TALK_SMTP_USERNAME=username
TALK_SMTP_PASSWORD=password
TALK_SMTP_HOST=smtp.domain.net
TALK_SMTP_PORT=2525
```
### Enabling Notifications
Enabling the `talk-plugin-notifications` creates a NotificationManager that creates and manages events send from the event emitter that is linked to the Graph API PubSub system.
Adding the `talk-plugin-notifications` plugin will also enable the `notifications` plugin hook. Any plugin that registers before the `talk-plugin-notifications` plugin will get picked up by.
See https://github.com/coralproject/talk/blob/8b669a31c551a042f0f079d8cfc16825673eb8f0/plugins/talk-plugin-notifications-reply/index.js for an example.
### Notification Categories
Talk currently supports the following Notifications options out of the box:
`talk-plugin-notifications-category-reply`
`talk-plugin-notifications-category-staff-reply`
`talk-plugin-notifications-category-featured`
### Notification Digests
Talk supports hourly and daily digests out the box, if you would like to create your own, refer to the below:
https://github.com/coralproject/talk/blob/9cc9969320dca47bb0f8f81e8d944ae4d19e548b/plugins/talk-plugin-notifications/server/connect.js#L69-L102
### Connect API
This exposes the `graph/connectors.js` via the `connect` hook.
```
module.exports = {
connect(connectors) {
// use `connectors`, contents of https://github.com/coralproject/talk/blob/b758dc91cb1f1969ecd895b6059306b318995b33/graph/connectors.js#L104
}
}
```
See https://github.com/coralproject/talk/blob/90290cfa2de88e62f687e1ed0235ba6dfe4cde26/plugins/talk-plugin-notifications/server/connect.js for an example.
### Email Templates
Email templates are text based and support translations. If you would like to create a new email template, you can register it via the Connect API, see https://github.com/coralproject/talk/blob/8b669a31c551a042f0f079d8cfc16825673eb8f0/plugins/talk-plugin-notifications/server/connect.js#L12-L28###
@@ -1,13 +1,22 @@
---
title: Talk Quickstart
permalink: /
permalink: /installation-quickstart/
---
Online comments are broken. Our open-source Talk tool rethinks how moderation,
comment display, and conversation function, creating the opportunity for safer,
smarter discussions around your work. Read more about our product features and
goals [here](https://coralproject.net/talk). The
documentation available here is pertaining to the technical details for
smarter discussions around your work.
More than 60 newsrooms use Talk to run their on-site communities, including the Washington Post, the Wall Street Journal, and IGN.
[Read more about our product features and
goals here](https://coralproject.net/).
<div class="callout">
We offer hosting and support packages for Talk. [Contact us for more information.](https://coralproject.net/pricing/)
</div>
The documentation available here is pertaining to the technical details for
installing, configuring, and deploying Talk.
Talk is a [Node](https://nodejs.org/) application with
@@ -22,7 +31,7 @@ to persist data. The following versions are supported:
- MongoDB 3.2+
- Redis 3.2.5+
You can run Talk (and its dependencies) locally or from [Docker](https://www.docker.com/community-edition#/download) containers. Docker is used in the local example below for the database and cache, however it is possible to run Talk without Docker by configuring your own MongoDB and Redis instances. We have tested Talk
You can run Talk (and its dependencies) locally or from [Docker](https://www.docker.com/community-edition#/download) containers. Docker is used in the local example below for the database and cache, however it is possible to run Talk without Docker by configuring your own MongoDB and Redis instances. We have tested Talk
and this documentation with Docker versions 17.06.2+.
An optional dependency for Talk is
@@ -35,7 +44,7 @@ and this documentation with versions 1.14.0+.
### Installation from Docker
To use Talk without major customization you can run the application using our
provided docker image.
provided docker image.
Start by making a new directory and create a file called `docker-compose.yml` and copy the following:
@@ -43,10 +52,10 @@ Start by making a new directory and create a file called `docker-compose.yml` an
# For details on the syntax of docker-compose.yml files, check out:
# https://docs.docker.com/compose/compose-file/compose-file-v2/
version: '2'
version: "2"
services:
talk:
image: coralproject/talk:4.5
image: coralproject/talk:4
restart: always
ports:
- "3000:3000"
@@ -80,7 +89,6 @@ volumes:
The environment variables listed above are the bare minimum needed to run the demo, for more configuration
variables, check out the [Configuration](/talk/configuration/) section.
And you can then start it with:
```bash
@@ -112,7 +120,6 @@ redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
talk_1 yarn start Up 0.0.0.0:3000->3000/tcp
```
You now have a Talk instance up and running! Continue on to the [Setup](#setup) section for details on how to complete the
initial setup and get started using Talk.
@@ -148,7 +155,6 @@ Didn't work? Sometimes you may already have a container running on these ports,
run `docker ps` to see what other containers you have running and running
`docker stop <id>` on those containers to stop them.
_This documentation assumes that you will be running MongoDB on
`127.0.0.1:27017` and Redis on `127.0.0.1:6379`. The above Docker commands bind
MongoDB and Redis on these interfaces for you._
@@ -179,27 +185,26 @@ Continue onto the [Setup](#setup) section for details on how to complete the
installation and get started using Talk.
## Setup
### Create Admin Account
With Talk running, you can now navigate to
[http://127.0.0.1:3000/admin/install](http://127.0.0.1:3000/admin/install)
and walk through the initial setup steps.
and walk through the initial setup steps.
* First, enter your **Organization Name** and **Organization Contact Email**. This will appear in emails when inviting new team members.
* Next, create your Admin user. You can specify an **Email Address**, **Username**, and **Password**
* Finally, enter your list of **Permitted Domains**, read [here](/talk/configuring-talk/#permitted-domains) about whitelisting domains
- First, enter your **Organization Name** and **Organization Contact Email**. This will appear in emails when inviting new team members.
- Next, create your Admin user. You can specify an **Email Address**, **Username**, and **Password**
- Finally, enter your list of **Permitted Domains**, read [here](/talk/configuring-talk/#permitted-domains) about whitelisting domains
_During development, ensure you whitelist 127.0.0.1:3000 otherwise the
[http://127.0.0.1:3000/](http://127.0.0.1:3000/) page will not
load._
Once the setup wizard has been completed you can log into Talk ([http://127.0.0.1:3000/](http://127.0.0.1:3000/)) using the email address and password for the Admin user account that you just created.
From here you can test out features in Talk, see comments in the admin interface where you can do moderation, and configure the user experience.
In the next step you'll create some user comments to moderate.
Once the setup wizard has been completed you can log into Talk ([http://127.0.0.1:3000/](http://127.0.0.1:3000/)) using the email address and password for the Admin user account that you just created.
From here you can test out features in Talk, see comments in the admin interface where you can do moderation, and configure the user experience.
In the next step you'll create some user comments to moderate.
### Demo Embedded Comments
@@ -212,7 +217,7 @@ In order for the demo to work, you must add
`https://docs.coralproject.net/` to your
permitted domains list. You can do this by visiting
[http://127.0.0.1:3000/admin/configure](http://127.0.0.1:3000/admin/configure)
now and selecting *Tech Settings* from the sidebar.
now and selecting _Tech Settings_ from the sidebar.
Once you have added the domain of these docs, you can click the button below.
@@ -226,13 +231,12 @@ Once you have added the domain of these docs, you can click the button below.
With your local instance of Talk running in development mode (env variable `NODE_ENV=development`) you should now also be able to access the following developer routes:
* [http://127.0.0.1:3000/dev](http://127.0.0.1:3000/dev]) provides a sample comment stream
- [http://127.0.0.1:3000/dev](http://127.0.0.1:3000/dev]) provides a sample comment stream
* [http://127.0.0.1:3000/dev/assets](http://127.0.0.1:3000/dev/assets]) provides a list of all stories in Talk and can generate new sample assets
- [http://127.0.0.1:3000/dev/assets](http://127.0.0.1:3000/dev/assets]) provides a list of all stories in Talk and can generate new sample assets
### Conclusion
At this point you've successfully installed, configured, and ran your very own
instance of Talk! Continue through this documentation on this site to learn more
on how to configure, develop with, and contribute to Talk!
@@ -43,7 +43,7 @@ be used to setup Talk:
version: '2'
services:
talk:
image: coralproject/talk:4.5
image: coralproject/talk:4
restart: always
ports:
- "3000:3000"
@@ -121,7 +121,7 @@ base installation with additional custom plugins. Images can be created with the
most basic of `Dockerfile`'s:
```docker
FROM coralproject/talk:4.5-onbuild
FROM coralproject/talk:4-onbuild
```
And running the following to build the docker image:
@@ -153,7 +153,7 @@ your containerized infrastructure. The versioning of our Docker tags as well
lets you do something like:
```docker
FROM coralproject/talk:4.5-onbuild
FROM coralproject/talk:4-onbuild
```
Which would pin your image to `4.5.x release's.
Which would pin your image to `4.x.x release's.

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