Compare commits

..
57 Commits
Author SHA1 Message Date
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
77 changed files with 1153 additions and 360 deletions
+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.
+1 -1
View File
@@ -2,7 +2,7 @@
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, a part of [Vox Media](https://www.voxmedia.com/a/go-deeper/about).
Built with <3 by The Coral Project, a part of [Vox Media](https://www.voxmedia.com/).
## Getting Started
@@ -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>
);
@@ -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)}
@@ -140,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 {
@@ -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,7 +62,7 @@
font-weight: 300;
margin-bottom: 8px;
overflow-wrap: break-word;
word-break:break-word;
word-break: break-word;
}
.body {
@@ -103,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;
@@ -111,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;
@@ -127,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;
@@ -140,7 +140,7 @@
white-space: nowrap;
&:hover {
text-decoration: underline;
opacity: .9;
opacity: 0.9;
}
i {
font-size: 12px;
@@ -196,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)}
+41 -6
View File
@@ -3,6 +3,7 @@ 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 {
@@ -92,6 +93,21 @@ function viewportDimensions() {
};
}
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,
@@ -110,19 +126,23 @@ export default class Bridge {
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.opts = opts;
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 = new SnackBar(snackBarStyles || {});
this.snackBar = amp ? null : new SnackBar(snackBarStyles || {});
this.onAuthChanged = onAuthChanged;
this.talkBaseUrl = ensureEndSlash(talkBaseUrl);
this.talkStaticUrl = ensureEndSlash(talkStaticUrl);
this.lazy = lazy;
// Store queued operations in a queue that can be processed once the stream
// is rendered.
@@ -179,6 +199,16 @@ export default class Bridge {
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,
},
'*'
);
}
}
});
@@ -267,8 +297,10 @@ export default class Bridge {
// Setup Pym.
this.setupPym();
// Attach the snackBar to the pym parent and to the body of the page.
this.snackBar.attach(window.document.body, this.pym);
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);
@@ -315,7 +347,10 @@ export default class Bridge {
this.emitter.removeAllListeners();
// Remove the snackbar.
this.snackBar.remove();
if (this.snackBar) {
this.snackBar.remove();
this.snackBar = null;
}
// Remove the pym parent.
this.pym.remove();
+1
View File
@@ -56,6 +56,7 @@ export const Talk = {
* @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.talkStaticUrl] - (optional) Static URL used to serve Talk
* @param {Boolean} [config.amp] - (optional) Run Talk in AMP mode
* @return {Object}
*/
render: (element, config) => {
+26
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',
+3 -3
View File
@@ -76,9 +76,7 @@ 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
children:
@@ -122,6 +120,8 @@ sidebar:
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
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"hexo": {
"version": "3.7.1"
"version": "3.8.0"
},
"scripts": {
"start": "hexo serve",
+13 -4
View File
@@ -5,9 +5,18 @@ permalink: /
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/talk).
<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
@@ -46,7 +55,7 @@ Start by making a new directory and create a file called `docker-compose.yml` an
version: '2'
services:
talk:
image: coralproject/talk:4.5
image: coralproject/talk:4
restart: always
ports:
- "3000:3000"
@@ -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.
+7 -1
View File
@@ -13,6 +13,9 @@ permalink: /pre-launch-checklist/
- [ ] Do you have a domain name for Talk?
- **Recommended**: You should host Talk on a subdomain on your main site (e.g. if your site is mysitefornews.org, you should serve Talk from a subdomain like talk.mysitefornews.org) to avoid issues with third-party cookie sharing.
- [ ] Do you have a Content Delivery Network (CDN) to improve performance and reduce load on your Talk servers?
- [ ] Do you have a MongoDB instance?
- A MongoDB [Docker instance](/talk/installation-from-docker/) hosted alongside Talk?
@@ -28,9 +31,11 @@ permalink: /pre-launch-checklist/
- [ ] Do you want to provide single sign-on (SSO) by integrating with an external auth system?
- See [Authenticating with Talk](/talk/integrating/authentication/)
- [ ] Do you want to integrate Talk with your CMS to automate embedding Talk Comment Stream into your site?
- See [CMS Integration](/talk/integrating/cms-integration/)
- [ ] Do you want to use Social sign-on?
- Facebook
- Install [talk-plugin-facebook-auth](/talk/plugin/talk-plugin-facebook-auth/)
@@ -72,7 +77,8 @@ permalink: /pre-launch-checklist/
- See [our blog for more information](https://coralproject.net/blog/slacking-on/)
- [ ] Has your community team configured Talk to match your community strategy?
- [ ] Have you configured Talks admin settings and determined your community strategy?
- See [our tutorial for more information](https://docs.coralproject.net/talk/when-youve-installed-talk/)
- See [Configuring Talk](/talk/configuring-talk/)
+12 -5
View File
@@ -416,13 +416,19 @@ cannot. You would use this option in the latter situation.
## TALK_SMTP_FROM_ADDRESS
The email address to send emails from using the SMTP provider in the format:
The email address to send emails from using the SMTP provider. You can include the name and email address or only provide the email.
```plain
TALK_SMTP_FROM_ADDRESS="The Coral Project" <support@coralproject.net>
TALK_SMTP_FROM_ADDRESS="The Coral Project" support@coralproject.net
```
or
```plain
TALK_SMTP_FROM_ADDRESS=support@coralproject.net
```
Including the name and email address.
## TALK_SMTP_HOST
@@ -499,8 +505,9 @@ Could be read as:
pre-moderated once in order to post freely again. If they instead get rejected
again, then they must have two of their comments approved in order to get
added back to the queue.
- At the moment of writing, behavior is not attached to the flagging
reliability, but it is recorded.
- At the moment of writing, behavior is not attached to reliable commenters
as well as the flagging reliability and unreliability. Regardless,
all of these scores are recorded for future implementation.
## TALK_DISABLE_IGNORE_FLAGS_AGAINST_STAFF
+5
View File
@@ -81,12 +81,17 @@ Usage: cli-assets [options] [command]
Commands:
list [options] list all the assets in the database
debug <url> prints the scraped metadata from that URL
refresh [age] queues the assets that exceed the age requested
update-url <assetID> <url> update the URL of an asset
merge <srcID> <dstID> merges two assets together by moving comments from src to dst and deleting the src asset
rewrite [options] <search> <replace> rewrites asset url's using the provided regex replacement pattern
```
When using the `refresh` command, the `age` value specifies how far back in time to re-scrape assets; i.e. to re-scrape everything that was scraped in the last week use `1w` or `7d`. Supports ms (milliseconds), s (seconds), m (minutes), h (hours), d (days) and w (weeks). Assets that have not been scraped will also be queued for scraping.
See also, [Asset Scraping](/talk/integrating/asset-scraping/) for more details about asset scraping.
## Setting up the application
You can also run a setup wizard to setup the wizard using `./bin/cli setup`. Below is a list of additional options available for this command:
```
@@ -57,7 +57,7 @@ The timeframe in seconds in which commenters have to edit their comment.
#### Close Comments After
Default time after which all comment streams will close.
Default time after which all comment streams will close. Applies to assets created in Talk after this configuration is saved, and does not update existing assets.
### Moderation Settings
@@ -88,8 +88,9 @@ Comments with suspect words will display until a moderator takes action on them.
#### Permitted Domains
A list of domains where your Talk instance is allowed to be embedded. Typical
use is `localhost`, `staging.yourdomain.com`, `yourdomain.com`, etc.
A list of domains where your Talk instance is allowed to be embedded. List each domain and subdomain in use
separately, for example: `localhost`, `staging.yourdomain.com`, `yourdomain.com`, `www.yourdomain.com`, etc.
#### Embed Script
+6
View File
@@ -47,3 +47,9 @@ Source: [talk-recipes/tree/master/plugins/author-name](https://github.com/coralp
Enables the ability to hover over a commenters name and add plugin
functionality there. The Member Since plugin that is provided in this recipe is
an example of this.
## recipe-custom-auth
Source: [talk-recipes/tree/master/plugins/custom-auth](https://github.com/coralproject/talk-recipes/tree/master/plugins/custom-auth)
Example code for creating, and updating Users in Talk from JWT data provided by your custom SSO authentication service.
@@ -87,6 +87,8 @@ The most important predictors of the success of an online community are:
#### Effectively
* Keep conversations fresh and reduce moderation overhead by configuring Talk to automatically close commenting after a specified time window. Should comments be open for a day, a week, or a month? Set a manageable window and automatically close stories for commenting after that time period.
* If you're using the Toxic Comments plugin, make sure that its threshold is set at the level that catches most comments with fewest false positives (default is 80%). You can see the Likely to be Toxic level of every comment by clicking "More Details" on the comment card in the moderation view.
* Publicly discourage behavior in the comments that doesn't cross the line but suggests that the tone or focus could shift quickly in a direction you don't want. Point to relevant sections of your community guidelines. [Read more about defining and discouraging this kind of behavior here.](https://guides.coralproject.net/manage-a-successful-community/)
+245 -168
View File
@@ -56,16 +56,32 @@ Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
definitions. `enum`'s will be appended to, types will be appended, and new types
will be added.
This can be included as a plugin via:
```js
const fs = require("fs");
const path = require("path");
module.exports = {
typeDefs: fs.readFileSync(path.join(__dirname, "typeDefs.graphql"), "utf8")
};
```
Assuming the above GraphQL schema is located in the plugin folder at
`typeDefs.graphql`.
### context
```js
{
Slack: (context) => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
}
module.exports = {
context: {
Slack: context => ({
notify: message => {
// return a promise after we're done sending notifications.
},
});
},
};
```
Any property provided here will be added to the context parameter available
@@ -83,41 +99,50 @@ A special context hook, `Sort` will allow plugin authors to provide new
methods to sort data. An example is as follows:
```js
{
Sort: () => ({
Comments: { // <-- (1)
likes: { // <-- (2)
startCursor(ctx, nodes, {cursor}) { // <-- (3)
return cursor != null ? cursor : 0;
},
endCursor(ctx, nodes, {cursor}) { // <-- (4)
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
},
sort(ctx, query, {cursor, sort}) { // <-- (5)
if (cursor) {
query = query.skip(cursor);
}
module.exports = {
context: {
Sort: () => ({
Comments: {
// <-- (1)
likes: {
// <-- (2)
startCursor(ctx, nodes, { cursor }) {
// <-- (3)
return cursor != null ? cursor : 0;
},
endCursor(ctx, nodes, { cursor }) {
// <-- (4)
return nodes.length
? (cursor != null ? cursor : 0) + nodes.length
: null;
},
sort(ctx, query, { cursor, sort }) {
// <-- (5)
if (cursor) {
query = query.skip(cursor);
}
return query.sort({
'action_counts.like': sort === 'DESC' ? -1 : 1,
created_at: sort === 'DESC' ? -1 : 1,
});
},
},
},
}),
}
return query.sort({
"action_counts.like": sort === "DESC" ? -1 : 1,
created_at: sort === "DESC" ? -1 : 1
});
}
}
}
})
}
};
```
This has a bunch of special features:
1. `Comments` is the name of the type being sorted, this is pluralized and
capitalized.
capitalized.
2. `likes` is the `sortBy` field in lowercase.
3. `startCursor` will retrieve the start cursor based on the current set of
nodes and the current cursor.
nodes and the current cursor.
4. `endCursor` will retrieve the end cursor based on the current set of nodes
and the current cursor.
and the current cursor.
5. `sort` will mutate the `query` to apply the sort operations.
All the `startCursor`, `endCursor`, and `sort` functions must be provided in
@@ -126,11 +151,13 @@ order for the sorting to apply properly.
### loaders
```js
(context) => ({
People: {
load: () => db.people.find({user: context.user})
}
})
module.exports = {
context: context => ({
People: {
load: () => db.people.find({ user: context.user })
}
})
};
```
Loaders should be provided as a function which returns a map which is used in
@@ -139,13 +166,15 @@ the resolvers function. These must return a promise or a value.
### mutators
```js
(context) => ({
People: {
create: (name) => {
return db.people.insert({user: context.user, name});
module.exports = {
context: context => ({
People: {
create: name => {
return db.people.insert({ user: context.user, name });
}
}
}
})
})
};
```
Mutators should be provided as a function which returns a map which is used in
@@ -154,27 +183,41 @@ the resolvers function. These must return a promise or a value.
### resolvers
```js
{
Person: {
name(obj, args, context) {
return obj.name;
module.exports = {
resolvers: {
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === "bill" ? "RED" : "BLUE";
}
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
RootQuery: {
people(
obj,
args,
{
loaders: { People }
}
) {
return People.load();
}
},
RootMutation: {
createPerson(
obj,
{ name },
{
mutators: { People }
}
) {
return People.create(name);
}
}
}
}
};
```
Should return a resolver map as described in the
@@ -185,40 +228,44 @@ This will merge with the existing resolvers in core and from previous plugins.
### hooks
```js
{
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) {
if (!person) {
module.exports = {
hooks: {
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
}
};
```
Hooks here are pretty special, for each resolver field, you can specify a
pre/post hook that will execute pre and post field resolution.
If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
result. It is _required_ that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
### setupFunctions
```js
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
},
}),
}
module.exports = {
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: person => person.place === 1
}
})
}
};
```
Setup functions allow you to create filters that control which pubsub.publish() events
@@ -229,28 +276,32 @@ For more information, see the [Apollo Docs](https://github.com/apollographql/gra
### tokenUserNotFound
```js
tokenUserNotFound: async ({jwt, token}) => {
let profile = await someExternalService(token);
if (!profile) {
return null;
const Users = require("services/users");
module.exports = {
tokenUserNotFound: async ({ jwt }) => {
const user = await Users.upsertExternalUser(
null,
jwt.sub,
jwt.iss,
jwt.username
);
const email = jwt.email.toLowerCase();
//upsertExternalUser will also create a profile with provider:jwt.iss, id:jwt.sub
//add a "local" profile to persist email on the user
user.profiles.push({
provider: "local",
id: email
});
user.created_at = new Date(jwt.memberSince * 1000);
await user.save();
return user;
}
let user = await UserModel.findOneAndUpdate({
id: profile.id
}, {
id: profile.id,
username: profile.username,
lowercaseUsername: profile.username.toLowerCase(),
roles: [],
profiles: []
}, {
setDefaultsOnInsert: true,
new: true,
upsert: true
});
return user;
}
};
```
The `tokenUserNotFound` hook allows auth integrations to hook into the event
@@ -258,8 +309,15 @@ when a valid token is provided but a user can't be found in the database that
matches the provided id.
The function is async, and should return the user object that was created in the
database, or null if the user wasn't found. The `jwt` parameter of the object
is the unpacked token, while `token` is the original jwt token string.
database. The `jwt` parameter of the object is the unpacked token, which has already been validated.
More details about the `upsertExternalUser` method can be found at:
https://github.com/coralproject/talk/blob/32962aa1e84c09d87141440a1f04cbd1659b3336/services/users.js/#L592
In this example, a unix timestamp was included on the jwt with a date that is being persisted on the user. This is optional, but can be used to handle any extra claims you want to include on the jwt. You can use the metadata object on the user to store custom values.
Having trouble implementing your own `tokenUserNotFound` hook?
Submit a Support ticket ([support@coralproject.net](mailto:support@coralproject.net))
### tags
@@ -268,18 +326,20 @@ or enabled by code). Below is an example pulled from the core off topic plugin
on how to create a hook for the `OFF_TOPIC` name:
```js
[
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
module.exports = {
tags: [
{
name: "OFF_TOPIC",
permissions: {
public: true,
self: true,
roles: []
},
models: ["COMMENTS"],
created_at: new Date()
}
]
};
```
You can refer to `models/schema/tag.js` for the available schema to match when
@@ -288,11 +348,13 @@ creating models to enable/disable specific features.
### router
```js
(router) => {
router.get('/api/v1/people', (req, res) => {
res.json({people: [{name: 'Bob'}]});
});
}
module.exports = {
router: router => {
router.get("/api/v1/people", (req, res) => {
res.json({ people: [{ name: "Bob" }] });
});
}
};
```
The Router hook allows you to create a function that accepts the base express
@@ -302,56 +364,73 @@ action needed by external applications.
### passport
```js
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
const FacebookStrategy = require("passport-facebook").Strategy;
const UsersService = require("services/users");
const {
ValidateUserLogin,
HandleAuthPopupCallback
} = require("services/passport");
module.exports = {
passport(passport) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, async (req, accessToken, refreshToken, profile, done) => {
passport: passport => {
passport.use(
new FacebookStrategy(
{
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${
process.env.TALK_ROOT_URL
}/api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ["id", "displayName", "picture.type(large)"]
},
async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
const { id, provider, displayName } = profile;
user = await UsersService.upsertSocialUser(
req.context,
id,
provider,
displayName
);
} catch (err) {
return done(err);
}
let user;
try {
const { id, provider, displayName } = profile;
user = await UsersService.upsertSocialUser(
req.context,
id,
provider,
displayName
);
} catch (err) {
return done(err);
}
return ValidateUserLogin(profile, user, done);
}));
return ValidateUserLogin(profile, user, done);
}
)
);
},
router(router) {
router: router => {
// Note that we have to import the passport instance here, it is
// instantiated after all the strategies have been mounted.
const {passport} = require('services/passport');
const { passport } = require("services/passport");
/**
* Facebook auth endpoint, this will redirect the user immediately to facebook
* for authorization.
*/
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
router.get(
"/facebook",
passport.authenticate("facebook", {
display: "popup",
authType: "rerequest",
scope: ["public_profile"]
})
);
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon successful login.
*/
router.get('/facebook/callback', (req, res, next) => {
router.get("/facebook/callback", (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
passport.authenticate(
"facebook",
HandleAuthPopupCallback(req, res, next)
)(req, res, next);
});
}
};
@@ -360,10 +439,10 @@ module.exports = {
### translations
```js
const path = require('path');
const path = require("path");
module.exports = {
translations: path.join(__dirname, 'translations.yml'),
translations: path.join(__dirname, "translations.yml")
};
```
@@ -377,11 +456,11 @@ en:
Which overrides the copy for the `embedlink.copy` template. You can
also provide other languages as well by using the correct language
prefix.
prefix.
When creating a plugin using this `translations` hook to override copy
When creating a plugin using this `translations` hook to override copy
from another plugin, be sure to list it after the plugin it's overriding
in the `plugins.json` file.
in the `plugins.json` file.
### websockets
@@ -392,12 +471,12 @@ module.exports = {
// Do something with the connection params or connection, like
// logging it out, or incrementing a metric.
},
onDisconnect: (connection) => {
onDisconnect: connection => {
// Do something with the connection params or connection, like
// logging it out, or decrementing a metric.
},
},
}
}
}
};
```
This `websockets` hook can be used to attach methods to the
@@ -411,12 +490,13 @@ connections.
module.exports = {
schemaLevelResolveFunction: (root, args, ctx, info) => {
// The GraphQL Operation Name. Example: CoralEmbedStream_Embed
const name = info.operation.name !== null ? info.operation.name.value : null;
const name =
info.operation.name !== null ? info.operation.name.value : null;
// Maybe increment a metric based on the operation name...
// You must _always_ return the root.
return root;
},
}
};
```
@@ -431,9 +511,7 @@ Contents of `plugins.json`:
```json
{
"server": [
"people"
]
"server": ["people"]
}
```
@@ -527,5 +605,4 @@ module.exports = {
}
}
};
```
+47
View File
@@ -0,0 +1,47 @@
---
title: Accelerated Mobile Page
permalink: /integrating/amp/
---
[AMP](https://amp.dev/) is a light-weight, stripped down HTML page that aims to improve reader experience. _Talk v4.9.0+_ comes with [AMP](https://amp.dev/) support. The current caveat however is that _toast notifications_ are not being rendered when viewing inside AMP.
# How to integrate
Put the following code into your _AMP_ page and replace `$TALK_URL` and `$ASSET_URL` with the
corresponding values. You can also pass `asset_id` instead of `asset_url`.
```html
<amp-iframe
  width=600 height=140
  layout="responsive"
  sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
  resizable
  src="https://$TALK_URL/embed/amp#asset_url=$ASSET_URL">
  <div placeholder></div>
  <div overflow tabindex=0 role=button aria-label="Read more">Read more</div>
</amp-iframe>
```
## Single Sign-On
For SSO integration you need to create a page with the following output and replace `$TALK_URL` and `$AUTH_TOKEN` with the appropriate values. Inject your SSO auth scripts to get the `$AUTH_TOKEN` for the current user. Integrating with [amp-access](https://amp.dev/documentation/components/amp-access) is recommended which opens a 1st-party popup to not have browsers block your cookies. This page is then used in `src` of `<amp-iframe>` above. It must be accessed over `https` and live in a different domain than the `amp` page.
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>Coral Talk Amp Embed</title>
</head>
<body>
<div id='coralStreamEmbed'></div>
<script src="https://$TALK_URL/static/embed.js"></script>
<script>
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
talk: '$TALK_URL',
auth_token: '$AUTH_TOKEN',
amp: true,
})
</script>
</body>
</html>
```
+9 -2
View File
@@ -9,6 +9,9 @@ in a simple way. We use the following
[meta tags](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) on
the target pages that allow us to extract some properties.
Asset scraping is performed by the `scraper` job which is enabled by default when you launch Talk. If your production site is behind a paywall or otherwise prevents scraping, you might need to confiugre a [TALK_SCRAPER_PROXY_URL](/talk/advanced-configuration/#talk-scraper-proxy-url) or custom [TALK_SCRAPER_HEADERS](/talk/advanced-configuration/#talk-scraper-headers).
| Asset Property | Selector |
|--------------------|----------|
| `title` | See [`metascraper-title`](https://github.com/microlinkhq/metascraper/blob/dc664c37ea1b238b1e3e9d5342edfacc9027892c/packages/metascraper-title/index.js) |
@@ -19,7 +22,7 @@ the target pages that allow us to extract some properties.
| `modified_date` | `meta[property="article:modified"]` |
| `section` | `meta[property="article:section"]` |
You can use the `./bin/cli assets debug <url>` to print the scraped metadata
You can use the `./bin/cli assets debug <url>` command to print the scraped metadata
from that URL. For example:
```bash
@@ -41,4 +44,8 @@ from that URL. For example:
├──────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ section │ │
└──────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```
```
You can use the `./bin/cli assets refresh [age]` to trigger scraping or rescrape assets where the scraper job was unsuccessful.
+22 -23
View File
@@ -3,25 +3,18 @@ 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.
### Authentication Strategies
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:
Talk comes with built in user management features like account registration with username/email and password, as well as features like resetting a forgotten password.
1. Passport Middleware
2. Custom Token Integration
There are four strategies to choose from when it comes to user authentication:
Both methods work, but there are product decisions that will affect the overall
choice.
#### Talk Email Authentication
Best when you want Talk 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 and are stored in Talks database. Optionally - you can require an email verification step at time of account creation. *Plugins required: [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth), [talk-plugin-auth](/talk/plugin/talk-plugin-auth)*
## Passport Middleware
#### Social Sign-in (FaceBook or Google via 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:
Allows users to use existing social media account to sign in to Talk. Requires you to setup API keys with social sign on provider, and configure your API keys in Talk. Users register and login via a *“Sign in with ….”*, but are not required to set user details such as password or email address. *Plugins required: [talk-plugin-auth](/talk/plugin/talk-plugin-auth), plus at least one of the following:*
* [Facebook](/talk/plugin/talk-plugin-facebook-auth/)
* [Google](/talk/plugin/talk-plugin-google-auth/)
@@ -29,15 +22,20 @@ Plugins are available for the following 3rd party authentication providers:
_FAQ: Can I create a Twitter auth plugin?_
This is currently not possible because Talk uses passport.js which does not support Twitter's oAuth2 requirements.
## Custom Token Integration
#### Social Sign-in with Email Association
This strategy is actually a combination of the first two. Talks built in Email Authentication is augmented by also allowing registration with a social account. Users with social accounts are prompted to associate an email address with their social profile to enable notifications. *Plugins required: [talk-plugin-auth](/talk/plugin/talk-plugin-auth), [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth), [talk-plugin-facebook-auth](/talk/plugin/talk-plugin-facebook-auth/), and or [talk-plugin-google-auth](/talk/plugin/talk-plugin-google-auth/)*
#### Custom SSO Token Integration
When you want users to authenticate via your existing identity system, and be automatically registered/logged into Talk. 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 Talk via jwt tokens passed from your auth service to Talk. *Required Plugins: requires custom plugin development, as well as disabling talk-plugin-auth, and talk-plugin-local-auth*
## Setting Up Custom SSO 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.
2. Include the JWT token in your Talk embed script
3. Create a custom plugin to generate and authenticate Talk users from the jwt token
### Create JWT Token
@@ -67,7 +65,7 @@ The generated JWT must contain the following claims:
### Generate a key to sign the JWT
Optionally you can use https://github.com/coralproject/coralcert to generate a key with which to sign the JWTs and specify the secret as an environment variable.
### Push token into embed
### Include token in 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
@@ -83,15 +81,17 @@ Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
});
```
Which will pass down the token to Talk and will fire the next steps
Which will pass down the token to Talk and will fire the
`tokenUserNotFound` hook to complete the auth flow.
### Implement `tokenUserNotFound`
### Create a Custom Plugin to Generate Talk Users
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.
Your custom plugin will need to include a `tokenUserNotFound` function that will handle creating a user in Talk from the claims provided on the JWT, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound) for reference.
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
@@ -108,5 +108,4 @@ example issuer and Talk must match:
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.
For an example of implementing the plugin, check out our [SSO Custom Auth Recipe](https://github.com/coralproject/talk-recipes/tree/master/plugins/custom-auth)
@@ -3,9 +3,19 @@ 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.
Talk supports event emitting. 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, youll need to configure your Talk embed to process them.
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.
The following example demonstrates how the event emitter works on a sample article page generated by Talk. When youre ready to capture and handle events from your actual site, add the events property along with your event handling functions to your Talk embed script.
## Example Using Local Development Instance of Talk
If you have Talk running in a local development environment you can follow these steps to first view all of the events emitted by Talk, and then capture the `PostComment.success` event and send it to a third party analytics provider.
First, with Talk running, ensure that you can access the `/dev/assets` endpoint and create a random article (i.e.: `localhost:3000/dev/assets`).
Then in Talk, uncomment the events code in `/views/dev/article.njk`
(https://github.com/coralproject/talk/blob/master/views/dev/article.njk#L32). This will enable events to be sent via the Talk embed that is on the sample article page. This will start a stream of events to the browser console, so that you can see which events are available.
```
events: function(events) {
@@ -16,7 +26,10 @@ First, we want to uncomment the tracking code in `article.ejs` (https://github.c
},
```
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.
Save your changes to `article.njk`, restart Talk, and try refreshing your article in a browser with development console open. You should see the console log the list of Talks events.
Next, we want to add our code that sends the events to our analytics system. In this example, 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) {
@@ -30,4 +43,24 @@ Now, we want to add our code that sends the events to our analytics system. In t
```
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
## Metrics List
Note: This is not an exhaustive list, but should get you started.
```
mutation.CreateRespectAction.success
mutation.CreateLikeAction.success
mutation.PostComment.success
mutation.PostFlag.success
mutation.EditComment.success
action.CORAL_STREAM_SET_ACTIVE_TAB
action.SHOW_SIGNIN_DIALOG
action.FOCUS_SIGNIN_DIALOG
action.TALK_FRAMEWORK_HANDLE_SUCCESSFUL_LOGIN
action.HIDE_SIGNIN_DIALOG
action.TALK_VIEWING_OPTIONS_OPEN_MENU
action.TALK_VIEWING_OPTIONS_CLOSE_MENU
action.CORAL_STREAM_SET_SORT
action.SET_ACTIVE_REPLY_BOX
```
@@ -0,0 +1,18 @@
---
title: talk-plugin-notifications-category-moderation-actions
permalink: /plugin/talk-plugin-notifications-category-moderation-actions/
layout: plugin
plugin:
name: talk-plugin-notifications-moderation-actions
default: true
depends:
- name: talk-plugin-notifications
provides:
- Server
- Client
---
When a comment that is initially withheld from publication and is then approved or rejected, the user will receive a notification email.
Configuration:
`TALK_MODERATION_NOTIFICATION_TYPES`. This plugin requires values to be set. Available options: `APPROVED`, `REJECTED` as a single string (comma separated).
+1 -1
View File
@@ -111,7 +111,7 @@ If you deploy using Docker, you can extend from the `*-onbuild` image, an
example `Dockerfile` for your project could be:
```Dockerfile
FROM coralproject/talk:4.5-onbuild
FROM coralproject/talk:4-onbuild
```
Establish a private repository for your instance that includes the following:
+3 -4
View File
@@ -28,9 +28,8 @@
<div class="notice">
<p>Don't see what you're looking for? We're upgrading the docs, check out our <a href="https://github.com/coralproject/talk/tree/ef49d9a3d2acc4d2fc03b00e0c872dfbc57f005a/docs/_docs" target="_blank" class="coral-link-invert">old docs</a>.</p>
<p><a href="https://coralproject.net/" class="coral-link-invert" target="_blank">The Coral Project</a> from Mozilla</p>
<a href="https://www.mozilla.org/" target="_blank" title="Mozilla"><figure class="mozilla" alt="Mozilla">{{ image_tag('images/moz-logo-bw-rgb.svg') }}</figure></a>
<p>Made by <a href="https://coralproject.net/" class="coral-link-invert" target="_blank">The Coral Project</a> by <a href="https://www.voxmedia.com/" class="coral-link-invert" target="_blank" title="Vox Media">Vox Media</a>.</p>
<a href="https://www.voxmedia.com/" target="_blank" title="Vox Media"><figure class="voxmedia" alt="Vox Media">{{ image_tag('images/coralproject.png') }}</figure></a>
</div>
</aside>
<div class="sidebar__backdrop"></div>
<div class="sidebar__backdrop"></div>
+25 -2
View File
@@ -344,9 +344,13 @@ a.brand {
padding: $content_gutter $content_gutter 60px;
}
.mozilla {
width: 50%;
.voxmedia {
// width: 50%;
margin: 0 0 $sidebar_gutter;
> img {
width: 100%;
}
}
.sidebar {
@@ -461,3 +465,22 @@ a.brand {
background: rgb(0, 102, 176);
color: #fff;
}
.callout {
background-color: $coral_button_background;
color: white;
margin: 20px 0;
padding: 20px;
text-align: center;
a {
@extend .coral-link;
color: #FFF;
border-color: #FFF;
&:hover {
color: #FFF;
border-color: darken(#FFF, 5%);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 360 102.8" style="enable-background:new 0 0 360 102.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<rect y="-0.2" width="360" height="102.9"/>
<path class="st0" d="M343.8,73.7c-0.7,0.2-1.3,0.3-1.9,0.3c-2.1,0-3.1-0.9-3.1-3.5V51c0-10.3-8.2-15.3-17.9-15.3
c-7.4,0-11.4,0.9-19.3,4.1L300,50.2l10.3,1.1l1.5-5.1c2.1-1.1,4.2-1.3,6.9-1.3c7.3,0,7.4,5.5,7.4,10.1v1.5
c-2.3-0.3-4.9-0.4-7.4-0.4c-10.3,0-21,2.6-21,13.7c0,9.4,7.4,12.9,13.9,12.9c7.3,0,11.9-4.4,14.5-9c0.6,5.5,3.9,9,9.9,9
c2.8,0,5.7-0.8,8.1-2.1L343.8,73.7z M316.2,73.4c-3.9,0-5.3-2.3-5.3-5.2c0-4.9,4-6.2,8.6-6.2c2.1,0,4.4,0.3,6.5,0.6
C325.7,70.1,320.8,73.4,316.2,73.4z M298.9,12.9l-19.5,68.7h-12.7l19.5-68.7H298.9z M272.7,12.9l-19.5,68.7h-12.7L260,12.9H272.7z
M218.7,36.6h13.5v16.2h-13.5V36.6z M218.7,65.5h13.5v16.2h-13.5V65.5z M199,64.9l10.2,1l-2.8,15.8h-39.3l-1.3-6.8l24.8-28.5h-14.1
l-2,7l-9.3-1l1.6-15.8h39.5l1,6.8l-25,28.5h14.6L199,64.9z M134.5,35.7c-16.2,0-24.2,10.9-24.2,24.1c0,14.4,9.6,22.9,23.5,22.9
c14.4,0,24.8-9.1,24.8-23.5C158.6,46.6,150.7,35.7,134.5,35.7z M134.2,72.7c-7,0-10.6-6-10.6-13.8c0-8.5,4.1-13.4,10.7-13.4
c6.1,0,11,4.1,11,13.2C145.3,67.4,140.9,72.7,134.2,72.7z M98.7,71.8h6v9.8H85.9V56.3c0-7.8-2.6-10.8-7.7-10.8
c-6.2,0-8.7,4.4-8.7,10.7v15.7h6v9.8H56.6V56.3c0-7.8-2.6-10.8-7.7-10.8c-6.2,0-8.7,4.4-8.7,10.7v15.7h8.6v9.8H21.4v-9.8h6V46.4h-6
v-9.8h18.9v6.8c2.7-4.8,7.4-7.7,13.7-7.7c6.5,0,12.5,3.1,14.7,9.7c2.5-6,7.6-9.7,14.7-9.7c8.1,0,15.5,4.9,15.5,15.6V71.8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

+3 -3
View File
@@ -64,13 +64,13 @@ const wrapCheck = (
/**
* checkPermissions checks that the current user has all the required
* permissions.
* permissions. It will return true if that's the case.
*
* @param {Object} ctx graph context
* @param {Array<String>} permissions permissions that the user must have
*/
const checkPermissions = (ctx, permissions) =>
!ctx.user || !ctx.user.can(...permissions);
ctx.user && ctx.user.can(...permissions);
/**
* wrapCheckPermissions will wrap a specific field with a permission check.
@@ -89,7 +89,7 @@ const wrapCheckPermissions = (
wrapCheck(
typeResolver,
field,
(obj, args, ctx) => !checkPermissions(ctx, permissions),
(obj, args, ctx) => checkPermissions(ctx, permissions),
skipFieldResolver
);
+6
View File
@@ -879,6 +879,9 @@ type Settings {
# customCssUrl is the URL of the custom CSS used to display on the frontend.
customCssUrl: String
# customAdminCssUrl is the URL of the custom CSS used to display on the admin panel.
customAdminCssUrl: String
# closedTimeout is the amount of seconds from the created_at timestamp that a
# given asset will be considered closed.
closedTimeout: Int
@@ -1353,6 +1356,9 @@ input UpdateSettingsInput {
# customCssUrl is the URL of the custom CSS used to display on the frontend.
customCssUrl: String
# customAdminCssUrl is the URL of the custom CSS used to display on the admin panel.
customAdminCssUrl: String
# closedTimeout is the amount of seconds from the created_at timestamp that a
# given asset will be considered closed.
closedTimeout: Int
+6 -1
View File
@@ -111,7 +111,12 @@ const processJob = transport => async ({ id, data }, done) => {
const { message } = data;
// Get the email address from the job data.
message.to = await getEmailAddress(data);
try {
message.to = await getEmailAddress(data);
} catch (err) {
logger.error({ err }, 'Failed to get user email address to send mail');
return done(err);
}
const log = logger.child({ jobID: id });
log.info('Starting to send mail');
+6 -1
View File
@@ -35,6 +35,9 @@ en:
flagged: flagged
undo_reject: Undo
view_context: 'View context'
edit_history: 'Edit history'
show_edit_history: 'Show edit history'
hide_edit_history: 'Hide edit history'
comment_box:
cancel: Cancel
characters_remaining: 'characters remaining'
@@ -124,6 +127,8 @@ en:
copy_and_paste: 'Copy and paste code below into your CMS to embed your comment box in your articles'
custom_css_url: 'Custom CSS URL'
custom_css_url_desc: 'URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.'
custom_admin_css_url: 'Custom Admin Panel CSS URL'
custom_admin_css_url_desc: 'URL of a CSS stylesheet that will override default admin panel styles. Can be internal or external.'
days: Days
description: 'Change the comment settings on this story.'
disable_commenting_desc: 'Write a message that will be displayed while commenting is deactivated.'
@@ -219,7 +224,7 @@ en:
subject: 'Your account has been banned'
confirm:
confirm_email: 'Confirm Email'
has_been_requested: 'A email confirmation has been requested for the following account:'
has_been_requested: 'An email confirmation has been requested for the following account:'
if_you_did_not: 'If you did not request this, you can safely ignore this email.'
subject: 'Email Confirmation'
to_confirm: 'To confirm the account, please visit the following link:'
+3
View File
@@ -35,6 +35,9 @@ es:
flagged: Reportado
undo_reject: Deshacer
view_context: 'Ver contexto'
edit_history: 'Historial de ediciones'
show_edit_history: 'Mostrar historial de ediciones'
hide_edit_history: 'Ocultar historial de ediciones'
comment_box:
cancel: Cancelar
characters_remaining: 'caracteres restantes'
+11 -2
View File
@@ -12,6 +12,8 @@ const {
STATIC_ORIGIN,
} = require('../url');
const { PORT } = require('../config');
const { RECAPTCHA_PUBLIC, WEBSOCKET_LIVE_URI } = require('../config');
// Grab TALK_CLIENT_* environment variables.
@@ -42,6 +44,7 @@ const TEMPLATE_LOCALS = {
MOUNT_PATH,
STATIC_URL,
TALK_CLIENT_ENV,
PORT,
data: TALK_CLIENT_ENV,
};
@@ -96,12 +99,18 @@ const createResolveFactory = (() => {
module.exports = async (req, res, next) => {
try {
// Attach the custom css url and organization name.
const { customCssUrl, organizationName } = await SettingsService.select(
// Attach the custom css urls and organization name.
const {
customCssUrl,
customAdminCssUrl,
organizationName,
} = await SettingsService.select(
'customCssUrl',
'customAdminCssUrl',
'organizationName'
);
res.locals.customCssUrl = customCssUrl;
res.locals.customAdminCssUrl = customAdminCssUrl;
res.locals.organizationName = organizationName;
} catch (err) {
console.warn(err);
+4
View File
@@ -28,6 +28,10 @@ const Setting = new Schema(
type: String,
default: '',
},
customAdminCssUrl: {
type: String,
default: '',
},
infoBoxContent: {
type: String,
default: '',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "4.8.3",
"version": "4.9.0",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
+22 -6
View File
@@ -1,26 +1,42 @@
{
"server": [
"talk-plugin-featured-comments",
"talk-plugin-local-auth",
"talk-plugin-featured-comments",
"talk-plugin-respect",
"talk-plugin-rich-text",
"talk-plugin-profile-data",
"talk-plugin-respect"
"talk-plugin-notifications",
"talk-plugin-notifications-category-featured",
"talk-plugin-notifications-category-moderation-actions",
"talk-plugin-notifications-category-reply",
"talk-plugin-notifications-category-staff",
"talk-plugin-notifications-digest-hourly",
"talk-plugin-notifications-digest-daily"
],
"client": [
"talk-plugin-auth",
"talk-plugin-local-auth",
"talk-plugin-author-menu",
"talk-plugin-featured-comments",
"talk-plugin-flag-details",
"talk-plugin-ignore-user",
"talk-plugin-local-auth",
"talk-plugin-member-since",
"talk-plugin-moderation-actions",
"talk-plugin-permalink",
"talk-plugin-profile-data",
"talk-plugin-respect",
"talk-plugin-rich-text",
"talk-plugin-viewing-options",
"talk-plugin-sort-most-replied",
"talk-plugin-sort-most-respected",
"talk-plugin-sort-newest",
"talk-plugin-sort-oldest",
"talk-plugin-viewing-options"
"talk-plugin-profile-data",
"talk-plugin-notifications",
"talk-plugin-notifications-category-featured",
"talk-plugin-notifications-category-moderation-actions",
"talk-plugin-notifications-category-reply",
"talk-plugin-notifications-category-staff",
"talk-plugin-notifications-digest-hourly",
"talk-plugin-notifications-digest-daily",
"talk-plugin-ignore-user"
]
}
@@ -4,7 +4,6 @@ permalink: /plugin/talk-plugin-comment-content/
layout: plugin
plugin:
name: talk-plugin-comment-content
default: true
provides:
- Client
---
@@ -12,4 +11,4 @@ plugin:
Pluginizes the text of a comment to support custom treatment of this text. This
plugin currently parses the given text to see if it contains a link, and makes
them clickable using
[react-linkify](https://www.npmjs.com/package/react-linkify).
[react-linkify](https://www.npmjs.com/package/react-linkify).
+31 -12
View File
@@ -11,25 +11,44 @@ plugin:
- Client
---
Enables sign-in via Facebook via the server side passport middleware.
Enables sign-in via Facebook via the server side passport middleware. Requires creating and registering a login app with Facebook.
Configuration:
**Configuration:**
- `TALK_FACEBOOK_APP_ID` (**required**) - The Facebook App ID for your Facebook
Login enabled app. You can learn more about getting a Facebook App ID at the
[Facebook Developers Portal](https://developers.facebook.com) or by visiting
the [Creating an App ID](https://developers.facebook.com/docs/apps/register)
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
Login enabled app. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
- `TALK_FACEBOOK_APP_SECRET` (**required**) - The Facebook App Secret for your
Facebook Login enabled app. You can learn more about getting a Facebook App
Secret at the [Facebook Developers Portal](https://developers.facebook.com)
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register)
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
Facebook Login enabled app. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
You can learn more about getting a Facebook App ID at the
[Facebook Developers Portal](https://developers.facebook.com) or by visiting
their [Creating an App ID](https://developers.facebook.com/docs/apps/register)
guide.
_NOTE: FabceBook auth requires your site to use `https` (SSL) not `http`. If your site is not `https` you can not use this plugin!_
**Setting up your Facebook app:**
* Go to [Facebook Developers Portal](https://developers.facebook.com) and click on Getting Started or My Apps
* Create a new app > set the App Name and Email to create an app id
* Confirm that you are not a robot, then configure the app as follows:
* In Settings > Basic:
* add app domains (your Talk domain)
* add a link to your privacy policy
* add a link to your terms of service
* In Settings > Advanced:
* turn on "Require App Secret"
* Add a "Product" (Under "Products" click + to add a Product):
* Setup "Facebook Login"
* choose `www`
* enter your Talk domain url
* click _Next_ several times to get through the add code steps (You do not need to modify any code, the plugin takes care of this part for you.)
* Under Product Settings:
* set Valid OAuth Redirect URIs to your callback url (Use your Talk domain with this endpoint: `/api/v1/auth/facebook/callback`)
* Locate your App Id and App Secret, set these as config vars on your instance of Talk
* Toggle the "Live" button on the top bar to make app live
_NOTE: Facebook auth requires your site to use `https` (SSL) not `http`. If your site is not `https` you can not use this plugin!_
## GDPR Compliance
@@ -14,6 +14,17 @@ Enables the ability for Moderators to feature and un-feature comments via the
Stream and the Admin. Featured comments show in a first-place tab on the Stream
if there are any featured comments on that story.
When paired with the [talk-plugin-moderator-actions](/talk/plugin/talk-plugin-moderator-actions)
When paired with the [talk-plugin-moderation-actions](/talk/plugin/talk-plugin-moderation-actions)
plugin, moderators will have the option of featuring comments from the comment
stream.
stream.
## Default Comments Tab
You can also change the default tab when rendering the comment stream to the
featured tab so when a user reaches a comment stream, they will see the featured
comments first by setting the [TALK_DEFAULT_STREAM_TAB](/talk/advanced-configuration/#talk-default-stream-tab)
variable to `talk-plugin-featured-comments`.
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild](/talk/installation-from-docker/#onbuild)
image you can specify it with `--build-arg TALK_DEFAULT_STREAM_TAB=talk-plugin-featured-comments`.
@@ -42,19 +42,10 @@
text-align: left;
letter-spacing: 0.1px;
margin: 0;
quotes: '\201c' '\201d';
margin-bottom: 10px;
word-wrap: break-word;
}
.quote:before {
content: open-quote;
}
.quote:after {
content: close-quote;
}
.footer {
display: flex;
}
@@ -1,9 +1,9 @@
---
title: talk-plugin-moderator-actions
permalink: /plugin/talk-plugin-moderator-actions/
title: talk-plugin-moderation-actions
permalink: /plugin/talk-plugin-moderation-actions/
layout: plugin
plugin:
name: talk-plugin-moderator-actions
name: talk-plugin-moderation-actions
default: true
provides:
- Client
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-category-featured/
layout: plugin
plugin:
name: talk-plugin-notifications-category-featured
default: true
depends:
- name: talk-plugin-notifications
- name: talk-plugin-featured-comments
@@ -14,4 +15,4 @@ plugin:
When a comment is featured (via the
[talk-plugin-featured-comments](/talk/plugin/talk-plugin-featured-comments)
plugin), the user will receive a notification email.
plugin), the user will receive a notification email.
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-moderation-actions/
layout: plugin
plugin:
name: talk-plugin-notifications-moderation-actions
default: true
depends:
- name: talk-plugin-notifications
provides:
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-category-reply/
layout: plugin
plugin:
name: talk-plugin-notifications-category-reply
default: true
depends:
- name: talk-plugin-notifications
provides:
@@ -12,4 +13,4 @@ plugin:
---
Replies made to each user will trigger an email to be sent with the notification
details if enabled.
details if enabled.
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-category-staff/
layout: plugin
plugin:
name: talk-plugin-notifications-category-staff
default: true
depends:
- name: talk-plugin-notifications
provides:
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-digest-daily/
layout: plugin
plugin:
name: talk-plugin-notifications-digest-daily
default: true
depends:
- name: talk-plugin-notifications
provides:
@@ -13,4 +14,4 @@ plugin:
Enables a digesting option for users to digest their notifications on an `DAILY`
basis, where the notification batching occurs every day at midnight in the
`America/New_York` timezone.
`America/New_York` timezone.
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-digest-hourly/
layout: plugin
plugin:
name: talk-plugin-notifications-digest-hourly
default: true
depends:
- name: talk-plugin-notifications
provides:
@@ -13,4 +14,4 @@ plugin:
Enables a digesting option for users to digest their notifications on an `HOURLY`
basis, where the notification batching occurs every hour in the
`America/New_York` timezone.
`America/New_York` timezone.
+2 -1
View File
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications/
layout: plugin
plugin:
name: talk-plugin-notifications
default: true
provides:
- Server
- Client
@@ -28,4 +29,4 @@ line by adjusting the translation, Talk's default behavior is to add a prefix
before the subject of each email sent. This is always set to the
[TALK-EMAIL-SUBJECT-PREFIX](/talk/advanced-configuration/#TALK-EMAIL-SUBJECT-PREFIX)
configuration variable. You should change this parameter if you want to affect
how the subject is rendered.
how the subject is rendered.
+1 -2
View File
@@ -4,11 +4,10 @@ permalink: /plugin/talk-plugin-offtopic/
layout: plugin
plugin:
name: talk-plugin-offtopic
default: true
provides:
- Server
- Client
---
Allows the comment authors to tag their comment as `Off-Topic` which will add a
visible badge on the frontend to other users that their comment is off-topic.
visible badge on the frontend to other users that their comment is off-topic.
@@ -41,7 +41,7 @@ type CancelAccountDeletionResponse implements Response {
errors: [UserError!]
}
# DownloadUserResponse contaisn the account download archiveURL that can be used
# DownloadUserResponse contains the account download archiveURL that can be used
# to directly download a zip file containing the user data.
type DownloadUserResponse implements Response {
+1
View File
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-rich-text/
layout: plugin
plugin:
name: talk-plugin-rich-text
default: true
provides:
- Client
- Server
@@ -22,7 +22,7 @@ class Editor extends React.Component {
};
getHTML(props = this.props) {
if (props.input.richTextBody) {
if (props.input.richTextBody !== undefined) {
return props.input.richTextBody;
}
return (
@@ -11,7 +11,7 @@ plugin:
Enables all new comments that are written to be posted to a Slack channel as
well. Configure an
[Incoming Webhook](https://api.slack.com/incoming-webhooks)
app and provide that url in the form of the `SLACK_WEBHOOK_URL`
app and provide that url in the form of the `TALK_SLACK_WEBHOOK_URL`
detailed below.
*Warning: On high volume sites, this means every single comment will flow into
@@ -20,5 +20,5 @@ recipe to further customize the behavior*.
Configuration:
- `SLACK_WEBHOOK_URL` (**required**) - The webhook url that will be
used to post new comments to.
- `TALK_SLACK_WEBHOOK_URL` (**required**) - The webhook url that will be
used to post new comments to.
+20
View File
@@ -0,0 +1,20 @@
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
return res.render('dev/amp.njk', {
title: 'Coral Talk AMP',
asset_url: '',
asset_id: '',
});
});
router.get('/button', (req, res) => {
return res.render('dev/amp-button.njk', {
title: 'Coral Talk AMP',
asset_url: '',
asset_id: '',
});
});
module.exports = router;
+1
View File
@@ -19,5 +19,6 @@ router.get('/', staticTemplate, async (req, res) => {
});
}
});
router.use('/amp', staticTemplate, require('./amp'));
module.exports = router;
+4
View File
@@ -5,4 +5,8 @@ router.use('/stream', (req, res) => {
res.render('embed/stream.njk');
});
router.use('/amp', (req, res) => {
res.render('embed/amp.njk');
});
module.exports = router;
+14 -5
View File
@@ -22,6 +22,10 @@ const {
ROOT_URL,
RECAPTCHA_WINDOW,
RECAPTCHA_INCORRECT_TRIGGER,
USERNAME_CAST_REGEXP,
USERNAME_REPLACEMENT_CAST_REGEXP,
USERNAME_REPLACEMENT_CHARACTER,
USERNAME_VALIDATION_REGEX,
} = require('../config');
const { jwt: JWT_SECRET } = require('../secrets');
const debug = require('debug')('talk:services:users');
@@ -525,7 +529,10 @@ class Users {
}
static castUsername(username) {
return username.replace(/ /g, '_').replace(/[^a-zA-Z_]/g, '');
return username
.trim()
.replace(USERNAME_REPLACEMENT_CAST_REGEXP, USERNAME_REPLACEMENT_CHARACTER)
.replace(USERNAME_CAST_REGEXP, '');
}
/**
@@ -554,7 +561,11 @@ class Users {
for (let i = 0; i < MAX_ATTEMPTS; i++) {
// Generate `GROUP_ATTEMPTS` guesses for the username.
const usernameGuesses = Array.from(Array(GROUP_ATTEMPTS)).map(
() => `${castedName}_${random(0, END_NUMBER_MAX)}`
() =>
`${castedName}${USERNAME_REPLACEMENT_CHARACTER}${random(
0,
END_NUMBER_MAX
)}`
);
// Map them all to lowercase.
@@ -684,13 +695,11 @@ class Users {
* @return {Promise}
*/
static async isValidUsername(username, checkAgainstWordlist = true) {
const onlyLettersNumbersUnderscore = /^[A-Za-z0-9_]+$/;
if (!username) {
throw new ErrMissingUsername();
}
if (!onlyLettersNumbersUnderscore.test(username)) {
if (!USERNAME_VALIDATION_REGEX.test(username)) {
throw new ErrSpecialChars();
}
+1 -1
View File
@@ -97,7 +97,7 @@ module.exports = {
elements: {
logoutButton: '.talk-stream-userbox-logout',
signInButton: '#coralSignInButton',
commentBoxTextarea: '.talk-plugin-commentbox-textarea',
commentBoxTextarea: '.talk-plugin-rich-text-content',
commentBoxPostButton: '.talk-plugin-commentbox-button',
firstComment: '.talk-stream-comment.talk-stream-comment-level-0',
firstCommentContent:
+2 -1
View File
@@ -16,7 +16,7 @@ describe('graph.mutations.createComment', () => {
beforeEach(() => SettingsService.init());
const query = `
mutation CreateComment($input: CreateCommentInput = {asset_id: 123, body: "Here's my comment!"}) {
mutation CreateComment($input: CreateCommentInput = {asset_id: 123, body: "Here's my comment!", richTextBody: "Here's my comment!"}) {
createComment(input: $input) {
comment {
id
@@ -293,6 +293,7 @@ describe('graph.mutations.createComment', () => {
input: {
asset_id: '123',
body,
richTextBody: 'This is such a nice comment!',
},
});
+23 -3
View File
@@ -39,10 +39,12 @@ describe('graph.mutations.editComment', () => {
it('a user can edit their own comment', async () => {
const context = new Context({ user });
const testStartedAt = new Date();
const body = `hello there! ${String(Math.random()).slice(2)}`;
const comment = await CommentsService.publicCreate({
asset_id: asset.id,
author_id: user.id,
body: `hello there! ${String(Math.random()).slice(2)}`,
body,
richTextBody: body,
});
// body_history should be there
@@ -58,6 +60,7 @@ describe('graph.mutations.editComment', () => {
asset_id: asset.id,
edit: {
body: newBody,
richTextBody: newBody,
},
});
if (response.errors && response.errors.length) {
@@ -86,10 +89,12 @@ describe('graph.mutations.editComment', () => {
});
it("A user can't edit their comment outside of the edit comment time window", async () => {
const body = `hello there! ${String(Math.random()).slice(2)}`;
const comment = await CommentsService.publicCreate({
asset_id: asset.id,
author_id: user.id,
body: `hello there! ${String(Math.random()).slice(2)}`,
body,
richTextBody: body,
});
const now = new Date();
@@ -103,6 +108,7 @@ describe('graph.mutations.editComment', () => {
asset_id: asset.id,
edit: {
body: newBody,
richTextBody: newBody,
},
});
if (response.errors && response.errors.length > 0) {
@@ -120,10 +126,12 @@ describe('graph.mutations.editComment', () => {
});
it("A user can't edit someone else's comment", async () => {
const body = `hello there! ${String(Math.random()).slice(2)}`;
const comment = await CommentsService.publicCreate({
asset_id: asset.id,
author_id: user.id,
body: `hello there! ${String(Math.random()).slice(2)}`,
body,
richTextBody: body,
});
const ctx = Context.forSystem();
@@ -140,6 +148,7 @@ describe('graph.mutations.editComment', () => {
asset_id: asset.id,
edit: {
body: newBody,
richTextBody: newBody,
},
});
expect(response.errors).to.be.empty;
@@ -162,6 +171,7 @@ describe('graph.mutations.editComment', () => {
asset_id: asset.id,
edit: {
body: newBody,
richTextBody: newBody,
},
});
if (response.errors && response.errors.length > 0) {
@@ -182,10 +192,12 @@ describe('graph.mutations.editComment', () => {
},
beforeEdit: {
body: 'I was offensive and thus REJECTED',
richTextBody: 'I was offensive and thus REJECTED',
status: 'REJECTED',
},
edit: {
body: 'I have been edited to be less offensive',
richTextBody: 'I have been edited to be less offensive',
},
error: true,
},
@@ -200,10 +212,12 @@ describe('graph.mutations.editComment', () => {
},
beforeEdit: {
body: "I'm a perfectly acceptable comment",
richTextBody: "I'm a perfectly acceptable comment",
status: 'ACCEPTED',
},
edit: {
body: `I have been sneakily edited to add a banned word: ${bannedWord}`,
richTextBody: `I have been sneakily edited to add a banned word: ${bannedWord}`,
},
afterEdit: {
status: 'REJECTED',
@@ -220,10 +234,12 @@ describe('graph.mutations.editComment', () => {
},
beforeEdit: {
body: `I'm a rejected comment with bad word ${bannedWord}`,
richTextBody: `I'm a rejected comment with bad word ${bannedWord}`,
status: 'REJECTED',
},
edit: {
body: 'I have been edited to remove the bad word',
richTextBody: 'I have been edited to remove the bad word',
},
error: true,
},
@@ -236,10 +252,13 @@ describe('graph.mutations.editComment', () => {
},
beforeEdit: {
body: "I'm a perfectly acceptable comment",
richTextBody: "I'm a perfectly acceptable comment",
status: 'ACCEPTED',
},
edit: {
body: 'I have been edited to add a link: https://coralproject.net/',
richTextBody:
'I have been edited to add a link: https://coralproject.net/',
},
afterEdit: {
status: 'SYSTEM_WITHHELD',
@@ -266,6 +285,7 @@ describe('graph.mutations.editComment', () => {
asset_id: asset.id,
edit: {
body: newBody,
richTextBody: newBody,
},
});
+1
View File
@@ -30,6 +30,7 @@ describe('graph.queries.settings', () => {
questionBoxIcon
autoCloseStream
customCssUrl
customAdminCssUrl
closedTimeout
closedMessage
charCountEnable
+6
View File
@@ -2,10 +2,16 @@
{% block title %}Talk Admin{% endblock %}
{# Favicon Configuration #}
{% include "partials/favicon.njk" %}
{% block css %}
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<link href="https://code.getmdl.io/1.2.1/material.min.css" rel="stylesheet">
<link href="{{ resolve('coral-admin/bundle.css') }}" rel="stylesheet">
{# Custom CSS is included after the CSS block so that its overrides will apply #}
{% include "partials/custom-admin-css.njk" %}
{% endblock %}
{% block js %}
+73
View File
@@ -0,0 +1,73 @@
<!-- ## Introduction -->
<!--
This is a sample showing how to use Talk with AMP. You need to access
this using an URL other than localhost. You can use ngrok to achieve that.
-->
<!-- -->
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<link rel="canonical" href="/dev">
<!-- ## Setup -->
<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>
<title>Coral Talk AMP</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<style amp-custom>
.container {
width: auto;
max-width: 680px;
padding: 0 15px;
margin: auto;
}
.title {
margin-top: 1rem;
margin-bottom: .5rem;
font-size: 2.5rem;
font-weight: 500;
line-height: 1.2;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
}
.hide{
display:none
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">Coral Talk AMP</h1>
<p>
Ask to go outside and ask to come inside and ask to go outside and ask to
come inside the dog smells bad. Lick butt and make a weird face. Toilet
paper attack claws fluff everywhere meow miao french ciao litterbox. Shake
treat bag immediately regret falling into bathtub or white cat sleeps on a
black shirt so what a cat-ass-trophy! eat owner's food spit up on light
gray carpet instead of adjacent linoleum. Warm up laptop with butt lick
butt fart rainbows until owner yells pee in litter box hiss at cats
scratch the box so loved it, hated it, loved it, hated it but need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me.
</p>
<button id=menu on="tap:AMP.setState({visible: !visible})">Show Comments</button>
<div [class]=visible?"show":"hide" class="hide">
<amp-iframe
width=600 height=140
layout="responsive"
sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
resizable
src="http://localhost:3000/embed/amp">
<div placeholder></div>
<div overflow tabindex=0 role=button aria-label="Read more">Read more</div>
</amp-iframe>
</div<
</div>
</body>
</html>
+151
View File
@@ -0,0 +1,151 @@
<!-- ## Introduction -->
<!--
This is a sample showing how to use Talk with AMP. You need to access
this using an URL other than localhost. You can use ngrok to achieve that.
-->
<!-- -->
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<link rel="canonical" href="/dev">
<!-- ## Setup -->
<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
<title>Coral Talk AMP</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<style amp-custom>
.container {
width: auto;
max-width: 680px;
padding: 0 15px;
margin: auto;
}
.title {
margin-top: 1rem;
margin-bottom: .5rem;
font-size: 2.5rem;
font-weight: 500;
line-height: 1.2;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">Coral Talk AMP</h1>
<p>
Dismember a mouse and then regurgitate parts of it on the family room
floor. Dont wait for the storm to pass, dance in the rain stand in front
of the computer screen, so stares at human while pushing stuff off a table
chew the plant meow hiss at vacuum cleaner. Terrorize the
hundred-and-twenty-pound rottweiler and steal his bed, not sorry chew the
plant. Litter kitter kitty litty little kitten big roar roar feed me rub
whiskers on bare skin act innocent sleep on keyboard, so give me attention
or face the wrath of my claws for demand to be let outside at once, and
expect owner to wait for me as i think about it spread kitty litter all
over house so nya nya nyan. Catty ipsum massacre a bird in the living room
and then look like the cutest and most innocent animal on the planet you
have cat to be kitten me right meow. Hiss and stare at nothing then run
suddenly away refuse to come home when humans are going to bed; stay out
all night then yowl like i am dying at 4am and lick plastic bags. Chase
dog then run away purrr purr littel cat, little cat purr purr and step on
your keyboard while you're gaming and then turn in a circle . Twitch tail
in permanent irritation put butt in owner's face and the dog smells bad
yet attempt to leap between furniture but woefully miscalibrate and
bellyflop onto the floor; what's your problem? i meant to do that now i
shall wash myself intently. Sniff all the things groom forever, stretch
tongue and leave it slightly out, blep, but bring your owner a dead bird
decide to want nothing to do with my owner today for lay on arms while
you're using the keyboard meow meow, i tell my human or scratch. Sleep on
my human's head then cats take over the world bleghbleghvomit my furball
really tie the room together sleep more napping, more napping all the
napping is exhausting. When in doubt, wash drink water out of the faucet,
cats are fats i like to pets them they like to meow back and cat dog hate
mouse eat string barf pillow no baths hate everything yet swat at dog
kitty kitty but you call this cat food. Cough furball into food bowl then
scratch owner for a new one flex claws on the human's belly and purr like
a lawnmower for has closed eyes but still sees you groom yourself 4 hours
- checked, have your beauty sleep 18 hours - checked, be fabulous for the
rest of the day - checked. Freak human out make funny noise mow mow mow
mow mow mow success now attack human flex claws on the human's belly and
purr like a lawnmower or meowwww. Terrorize the hundred-and-twenty-pound
rottweiler and steal his bed, not sorry paw at your fat belly so yowling
nonstop the whole night small kitty warm kitty little balls of fur or eat
owner's food reward the chosen human with a slow blink. Gate keepers of
hell plan steps for world domination for more napping, more napping all
the napping is exhausting give me some of your food give me some of your
food give me some of your food meh, i don't want it so flop over. Make
meme, make cute face ears back wide eyed so sit and stare. Dead stare with
ears cocked furrier and even more furrier hairball. Stand in front of the
computer screen demand to have some of whatever the human is cooking, then
sniff the offering and walk away for catasstrophe, kitty scratches couch
bad kitty. Wack the mini furry mouse intrigued by the shower, and pooping
rainbow while flying in a toasted bread costume in space. Mesmerizing
birds love me! shake treat bag, yet lies down where is my slave? I'm
getting hungry so lick face hiss at owner, pee a lot, and meow repeatedly
scratch at fence purrrrrr eat muffins and poutine until owner comes back.
You have cat to be kitten me right meow sniff other cat's butt and hang
jaw half open thereafter but run outside as soon as door open so munch on
tasty moths or munch on tasty moths, for paw at beetle and eat it before
it gets away. Sit on human. Gnaw the corn cob massacre a bird in the
living room and then look like the cutest and most innocent animal on the
planet for sit on the laptop. Meow scratch leg; meow for can opener to
feed me cat fur is the new black but hide when guests come over, and Gate
keepers of hell. Refuse to come home when humans are going to bed; stay
out all night then yowl like i am dying at 4am cat slap dog in face or eat
a rug and furry furry hairs everywhere oh no human coming lie on counter
don't get off counter for i like fish sit on human they not getting up
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.
</p>
<p>
I show my fluffy belly but it's a trap! if you pet it i will tear up your
hand refuse to drink water except out of someone's glass mice, so cough
hairball, eat toilet paper or curl into a furry donut lick sellotape but
wack the mini furry mouse. When owners are asleep, cry for no apparent
reason. Chase imaginary bugs. Stinky cat reward the chosen human with a
slow blink, or chase dog then run away. Chew on cable scratch the
furniture for you are a captive audience while sitting on the toilet, pet
me for i like cats because they are fat and fluffy and spend all night
ensuring people don't sleep sleep all day. Scoot butt on the rug need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me, leave fur on owners clothes, so instantly
break out into full speed gallop across the house for no reason play
riveting piece on synthesizer keyboard and scoot butt on the rug yet meow
meow. Attack dog, run away and pretend to be victim annoy the old grumpy
cat, start a fight and then retreat to wash when i lose or meow go back to
sleep owner brings food and water tries to pet on head, so scratch get
sprayed by water because bad cat. Meowwww pelt around the house and up and
down stairs chasing phantoms drink water out of the faucet meow meow, i
tell my human. Destroy couch.
</p>
<p>
Ask to go outside and ask to come inside and ask to go outside and ask to
come inside the dog smells bad. Lick butt and make a weird face. Toilet
paper attack claws fluff everywhere meow miao french ciao litterbox. Shake
treat bag immediately regret falling into bathtub or white cat sleeps on a
black shirt so what a cat-ass-trophy! eat owner's food spit up on light
gray carpet instead of adjacent linoleum. Warm up laptop with butt lick
butt fart rainbows until owner yells pee in litter box hiss at cats
scratch the box so loved it, hated it, loved it, hated it but need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me.
</p>
<amp-iframe
width=600 height=140
layout="responsive"
sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
resizable
src="http://localhost:{{ PORT }}/embed/amp">
<div placeholder></div>
<div overflow tabindex=0 role=button aria-label="Read more">Read more</div>
</amp-iframe>
</div>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>Coral Talk Amp Embed</title>
<style>body { margin: 0; }</style>
</head>
<body>
<div id='coralStreamEmbed'></div>
<script src="{{ resolve('embed.js') }}"></script>
<script>
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
talk: '{{ BASE_URL }}',
auth_token: '',
amp: true,
})
</script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "templates/base.njk" %}
{% block title %}Talk - Login{% endblock %}
{% block title %}Login{% endblock %}
{% block css %}
<link href="{{ resolve('coral-login/bundle.css')}}" rel="stylesheet">
+3
View File
@@ -0,0 +1,3 @@
{% if customAdminCssUrl %}
<link nonce="{{ nonce }}" href="{{ customAdminCssUrl }}" rel="stylesheet">
{% endif %}
-3
View File
@@ -6,9 +6,6 @@
<meta name="viewport" content="width=device-width, user-scalable=no">
{% block meta %}{% endblock %}
{# Favicon Configuration #}
{% include "partials/favicon.njk" %}
{# CSP and security headers #}
{% block security %}{% endblock %}