mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08f579225b | ||
|
|
46987b6303 | ||
|
|
c2d07ec79f | ||
|
|
a98f5ef980 | ||
|
|
d6515e23f8 | ||
|
|
a3db05845d | ||
|
|
c8fc419cec | ||
|
|
677c8a0828 | ||
|
|
08146fd979 | ||
|
|
ab92fe3fe8 | ||
|
|
95435f0793 | ||
|
|
7eed540671 | ||
|
|
374951bd33 | ||
|
|
42a694f56e | ||
|
|
1c997aea31 | ||
|
|
44830f6cb1 | ||
|
|
22d263c6d2 | ||
|
|
78ba82cc05 | ||
|
|
a6b9cc57ac | ||
|
|
cd9cb56844 | ||
|
|
d331bdee18 | ||
|
|
d6f6fbcd9c | ||
|
|
1ed5df71da | ||
|
|
8b1b61f695 | ||
|
|
f1a3a5ca28 | ||
|
|
8ef44dc6f5 | ||
|
|
362f29f77e | ||
|
|
08d342ea2e | ||
|
|
99777d6b49 | ||
|
|
6f6da1fa25 | ||
|
|
2fac304163 | ||
|
|
5ca7c092db | ||
|
|
cfae6b5374 | ||
|
|
3668a2cffa | ||
|
|
92681f52aa | ||
|
|
847ded5bb4 | ||
|
|
76c959f790 | ||
|
|
4659392ad3 | ||
|
|
e9cec73966 | ||
|
|
bac0ee9c3b | ||
|
|
5ae293af89 | ||
|
|
cce464b82a | ||
|
|
5d32c4ec03 | ||
|
|
50d4e2bb9b | ||
|
|
debdd3eae4 | ||
|
|
286d9baddf | ||
|
|
9c06a63afd | ||
|
|
f38e72c4df | ||
|
|
e2fac4a2dc | ||
|
|
ba0513fd6c | ||
|
|
8e29f0094c | ||
|
|
f60c7e3ca6 | ||
|
|
b3c034d89d | ||
|
|
be2657eb30 | ||
|
|
fd32d2aa24 | ||
|
|
809b39ae2a | ||
|
|
6197981480 | ||
|
|
eb58dd52f8 | ||
|
|
f5c011f44b | ||
|
|
396ccbd6c2 | ||
|
|
4fd61b3731 | ||
|
|
cbd795dc95 | ||
|
|
65a6b484a9 |
@@ -41,9 +41,9 @@ integration_job: &integration_job
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- <<: *create_indexes
|
||||
- run:
|
||||
name: Setup the database with defaults
|
||||
command: ./bin/cli setup --defaults
|
||||
# - run:
|
||||
# name: Setup the database with defaults
|
||||
# command: ./bin/cli setup --defaults
|
||||
- run:
|
||||
name: Run the integration tests
|
||||
command: bash .circleci/e2e.sh
|
||||
|
||||
@@ -11,6 +11,7 @@ ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_WHITELISTED_LANGUAGES
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
|
||||
ONBUILD ARG TALK_DEFAULT_LAZY_RENDER
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
+37
-31
@@ -45,11 +45,10 @@ const performSetup = async () => {
|
||||
} catch (err) {
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (err instanceof ErrSettingsNotInit) {
|
||||
if (!err instanceof ErrSettingsNotInit) {
|
||||
throw err;
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (program.defaults) {
|
||||
@@ -138,7 +137,7 @@ const performSetup = async () => {
|
||||
|
||||
console.log("\nWe'll ask you some questions about your first admin user.\n");
|
||||
|
||||
let user = await inquirer.prompt([
|
||||
let { username, email } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
@@ -161,39 +160,46 @@ const performSetup = async () => {
|
||||
return 'Email is required';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: password => {
|
||||
return UsersService.isValidPassword(password).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword, { password }) => {
|
||||
if (password !== confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
return UsersService.isValidPassword(confirmPassword).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
let password = '';
|
||||
while (!password) {
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: password => {
|
||||
try {
|
||||
UsersService.isValidPassword(password);
|
||||
} catch (err) {
|
||||
throw err.message;
|
||||
}
|
||||
|
||||
return password;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
},
|
||||
]);
|
||||
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
console.error('Passwords do not match');
|
||||
} else {
|
||||
password = answers.password;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
let { user: newUser } = await SetupService.setup(ctx, {
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
|
||||
smoothscroll.polyfill();
|
||||
|
||||
if (!NodeList.prototype.forEach) {
|
||||
// Polyfill IE11 missing forEach in NodeList.
|
||||
NodeList.prototype.forEach = Array.prototype.forEach;
|
||||
}
|
||||
|
||||
function init({ store, localStorage }) {
|
||||
const shouldHide = localStorage.getItem('coral:shortcutsNote') === 'hide';
|
||||
if (shouldHide) {
|
||||
|
||||
@@ -62,13 +62,19 @@
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
overflow-wrap: break-word;
|
||||
word-break:break-word;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
/*
|
||||
IE11 fix – Next line was supposed to be:
|
||||
max-width: 500px;
|
||||
*/
|
||||
padding-right: 20px;
|
||||
/** IE11 fix end **/
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import QuestionBox from '../../../components/QuestionBox';
|
||||
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './QuestionBoxBuilder.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
@@ -12,6 +13,7 @@ const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote'];
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
questionBoxIcon,
|
||||
questionBoxContent,
|
||||
onContentChange,
|
||||
@@ -20,7 +22,7 @@ class QuestionBoxBuilder extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<h4>Include an Icon</h4>
|
||||
<h4>{title}</h4>
|
||||
|
||||
<ul className={styles.iconList}>
|
||||
{icons.map(item => {
|
||||
@@ -53,4 +55,12 @@ class QuestionBoxBuilder extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
QuestionBoxBuilder.propTypes = {
|
||||
title: PropTypes.string,
|
||||
questionBoxIcon: PropTypes.string,
|
||||
questionBoxContent: PropTypes.string,
|
||||
onContentChange: PropTypes.func,
|
||||
onIconChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default QuestionBoxBuilder;
|
||||
|
||||
@@ -69,6 +69,7 @@ class Settings extends React.Component {
|
||||
{questionBoxEnable && (
|
||||
<div className={styles.questionBoxContainer}>
|
||||
<QuestionBoxBuilder
|
||||
title={t('configure.include_an_icon')}
|
||||
questionBoxIcon={questionBoxIcon}
|
||||
questionBoxContent={questionBoxContent}
|
||||
onIconChange={onQuestionBoxIconChange}
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import queryString from 'querystringify';
|
||||
import URLSearchParams from '@ungap/url-search-params';
|
||||
import pym from 'pym.js';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import { buildUrl } from 'coral-framework/utils/url';
|
||||
import SnackBar from './SnackBar';
|
||||
import onIntersect from './onIntersect';
|
||||
import {
|
||||
createStorage,
|
||||
connectStorageToPym,
|
||||
} from 'coral-framework/services/storage';
|
||||
|
||||
// Rebuild the origin if it isn't defined. This is our poor-mans polyfill
|
||||
// for the location APIs.
|
||||
if (!window.location.origin) {
|
||||
window.location.origin = `${window.location.protocol}//${
|
||||
window.location.hostname
|
||||
}${window.location.port ? `:${window.location.port}` : ''}`;
|
||||
}
|
||||
|
||||
const NOTIFICATION_OFFSET = 200;
|
||||
|
||||
// Ensure there is a trailing slash.
|
||||
function ensureEndSlash(p) {
|
||||
return p.match(/\/$/) ? p : `${p}/`;
|
||||
}
|
||||
|
||||
// Build the URL to load in the pym iframe.
|
||||
function buildStreamIframeUrl(talkBaseUrl, query) {
|
||||
let url = talkBaseUrl + 'embed/stream?';
|
||||
|
||||
url += queryString.stringify(query);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// detectAssetURL will try to grab the canonical url from the head, if it isn't
|
||||
// available, source the url from the current one.
|
||||
function detectAssetURL() {
|
||||
try {
|
||||
// Try to get the url from the canonical tag on the page.
|
||||
return document.querySelector('link[rel="canonical"]').href;
|
||||
} catch (e) {
|
||||
window.console.warn(
|
||||
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
|
||||
);
|
||||
|
||||
return window.location.origin + window.location.pathname;
|
||||
}
|
||||
}
|
||||
|
||||
function buildQuery({ asset_id, asset_url }) {
|
||||
// Compose the query to send down to the Talk API so it knows what to load.
|
||||
const query = {};
|
||||
|
||||
// Parse the url parameters to extract some of the information.
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
|
||||
// Pull the Comment ID out of the query string.
|
||||
const commentID = search.get('commentId') || search.get('commentID');
|
||||
if (commentID) {
|
||||
query.comment_id = commentID;
|
||||
}
|
||||
|
||||
// Insert the asset_id into the query.
|
||||
if (asset_id) {
|
||||
query.asset_id = asset_id;
|
||||
}
|
||||
|
||||
// If the asset_url is defined, use it, otherwise, detect it.
|
||||
if (asset_url) {
|
||||
query.asset_url = asset_url;
|
||||
} else {
|
||||
query.asset_url = detectAssetURL();
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
// Get dimensions of viewport.
|
||||
function viewportDimensions() {
|
||||
let target = window;
|
||||
let prefix = 'inner';
|
||||
if (!('innerWidth' in window)) {
|
||||
prefix = 'client';
|
||||
target = document.documentElement || document.body;
|
||||
}
|
||||
|
||||
return {
|
||||
width: target[`${prefix}Width`],
|
||||
height: target[`${prefix}Height`],
|
||||
};
|
||||
}
|
||||
|
||||
export default class Bridge {
|
||||
constructor(
|
||||
element,
|
||||
{
|
||||
// Pull out the URLs used to setup Talk.
|
||||
talk: talkBaseUrl,
|
||||
talkStaticUrl = talkBaseUrl,
|
||||
// Default the following to null.
|
||||
events = null,
|
||||
snackBarStyles = null,
|
||||
onAuthChanged = null,
|
||||
// Determine if we're in lazy mode or not. By default, the build argument
|
||||
// will determine the lazy render status. This default is primarily used
|
||||
// when the embed code cannot be changed, but control of the Talk serving
|
||||
// domain is available.
|
||||
lazy = process.env.TALK_DEFAULT_LAZY_RENDER === 'TRUE',
|
||||
// Any additional options are extracted to be sent to the embed via the
|
||||
// pym bridge.
|
||||
...opts
|
||||
}
|
||||
) {
|
||||
this.pym = null;
|
||||
this.element = element;
|
||||
this.opts = opts;
|
||||
this.query = buildQuery(this.opts);
|
||||
this.emitter = new EventEmitter({ wildcard: true });
|
||||
this.snackBar = 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.
|
||||
this.queued = [];
|
||||
|
||||
// Attach to the events emitted by the pym parent.
|
||||
if (events) {
|
||||
events(this.emitter);
|
||||
}
|
||||
|
||||
// Start the embed loading process.
|
||||
if (this.lazy) {
|
||||
// When the dom element containing the talk embed container is in view,
|
||||
// render the stream with force turned on so that it skips this portion.
|
||||
onIntersect(this.element, () => this.load());
|
||||
} else {
|
||||
// We aren't being lazy, load it now!
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
ensureRendered() {
|
||||
// Check to see if the pym bridge is created, and the embed is loaded.
|
||||
if (this.pym === null) {
|
||||
throw new Error('Stream Embed must be rendered first');
|
||||
}
|
||||
}
|
||||
|
||||
queueWhenRendered(callback) {
|
||||
// Check to see if the queue is alive, if it isn't, run the callback now,
|
||||
// otherwise, push the callback to be processed when the stream has loaded.
|
||||
if (this.queued !== null) {
|
||||
this.queued.push(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
setupPym() {
|
||||
const url = buildStreamIframeUrl(this.talkBaseUrl, this.query);
|
||||
this.pym = new pym.Parent(this.element.id, url, {
|
||||
title: this.opts.title,
|
||||
id: `${this.element.id}_iframe`,
|
||||
name: `${this.element.id}_iframe`,
|
||||
});
|
||||
|
||||
// NOTE: Workaround for iOS Safari which ignores `width` but respects `min-width` value.
|
||||
this.pym.el.firstChild.style.width = '1px';
|
||||
this.pym.el.firstChild.style.minWidth = '100%';
|
||||
|
||||
// Resize parent iframe height when child height changes
|
||||
let cachedHeight;
|
||||
this.pym.onMessage('height', height => {
|
||||
if (height !== cachedHeight) {
|
||||
this.pym.el.firstChild.style.height = `${height}px`;
|
||||
cachedHeight = height;
|
||||
}
|
||||
});
|
||||
|
||||
// Send the config back over the pym bridge if requested.
|
||||
this.pym.onMessage('getConfig', () => {
|
||||
this.pym.sendMessage('config', JSON.stringify(this.opts));
|
||||
});
|
||||
|
||||
// If the auth changes, and someone is listening for it, then re-emit it.
|
||||
if (this.onAuthChanged) {
|
||||
this.pym.onMessage('coral-auth-changed', message => {
|
||||
this.onAuthChanged(message ? JSON.parse(message) : null);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the permalink comment id from the search.
|
||||
this.pym.onMessage('coral-view-all-comments', () => {
|
||||
const query = queryString.parse(location.search);
|
||||
|
||||
// Remove the commentId/commentID url param.
|
||||
delete query.commentId;
|
||||
delete query.commentID;
|
||||
|
||||
// Rebuild the search field without the commentId in it.
|
||||
const search = queryString.stringify(query);
|
||||
|
||||
// Change the url.
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
buildUrl({ ...location, search })
|
||||
);
|
||||
});
|
||||
|
||||
// Remove the permalink comment id from the hash.
|
||||
this.pym.onMessage('coral-view-comment', id => {
|
||||
const search = queryString.stringify({
|
||||
...queryString.parse(location.search),
|
||||
commentId: id,
|
||||
});
|
||||
|
||||
// Change the url to the permalink url.
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
buildUrl({ ...location, search })
|
||||
);
|
||||
});
|
||||
|
||||
// Helps child show notifications at the right scrollTop.
|
||||
this.pym.onMessage('getPosition', () => {
|
||||
const { height } = viewportDimensions();
|
||||
let position = height + document.body.scrollTop;
|
||||
|
||||
if (position > NOTIFICATION_OFFSET) {
|
||||
position = position - NOTIFICATION_OFFSET;
|
||||
}
|
||||
|
||||
this.pym.sendMessage('position', position);
|
||||
});
|
||||
|
||||
// When end-user clicks link in iframe, open it in parent context
|
||||
this.pym.onMessage('navigate', url => {
|
||||
// Open the new window, detach the opener, and focus on it.
|
||||
const w = window.open(url, '_blank');
|
||||
w.opener = null;
|
||||
w.focus();
|
||||
});
|
||||
|
||||
// Pass events from iframe to the event emitter.
|
||||
this.pym.onMessage('event', raw => {
|
||||
const { eventName, value } = JSON.parse(raw);
|
||||
this.emitter.emit(eventName, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* load will configure the pym parent if it hasn't already been, and setup the
|
||||
* snackBar. If there are any queued operations, it will run them first.
|
||||
*/
|
||||
load() {
|
||||
if (this.pym !== null) {
|
||||
throw new Error('Stream Embed already rendered');
|
||||
}
|
||||
|
||||
// Setup Pym.
|
||||
this.setupPym();
|
||||
|
||||
// Attach the snackBar to the pym parent and to the body of the page.
|
||||
this.snackBar.attach(window.document.body, this.pym);
|
||||
|
||||
// If the user clicks outside the embed, then tell the embed.
|
||||
document.addEventListener('click', this.handleClick.bind(this), true);
|
||||
|
||||
// Listens to ${name}Storage requests on pym and relay it to
|
||||
// ${name}Storage.
|
||||
['local', 'session'].forEach(name => {
|
||||
connectStorageToPym(
|
||||
createStorage(`${name}Storage`),
|
||||
this.pym,
|
||||
`${name}Storage`
|
||||
);
|
||||
});
|
||||
|
||||
// Process any queued operations.
|
||||
const queued = this.queued;
|
||||
this.queued = null;
|
||||
queued.forEach(callback => callback());
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.pym.sendMessage('click');
|
||||
}
|
||||
|
||||
enablePluginsDebug() {
|
||||
this.pym.sendMessage('enablePluginsDebug');
|
||||
}
|
||||
|
||||
disablePluginsDebug() {
|
||||
this.pym.sendMessage('disablePluginsDebug');
|
||||
}
|
||||
|
||||
login(token) {
|
||||
this.pym.sendMessage('login', token);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.pym.sendMessage('logout');
|
||||
}
|
||||
|
||||
remove() {
|
||||
// Remove the event listeners.
|
||||
document.removeEventListener('click', this.handleClick.bind(this));
|
||||
this.emitter.removeAllListeners();
|
||||
|
||||
// Remove the snackbar.
|
||||
this.snackBar.remove();
|
||||
|
||||
// Remove the pym parent.
|
||||
this.pym.remove();
|
||||
this.pym = null;
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import queryString from 'querystringify';
|
||||
import pym from 'pym.js';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import { buildUrl } from 'coral-framework/utils/url';
|
||||
import Snackbar from './Snackbar';
|
||||
import {
|
||||
createStorage,
|
||||
connectStorageToPym,
|
||||
} from 'coral-framework/services/storage';
|
||||
|
||||
const NOTIFICATION_OFFSET = 200;
|
||||
|
||||
// Build the URL to load in the pym iframe.
|
||||
function buildStreamIframeUrl(talkBaseUrl, query) {
|
||||
let url = [
|
||||
talkBaseUrl,
|
||||
talkBaseUrl.match(/\/$/) ? '' : '/', // make sure no double-'/' if opts.talk already ends with '/'
|
||||
'embed/stream?',
|
||||
].join('');
|
||||
|
||||
url += queryString.stringify(query);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// Get dimensions of viewport.
|
||||
function viewportDimensions() {
|
||||
let e = window,
|
||||
a = 'inner';
|
||||
if (!('innerWidth' in window)) {
|
||||
a = 'client';
|
||||
e = document.documentElement || document.body;
|
||||
}
|
||||
|
||||
return {
|
||||
width: e[`${a}Width`],
|
||||
height: e[`${a}Height`],
|
||||
};
|
||||
}
|
||||
|
||||
export default class Stream {
|
||||
constructor(el, talkBaseUrl, query, config) {
|
||||
this.query = query;
|
||||
|
||||
// Extract the non-opts opts from the object.
|
||||
const {
|
||||
events = null,
|
||||
snackBarStyles = null,
|
||||
onAuthChanged = null,
|
||||
...opts
|
||||
} = config;
|
||||
|
||||
this.opts = opts;
|
||||
|
||||
this.emitter = new EventEmitter({ wildcard: true });
|
||||
this.pym = new pym.Parent(el.id, buildStreamIframeUrl(talkBaseUrl, query), {
|
||||
title: opts.title,
|
||||
id: `${el.id}_iframe`,
|
||||
name: `${el.id}_iframe`,
|
||||
});
|
||||
this.snackBar = new Snackbar(snackBarStyles || {});
|
||||
|
||||
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
|
||||
this.pym.el.firstChild.style.width = '1px';
|
||||
this.pym.el.firstChild.style.minWidth = '100%';
|
||||
|
||||
// Resize parent iframe height when child height changes
|
||||
let cachedHeight;
|
||||
this.pym.onMessage('height', height => {
|
||||
if (height !== cachedHeight) {
|
||||
this.pym.el.firstChild.style.height = `${height}px`;
|
||||
cachedHeight = height;
|
||||
}
|
||||
});
|
||||
|
||||
// Attach to the events emitted by the pym parent.
|
||||
if (events) {
|
||||
events(this.emitter);
|
||||
}
|
||||
|
||||
this.pym.onMessage('getConfig', () => {
|
||||
this.pym.sendMessage('config', JSON.stringify(opts));
|
||||
});
|
||||
|
||||
// If the auth changes, and someone is listening for it, then re-emit it.
|
||||
if (onAuthChanged) {
|
||||
this.pym.onMessage('coral-auth-changed', message => {
|
||||
onAuthChanged(message ? JSON.parse(message) : null);
|
||||
});
|
||||
}
|
||||
|
||||
// Attach the snackbar to the pym parent and to the body of the page.
|
||||
this.snackBar.attach(window.document.body, this.pym);
|
||||
|
||||
// Remove the permalink comment id from the hash.
|
||||
this.pym.onMessage('coral-view-all-comments', () => {
|
||||
const query = queryString.parse(location.search);
|
||||
|
||||
// Remove the commentId url param.
|
||||
delete query.commentId;
|
||||
|
||||
const search = queryString.stringify(query);
|
||||
|
||||
const url = buildUrl({ ...location, search });
|
||||
|
||||
// Change the url.
|
||||
window.history.replaceState({}, document.title, url);
|
||||
});
|
||||
|
||||
// Remove the permalink comment id from the hash.
|
||||
this.pym.onMessage('coral-view-comment', id => {
|
||||
const search = queryString.stringify({
|
||||
...queryString.parse(location.search),
|
||||
commentId: id,
|
||||
});
|
||||
|
||||
// Remove the commentId url param.
|
||||
const url = buildUrl({ ...location, search });
|
||||
|
||||
// Change the url.
|
||||
window.history.replaceState({}, document.title, url);
|
||||
});
|
||||
|
||||
// Helps child show notifications at the right scrollTop.
|
||||
this.pym.onMessage('getPosition', () => {
|
||||
const { height } = viewportDimensions();
|
||||
let position = height + document.body.scrollTop;
|
||||
|
||||
if (position > NOTIFICATION_OFFSET) {
|
||||
position = position - NOTIFICATION_OFFSET;
|
||||
}
|
||||
|
||||
this.pym.sendMessage('position', position);
|
||||
});
|
||||
|
||||
// When end-user clicks link in iframe, open it in parent context
|
||||
this.pym.onMessage('navigate', url => {
|
||||
window.open(url, '_blank').focus();
|
||||
});
|
||||
|
||||
// Pass events from iframe to the event emitter.
|
||||
this.pym.onMessage('event', raw => {
|
||||
const { eventName, value } = JSON.parse(raw);
|
||||
this.emitter.emit(eventName, value);
|
||||
});
|
||||
|
||||
// If the user clicks outside the embed, then tell the embed.
|
||||
document.addEventListener('click', this.handleClick.bind(this), true);
|
||||
|
||||
// Listens to local storage requests on pym and relay it to local storage.
|
||||
connectStorageToPym(
|
||||
createStorage('localStorage'),
|
||||
this.pym,
|
||||
'localStorage'
|
||||
);
|
||||
|
||||
// Listens to session storage requests on pym and relay it to session storage.
|
||||
connectStorageToPym(
|
||||
createStorage('sessionStorage'),
|
||||
this.pym,
|
||||
'sessionStorage'
|
||||
);
|
||||
}
|
||||
|
||||
enablePluginsDebug() {
|
||||
this.pym.sendMessage('enablePluginsDebug');
|
||||
}
|
||||
|
||||
disablePluginsDebug() {
|
||||
this.pym.sendMessage('disablePluginsDebug');
|
||||
}
|
||||
|
||||
login(token) {
|
||||
this.pym.sendMessage('login', token);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.pym.sendMessage('logout');
|
||||
}
|
||||
|
||||
remove() {
|
||||
// Remove the event listeners.
|
||||
document.removeEventListener('click', this.handleClick.bind(this));
|
||||
this.emitter.removeAllListeners();
|
||||
|
||||
// Remove the snackbar.
|
||||
this.snackBar.remove();
|
||||
|
||||
// Remove the pym parent.
|
||||
this.pym.remove();
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.pym.sendMessage('click');
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
export default class StreamInterface {
|
||||
constructor(stream) {
|
||||
this._stream = stream;
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
return this._stream.emitter.on(eventName, callback);
|
||||
}
|
||||
|
||||
off(eventName, callback) {
|
||||
return this._stream.emitter.off(eventName, callback);
|
||||
}
|
||||
|
||||
login(token) {
|
||||
return this._stream.login(token);
|
||||
}
|
||||
|
||||
logout() {
|
||||
return this._stream.logout();
|
||||
}
|
||||
|
||||
remove() {
|
||||
return this._stream.remove();
|
||||
}
|
||||
|
||||
enablePluginsDebug() {
|
||||
return this._stream.enablePluginsDebug();
|
||||
}
|
||||
|
||||
disablePluginsDebug() {
|
||||
return this._stream.disablePluginsDebug();
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1,71 @@
|
||||
import URLSearchParams from 'url-search-params';
|
||||
import Stream from './Stream';
|
||||
import StreamInterface from './StreamInterface';
|
||||
// Polyfill IntersectionObserver always, the alternative is that we have to also
|
||||
// polyfill for Promise, which itself adds 1KB gziped, which means that the
|
||||
// 4KB that the intersection observer really doesn't take up that much in terms
|
||||
// of size.
|
||||
import 'intersection-observer';
|
||||
|
||||
// Rebuild the origin if it isn't defined. This is our poor-mans polyfill
|
||||
// for the location API's.
|
||||
if (!window.location.origin) {
|
||||
window.location.origin = `${window.location.protocol}//${
|
||||
window.location.hostname
|
||||
}${window.location.port ? `:${window.location.port}` : ''}`;
|
||||
}
|
||||
import Bridge from './Bridge';
|
||||
import wrapBridge from './wrapBridge';
|
||||
|
||||
// parses the Asset URL from the config variable
|
||||
function parseAssetURL() {
|
||||
try {
|
||||
// Try to get the url from the canonical tag on the page.
|
||||
return document.querySelector('link[rel="canonical"]').href;
|
||||
} catch (e) {
|
||||
window.console.warn(
|
||||
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
|
||||
/**
|
||||
* validateElement will throw an error when the element is not configured
|
||||
* correctly or is not an HTMLElement at all.
|
||||
*
|
||||
* @param {HTMLElement} element the HTMLElement where the stream will be rendered
|
||||
*/
|
||||
function validateElement(element) {
|
||||
if (!element) {
|
||||
throw new Error(
|
||||
'Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'
|
||||
);
|
||||
}
|
||||
|
||||
return window.location.origin + window.location.pathname;
|
||||
if (typeof element !== 'object') {
|
||||
throw new Error(
|
||||
`Coral.Talk.render() expected HTMLElement but got ${element} (${typeof element})`
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure element has an id, as pym can't directly accept the HTMLElement.
|
||||
if (!element.id) {
|
||||
element.id = `_${Math.random()}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Talk {
|
||||
/**
|
||||
* validateConfig is the configuration validation tool.
|
||||
*
|
||||
* @param {Object} config the configuration that will be used to setup Talk.
|
||||
*/
|
||||
function validateConfig(config) {
|
||||
if (!config || typeof config !== 'object' || !config.talk) {
|
||||
throw new Error(
|
||||
'Coral.Talk.render() expected configuration with at least opts.talk as the Talk Base URL, none found'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Talk = {
|
||||
/**
|
||||
* Render a Talk stream
|
||||
* @param {HTMLElement} element - Element to render the stream in
|
||||
* @param {Object} config - Configuration options for talk
|
||||
* @param {String} config.talk - Talk base URL
|
||||
* @param {String} [config.title] - Title of Stream (rendered in iframe)
|
||||
* @param {String} [config.asset_url] - Asset URL
|
||||
* @param {String} [config.asset_id] - Asset ID
|
||||
* @param {String} config.talk - URL to the Talk installation
|
||||
* @param {String} [config.asset_id] - (optional) ID for the Asset
|
||||
* @param {String} [config.asset_url] - (optional) URL where the Asset is located
|
||||
* @param {String} [config.auth_token] - (optional) A jwt representing the session
|
||||
* @param {String} [config.lazy] - (optional) If set the stream will only render lazily
|
||||
* @param {String} [config.talkStaticUrl] - (optional) Static URL used to serve Talk
|
||||
* @return {Object}
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const embed = Talk.render(document.getElementById('talkStreamEmbed'), config);
|
||||
*
|
||||
* // trigger a login with optional token.
|
||||
* embed.login(token);
|
||||
*
|
||||
* // trigger a logout.
|
||||
* embed.logout();
|
||||
*
|
||||
* // listen to events (in this case all events).
|
||||
* embed.on('**', function(value) {
|
||||
* console.log(this.event, value);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
static render(element, config) {
|
||||
if (!element) {
|
||||
throw new Error(
|
||||
'Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'
|
||||
);
|
||||
}
|
||||
if (typeof element !== 'object') {
|
||||
throw new Error(
|
||||
`Coral.Talk.render() expected HTMLElement but got ${element} (${typeof element})`
|
||||
);
|
||||
}
|
||||
if (!config || typeof config !== 'object' || !config.talk) {
|
||||
throw new Error(
|
||||
'Coral.Talk.render() expected configuration with at least opts.talk as the Talk Base URL, none found'
|
||||
);
|
||||
}
|
||||
render: (element, config) => {
|
||||
// Validate the element.
|
||||
validateElement(element);
|
||||
|
||||
// Ensure el has an id, as pym can't directly accept the HTMLElement.
|
||||
if (!element.id) {
|
||||
element.id = `_${Math.random()}`;
|
||||
}
|
||||
// Validate the configuration.
|
||||
validateConfig(config);
|
||||
|
||||
// Compose the query to send down to the Talk API so it knows what to load.
|
||||
const query = {};
|
||||
|
||||
// Parse the url parameters to extract some of the information.
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
|
||||
// Pull the commentID out from the query params.
|
||||
const commentID = search.get('commentId') || search.get('commentID');
|
||||
if (commentID) {
|
||||
query.comment_id = commentID;
|
||||
}
|
||||
|
||||
// Extract the asset id from the options.
|
||||
if (config.asset_id) {
|
||||
query.asset_id = config.asset_id;
|
||||
}
|
||||
|
||||
// Parse the Asset URL.
|
||||
query.asset_url = config.asset_url;
|
||||
if (!query.asset_url) {
|
||||
query.asset_url = parseAssetURL();
|
||||
}
|
||||
|
||||
// Create the new Stream.
|
||||
const stream = new Stream(element, config.talk, query, config);
|
||||
|
||||
// Return the public interface for the stream.
|
||||
return new StreamInterface(stream);
|
||||
}
|
||||
}
|
||||
// Create the new Bridge, and wrap it up.
|
||||
return wrapBridge(new Bridge(element, config));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export default function onIntersect(el, callback) {
|
||||
// Ensure that the intersection observer is available.
|
||||
if (!IntersectionObserver) {
|
||||
// tslint:disable-next-line:no-console
|
||||
window.console.warn('IntersectionObserver not available, rendering now');
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the Intersection Observer that will wait till the embed is within
|
||||
// view and will then call the callback.
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
// Stop receiving intersection events.
|
||||
observer.disconnect();
|
||||
|
||||
// Fire the callback.
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '100px',
|
||||
threshold: 1.0,
|
||||
}
|
||||
);
|
||||
|
||||
// Start observing the element for visibility.
|
||||
observer.observe(el);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export default bridge => {
|
||||
// whenRendered will call the callback when the stream has rendered (or now if
|
||||
// the stream is already rendered).
|
||||
const whenRendered = callback => bridge.queueWhenRendered(callback);
|
||||
|
||||
// onlyWhenRendered will guard the callback unless the stream is rendered.
|
||||
const onlyWhenRendered = callback => {
|
||||
bridge.ensureRendered();
|
||||
callback();
|
||||
};
|
||||
|
||||
// Return the limited stream interface.
|
||||
return {
|
||||
// Directly map the on/off from the event emitter to the stream.
|
||||
on: (eventName, callback) => bridge.emitter.on(eventName, callback),
|
||||
off: (eventName, callback) => bridge.emitter.off(eventName, callback),
|
||||
|
||||
// Queue up the login operation until the stream has been rendered.
|
||||
login: token => whenRendered(() => bridge.login(token)),
|
||||
|
||||
// Queue up the logout operation until the stream has been rendered.
|
||||
logout: () => whenRendered(() => bridge.logout()),
|
||||
|
||||
// Remove the stream if it's already been rendered.
|
||||
remove: () => onlyWhenRendered(() => bridge.remove()),
|
||||
|
||||
// Queue up the plugin config until the embed has rendered.
|
||||
enablePluginsDebug: () => whenRendered(() => bridge.enablePluginsDebug()),
|
||||
disablePluginsDebug: () => whenRendered(() => bridge.disablePluginsDebug()),
|
||||
};
|
||||
};
|
||||
@@ -76,6 +76,10 @@ const CONFIG = {
|
||||
// on the scraper when it makes requests.
|
||||
SCRAPER_HEADERS: process.env.TALK_SCRAPER_HEADERS || '{}',
|
||||
|
||||
// HTTP_X_REQUEST_ID is a string which represents the request header where we
|
||||
// should source the request ID from, otherwise, a new one will be generated.
|
||||
HTTP_X_REQUEST_ID: process.env.TALK_HTTP_X_REQUEST_ID || null,
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// JWT based configuration
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -220,6 +224,14 @@ const CONFIG = {
|
||||
CACHE_EXPIRY_COMMENT_COUNT:
|
||||
process.env.TALK_CACHE_EXPIRY_COMMENT_COUNT || '1hr',
|
||||
|
||||
// EMBED_EXPIRY_TIME is the time that the embed will be cacheable for, sent as
|
||||
// the max-age= directive on the Cache-Control header.
|
||||
EMBED_EXPIRY_TIME: ms(process.env.TALK_EMBED_EXPIRY || '24hr'),
|
||||
|
||||
// EMBED_EXPIRY_TIME is the time that the rest of the static files will be
|
||||
// cacheable for, sent as the max-age= directive on the Cache-Control header.
|
||||
STATIC_EXPIRY_TIME: ms(process.env.TALK_STATIC_EXPIRY || '1w'),
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Recaptcha configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
+2
-2
@@ -104,8 +104,8 @@ sidebar:
|
||||
children:
|
||||
- title: Authentication
|
||||
url: /integrating/authentication/
|
||||
- title: Asset Management
|
||||
url: /integrating/asset-management/
|
||||
- title: CMS Integration
|
||||
url: /integrating/cms-integration/
|
||||
- title: Asset Scraping
|
||||
url: /integrating/asset-scraping/
|
||||
- title: Configuring the Comment Stream
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/ /talk/
|
||||
/talk/reference/server/ /talk/api/server/
|
||||
/talk/reference/graphql/ /talk/api/graphql/
|
||||
/talk/reference/graphql/ /talk/api/graphql/
|
||||
/talk/integrating/asset-management/ /talk/integrating/cms-integration/
|
||||
|
||||
@@ -187,8 +187,10 @@ and walk through the initial setup steps.
|
||||
|
||||
* First, enter your **Organization Name** and **Organization Contact Email**. This will appear in emails when inviting new team members.
|
||||
* Next, create your Admin user. You can specify an **Email Address**, **Username**, and **Password**
|
||||
|
||||
* Finally, enter your list of **Permitted Domains**, read [here](/talk/configuring-talk/#permitted-domains) about whitelisting domains
|
||||
|
||||
|
||||
_During development, ensure you whitelist 127.0.0.1:3000 otherwise the
|
||||
[http://127.0.0.1:3000/](http://127.0.0.1:3000/) page will not
|
||||
load._
|
||||
@@ -220,6 +222,17 @@ Once you have added the domain of these docs, you can click the button below.
|
||||
<div class="mount"></div>
|
||||
</div>
|
||||
|
||||
### Developer Endpoints
|
||||
|
||||
With your local instance of Talk running in development mode (env variable `NODE_ENV=development`) you should now also be able to access the following developer routes:
|
||||
|
||||
* [http://127.0.0.1:3000/dev](http://127.0.0.1:3000/dev]) provides a sample comment stream
|
||||
|
||||
* [http://127.0.0.1:3000/dev/assets](http://127.0.0.1:3000/dev/assets]) provides a list of all stories in Talk and can generate new sample assets
|
||||
|
||||
### Conclusion
|
||||
At this point you've successfully installed, configured, and ran your very own
|
||||
instance of Talk! Continue through this documentation on this site to learn more
|
||||
on how to configure, develop with, and contribute to Talk!
|
||||
|
||||
|
||||
|
||||
@@ -70,5 +70,7 @@ You can now start the application by running:
|
||||
yarn watch:server
|
||||
```
|
||||
|
||||
If you are developing a custom plugin you can use `yarn watch:client` or `yarn watch` to run both client and server.
|
||||
|
||||
At this stage, you should refer to the [configuration](/talk/configuration/) for
|
||||
configuration variables that are specific to your installation.
|
||||
|
||||
@@ -23,11 +23,13 @@ permalink: /pre-launch-checklist/
|
||||
|
||||
- [ ] Do you need to migrate comments from a legacy system? We currently support Disqus, Livefyre, and Civil Comments.
|
||||
- Use the [Talk Import](https://github.com/coralproject/talk-importer) framework
|
||||
|
||||
|
||||
|
||||
- [ ] 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
|
||||
|
||||
@@ -5,13 +5,15 @@ permalink: /commenter-features/
|
||||
|
||||
## Signing up for Talk
|
||||
|
||||
There are 2 ways that newsrooms can support signup/login functionality with Talk:
|
||||
There are 3 ways that newsrooms can support signup/login functionality with Talk:
|
||||
|
||||
* Use Talk’s auth plugin out of the box (supports account registration with username and password, as well as features like forgot password)
|
||||
|
||||
* Use 3rd party authentication provider such as FaceBook or Google. We provide plugins that support logging in with either [Facebook](/talk/plugin/talk-plugin-facebook-auth/)
|
||||
or [Google](/talk/plugin/talk-plugin-google-auth/). (Note: you must provide your own Facebook App ID and Secret, which you can read more about here: [https://developers.facebook.com](https://developers.facebook.com))
|
||||
|
||||
* Create their own auth plugin to integrate with your own auth systems
|
||||
|
||||
We also provide a Facebook auth plugin that supports logging in with Facebook (you must provide your own Facebook App ID and Secret, which you can read more about here: [https://developers.facebook.com](https://developers.facebook.com))
|
||||
|
||||
## Comments and Replies
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ title: Authenticating with Talk
|
||||
permalink: /integrating/authentication/
|
||||
---
|
||||
|
||||
You can integrate Talk with any external authentication service that will enable
|
||||
seamless single sign-on for users within your organization. There are a few
|
||||
Out of the box Talk supports account registration with username and password, as well as features like forgot password.
|
||||
|
||||
You can also integrate Talk with any external authentication service that will enable
|
||||
seamless single sign-on (SSO) for users within your organization. There are a few
|
||||
methods of doing so:
|
||||
|
||||
1. Passport Middleware
|
||||
@@ -17,8 +19,12 @@ choice.
|
||||
|
||||
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). A great example of this is our
|
||||
[talk-plugin-facebook-auth](/talk/plugin/talk-plugin-facebook-auth/) plugin.
|
||||
state (you don't use the auth anywhere else now).
|
||||
|
||||
Plugins are available for the following 3rd party authentication providers:
|
||||
|
||||
* [Facebook](/talk/plugin/talk-plugin-facebook-auth/)
|
||||
* [Google](/talk/plugin/talk-plugin-google-auth/)
|
||||
|
||||
## Custom Token Integration
|
||||
|
||||
|
||||
+73
-11
@@ -1,22 +1,81 @@
|
||||
---
|
||||
title: Asset Management
|
||||
permalink: /integrating/asset-management/
|
||||
title: CMS Integration
|
||||
permalink: /integrating/cms-integration/
|
||||
---
|
||||
|
||||
## Embedding Comments on Your Site
|
||||
Talk provides an embed script that you can drop into your site where you want a comments section to appear. By default that script dynamically generate Assets
|
||||
in Talk in order to make it easier for lighter installations.
|
||||
|
||||
You can find the embed script inside talk under `Configure > Tech Settings > Embed Script`. It should look something like this, but with your domain in place of `<TALK_ROOT_URL>`:
|
||||
```
|
||||
<div id="coral_talk_stream"></div>
|
||||
<script src="<TALK_ROOT_URL>/static/embed.js" async onload="
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '<TALK_ROOT_URL>'
|
||||
});
|
||||
"></script>
|
||||
```
|
||||
|
||||
## Triggering the Comments Section (client side, i.e. Your site)
|
||||
|
||||
When the embed script is triggered on your page load several things are initiated inside Talk, including fetching all comments for the specified article, establishing websocket connections for this user, and checking user’s session for SSO/authentication.
|
||||
|
||||
Instead of greedily triggering the embed to render on _EVERY PAGE LOAD_, we highly recommend implementing a _“lazy”_ rendering strategy to only render the comments section if a user wants to interact with it. This will greatly improve your initial page load performance, and will be critical to managing server resources if you’re running Talk on a heavy-traffic production site.
|
||||
|
||||
We recommend using one of these _“lazy”_ loading strategies:
|
||||
|
||||
#### Scroll to Comments Section
|
||||
Wait for user to scroll to the comment section before triggering the embed to render.
|
||||
|
||||
You can pass lazy: true to the render options, like so:
|
||||
```
|
||||
Coral.Talk.render(document.getElementById('container'), {
|
||||
talk: 'https://my-talk-installation.com',
|
||||
lazy: true,
|
||||
});
|
||||
```
|
||||
|
||||
Or you can enable lazy rendering by default on all assets using ENV variable `TALK_DEFAULT_LAZY_RENDER=TRUE`
|
||||
|
||||
_*Note: This feature requires Talk version 4.6.8 or greater_
|
||||
|
||||
#### Show Comments Button
|
||||
You can hide the comments section until a user clicks button, then trigger the embed to render
|
||||
|
||||
This example uses jQuery to render the embed on the button's click event
|
||||
```
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
|
||||
<div id="coral_talk_stream">
|
||||
<button>Show Comments</button>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('#coral_talk_stream button').on('click', function() {
|
||||
$.getScript('<TALK_ROOT_URL>/static/embed.js', function() {
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '<TALK_ROOT_URL>',
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Creating Assets in Talk (server side, i.e. What you need to send to Talk)
|
||||
One of the most frequent questions that we get asked by organizations trying to
|
||||
integrate Talk is: _How do we hook our CMS up to Talk so that articles are in
|
||||
sync?_
|
||||
|
||||
This guide is designed to explain the steps to take your base installation of
|
||||
Talk and configure it to allow only assets pushed into it from your CMS, and
|
||||
keep your URL/title in sync. We won't cover here how to install the plugin, as
|
||||
it is covered in our [Plugins Overview](/talk/plugins/).
|
||||
### “Lazy Asset Creation”
|
||||
Talk’s provided embed script by default dynamically generates Assets in Talk based on each unique url that triggers it. The url must reference an existing Permitted Domain. If your articles/stories always have unique urls, then you will not need to modify the default behavior.
|
||||
|
||||
## Why do we need to create a plugin?
|
||||
Assets created in this way will then be scraped to load metadata, see [Asset Scraping](/talk/integrating/asset-scraping/)
|
||||
|
||||
By default, Talk will use "Lazy Asset Creation" to dynamically generate Assets
|
||||
in Talk in order to make it easier for lighter installations. In order to have
|
||||
more strict control over this flow, we will create a plugin that will:
|
||||
## Customizing the Integration with a Plugin
|
||||
In order to have more strict control over the asset creation flow to allow only assets pushed into it from your CMS, and keep your URL/title in sync we will create a plugin.
|
||||
|
||||
We will create a plugin that will:
|
||||
|
||||
1. Disable "Lazy Asset Creation" by [Overriding a Resolver](#overriding-a-resolver).
|
||||
2. Create Assets from our CMS by [Creating a New Asset Route](#creating-a-new-asset-route).
|
||||
@@ -24,6 +83,9 @@ more strict control over this flow, we will create a plugin that will:
|
||||
|
||||
We will then modify our embed so that we can [Target the Asset](#target-the-asset).
|
||||
|
||||
|
||||
This guide is designed to explain the steps to take your base installation of Talk and configure it. We won't cover here how to install the plugin, as it is covered in our [Plugins Overview](/talk/plugins/).
|
||||
|
||||
But first we should grab our basic plugin structure:
|
||||
|
||||
```sh
|
||||
@@ -248,7 +310,7 @@ module.exports = router => {
|
||||
};
|
||||
```
|
||||
|
||||
As you can see from the previous step of [Creating a New Asset Route](#creating-a-New-Asset-Route)
|
||||
As you can see from the previous step of [Creating a New Asset Route](#Creating%20a%20New%20Asset%20Route)
|
||||
, we have added the new `PUT` route to the router. This is a simple addition
|
||||
that allows your CMS to call into Talk when the asset has updated it's title,
|
||||
it's url (or really anything in the [AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js)) to keep the Talk Admin and links up to date.
|
||||
@@ -105,6 +105,8 @@ Then the application can be started as is.
|
||||
|
||||
### Docker
|
||||
|
||||
To deploy customized plugins to a production instance of Talk, we recommend using the Docker onbuild strategy outlined below.
|
||||
|
||||
If you deploy using Docker, you can extend from the `*-onbuild` image, an
|
||||
example `Dockerfile` for your project could be:
|
||||
|
||||
@@ -112,14 +114,22 @@ example `Dockerfile` for your project could be:
|
||||
FROM coralproject/talk:4.5-onbuild
|
||||
```
|
||||
|
||||
Where the directory for your instance would contain a `plugins.json` file
|
||||
describing the plugin requirements and a `plugins` directory containing any
|
||||
Establish a private repository for your instance that includes the following:
|
||||
|
||||
* a `plugins.json` file
|
||||
listing the plugin requirements
|
||||
* a `plugins` directory containing any
|
||||
other local plugins that should be included.
|
||||
* a Dockerfile as outlined above
|
||||
|
||||
Git submodules can also be used to point to plugin directories that might be outside your primary repository.
|
||||
|
||||
Onbuild triggers will execute when the image is building with your custom
|
||||
configuration and will ensure that the image is ready to use by building all
|
||||
assets inside the image as well.
|
||||
|
||||
Once built, you can deploy the docker image to your architecture by tagging the image and including it in your docker-compose.yml.
|
||||
|
||||
For more information on the onbuild image, refer to the
|
||||
[Installation from Docker](/talk/installation-from-docker/) documentation.
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ de:
|
||||
code_of_conduct_summary_desc: 'Verfassen Sie eine Einleitung, die über jedem Kommentarbereich erscheint. Nützlich z.B. für Community-Richtlinien.'
|
||||
include_question_here: 'Stellen Sie Ihre Frage hier:'
|
||||
include_text: 'Fügen Sie Ihren Text hier ein.'
|
||||
include_an_icon: 'Fügen Sie ein Icon hinzu'
|
||||
moderate: Moderieren
|
||||
moderation_settings: Moderations-Einstellungen
|
||||
open: Öffnen
|
||||
|
||||
@@ -139,6 +139,7 @@ en:
|
||||
hours: Hours
|
||||
code_of_conduct_summary: 'Summary of your Code of Conduct'
|
||||
code_of_conduct_summary_desc: 'This message will appear above every comments stream on your site. Click here to learn more about writing a good code.'
|
||||
include_an_icon: 'Include an Icon'
|
||||
include_question_here: 'Write your question here:'
|
||||
include_text: 'Include your text here.'
|
||||
moderate: Moderate
|
||||
|
||||
@@ -38,6 +38,9 @@ es:
|
||||
name: Nombre
|
||||
post: Publicar
|
||||
reply: Responder
|
||||
comment_history_blank:
|
||||
info: 'Una historia de tus comentarios aparecerá aquí'
|
||||
title: 'No has escrito ningún comentario'
|
||||
comment_offensive: 'Este comentario es ofensivo'
|
||||
comment_plural: Comentarios
|
||||
comment_post_banned_word: 'Tu comentario contiene una o más palabras que no están permitidas en nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contactate con nuestro equipo de moderación.'
|
||||
|
||||
+93
-14
@@ -38,6 +38,9 @@ nl_NL:
|
||||
name: Naam
|
||||
post: Plaats
|
||||
reply: Beantwoord
|
||||
comment_history_blank:
|
||||
info: 'Reacties die je hebt geplaatst zullen hier verschijnen'
|
||||
title: 'Je hebt nog geen reacties geplaatst'
|
||||
comment_offensive: 'Deze reactie is aanstootgevend'
|
||||
comment_plural: Reacties
|
||||
comment_post_banned_word: 'Je reactie bevat een of meerdere termen die wij niet toestaan. Neem contact op wanneer je het er niet mee eens bent.'
|
||||
@@ -45,7 +48,10 @@ nl_NL:
|
||||
comment_post_notif_premod: 'Dank voor je reactie! Deze wordt zo snel mogelijk gemodereerd.'
|
||||
comment_singular: Reactie
|
||||
common:
|
||||
contains_link: 'Bevat link'
|
||||
copy: Kopieer
|
||||
copied: Gekopieerd
|
||||
notsupported: 'Niet ondersteund'
|
||||
error: 'Er is een fout opgetreden.'
|
||||
reaction: reactie
|
||||
reactions: reacties
|
||||
@@ -80,9 +86,11 @@ nl_NL:
|
||||
spam_ads: Spam/Ads
|
||||
staff: Staff
|
||||
status: Status
|
||||
suspended: Geschorst
|
||||
username_and_email: 'Gebruikersnaam en e-mailadres'
|
||||
yes_ban_user: 'Ja, verban gebruiker'
|
||||
configure:
|
||||
access_message: 'Je moet een admin zijn om toegang te krijgen tot de configuratie instellingen. Vraag de dichtsbijzijnde Admin om toegang!'
|
||||
apply: Toepassen
|
||||
banned_word_text: 'Reacties die deze woorden of zinnen bevatten (niet hoofdlettergevoelig) worden automatisch uit de conversatie verwijderd. Voer een woord in, en druk op enter of Tab om toe te passen. Je kunt ook een kommagescheiden lijst plakken.'
|
||||
banned_words_title: 'Lijst met verbannen woorden'
|
||||
@@ -107,6 +115,8 @@ nl_NL:
|
||||
custom_css_url_desc: 'URL van CSS stylesheet die de standaard Embed Conversatie stylesheet overschrijft. Kan intern of extern zijn.'
|
||||
days: Dagen
|
||||
description: 'Als admin kun je de instellingen aanpassen voor de conversatie bij dit verhaal:'
|
||||
disable_commenting_desc: 'Schrijf een bericht dat wordt weergeven wanneer reageren is afgesloten.'
|
||||
disable_commenting_title: 'Reacties afsluiten voor de gehele site'
|
||||
domain_list_text: 'Voer de domeinen in die je wil toestaan voor Talk, bijvoorbeeld je staging en productie-omgevingen (bijv. localhost:3000 staging.domeinnaam.com domeinnaam.com).'
|
||||
domain_list_title: 'Toegestane domeinnamen'
|
||||
edit_comment_timeframe_heading: 'Wijzig reactie tijdsduur'
|
||||
@@ -132,9 +142,23 @@ nl_NL:
|
||||
open: Open
|
||||
open_stream: 'Open Stream'
|
||||
open_stream_configuration: 'Deze converstatie is momenteel open. Door deze te sluiten kunnen geen nieuwe reacties meer worden geplaatst, maar de oude zullen nog steeds worden weergegeven.'
|
||||
organization_contact_email: 'Organisatie Contact E-mailadres'
|
||||
organization_info_copy: 'We gebruiken deze informatie in e-mail notificaties die door Talk worden verstuurd. Dit verbind de berichten met je organisatie en biedt gebruikers een manier om contact op te nemen mochten ze een probleem met hun account ondervinden.'
|
||||
organization_info_copy_2: 'We raden aan om een algemeen e-mail account (bijvoorbeeld community@yournewsroom.com) te gebruiken voor dit doeleinde. Dit betekent dat dit adres onveranderd kan blijven en deze geen naam blootstelt die gebruikers kunnen gebruiken mocht hun account geblokkeerd worden.'
|
||||
organization_information: 'Organisatie Informatie'
|
||||
organization_name: 'Organisatie Naam'
|
||||
suspect_or_forbidden_words_placeholder: 'Woord of uitdrukking'
|
||||
product_guide_link: 'Product Handleiding'
|
||||
report_bug_or_feedback: 'Rapporteer een bug of geef feedback'
|
||||
require_email_verification: 'Maak e-mail verificatie vereist'
|
||||
require_email_verification_text: 'Nieuwe gebruikers moeten hun e-mailadres verifiëren voordat ze door kunnen gaan.'
|
||||
save_changes: 'Wijzigingen opslaan'
|
||||
save_changes_dialog:
|
||||
cancel: Annuleren
|
||||
copy: 'Je hebt een of meerdere wijzigingen gedaan zonder op te slaan. Wil je je wijzigingen opslaan of ongedaan maken?'
|
||||
discard: 'Ongedaan maken'
|
||||
save_settings: 'Instellingen Opslaan'
|
||||
unsaved_changes: 'Niet-opgeslagen wijzigingen'
|
||||
shortcuts: Sneltoetsen
|
||||
sign_out: Uitloggen
|
||||
stories: Conversaties
|
||||
@@ -142,7 +166,7 @@ nl_NL:
|
||||
suspect_word_text: 'Reacties die deze woorden of zinsdelen bevatten (niet hoofdlettergevoelig) zullen worden gemarkeerd in de conversatie. Type een woord en druk op enter of tab om toe te voegen. Je kunt optioneel een komma-gescheiden lijst plakken.'
|
||||
suspect_word_title: 'Lijst met verdachte woorden'
|
||||
tech_settings: 'Technische instellingen'
|
||||
title: 'Conversatie cnfigureren'
|
||||
title: 'Conversatie configureren'
|
||||
weeks: Weken
|
||||
wordlist: 'Zwarte woordenlijst'
|
||||
confirm_email:
|
||||
@@ -183,6 +207,9 @@ nl_NL:
|
||||
if_you_did_not: 'Wanneer jij dit niet hebt aangevraagd, kun je deze e-mail negeren.'
|
||||
subject: 'Email bevestiging'
|
||||
to_confirm: 'Ga naar de volgende link om het account te bevestigen:'
|
||||
password_change:
|
||||
body: 'Het wachtwoord voor je account is gewijzigd.\n\nAls je deze wijziging niet hebt aangevraagd, neem dan contact met ons op: {0}'
|
||||
subject: '{0} wachtwoord wijziging'
|
||||
password_reset:
|
||||
if_you_did: 'Wanneer je dit zelf was,'
|
||||
please_click: 'klik dan hier om je wachtwoord te veranderen'
|
||||
@@ -192,44 +219,61 @@ nl_NL:
|
||||
embed_comments_tab: Reacties
|
||||
embedlink:
|
||||
copy: 'Kopieer naar klembord'
|
||||
copied: Gekopieerd
|
||||
error:
|
||||
ALREADY_EXISTS: 'Bron bestaat al'
|
||||
AUTHENTICATION: 'Er is een fout opgetreden tijdens het aanmelden van je account'
|
||||
CANNOT_IGNORE_STAFF: 'Kan geen Staff-leden negeren.'
|
||||
COMMENT_PARENT_NOT_VISIBLE: 'De reactie waarop je probeert te reageren bestaat niet of is inmiddels verwijderd.'
|
||||
COMMENT_TOO_SHORT: 'Reacties moeten meer dan één teken hebben. Herzie je reactie en probeer opnieuw.'
|
||||
COMMENT_TOO_LONG: 'Tekst is te lang'
|
||||
COMMENTING_CLOSED: 'Reageren is al afgesloten.'
|
||||
COMMENTING_DISABLED: 'Reageren is momenteel uigeschakeld op deze site'
|
||||
confirm_password: 'Wachtwoorden komen niet overeen. Controleer opnieuw.'
|
||||
DELETION_NOT_SCHEDULED: 'Verwijdering was niet ingepland'
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: 'Je bent niet gemachtigd om je gebruikersnaam te wijzigen.'
|
||||
EDIT_WINDOW_ENDED: 'Dit commentaar kan niet langer worden gewijzigd. Het tijdsbestek is verstreken.'
|
||||
email: 'Geen geldig e-mailadres'
|
||||
EMAIL_ALREADY_VERIFIED: 'E-mailadres is reeds geverifiëerd'
|
||||
EMAIL_IN_USE: 'E-mailadres al in gebruik'
|
||||
email_not_verified: 'E-mailadres {0} is niet geverifiëerd'
|
||||
EMAIL_NOT_VERIFIED: 'E-mailadres is niet geverifiëerd'
|
||||
email_password: 'E-mail en/of wachtwoord combinatie onjuist'
|
||||
EMAIL_REQUIRED: 'Een e-mailadres is vereist'
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: 'Ongeldig e-mail verificatie token'
|
||||
INCORRECT_PASSWORD: 'Ongeldig wachtwoord'
|
||||
INVALID_ASSET_URL: 'Ongeldige URL'
|
||||
LOGIN_MAXIMUM_EXCEEDED: 'Je hebt te veel mislukte inlogpogingen gedaan. Even geduld aub.'
|
||||
network_error: 'Kon geen verbinding maken met server. Controleer je internetverbinding en probeer het opnieuw.'
|
||||
NO_SPECIAL_CHARACTERS: 'Gebruikersnamen kunnen alleen cijfers, letters en _ bevatten.'
|
||||
NOT_AUTHORIZED: 'Je bent niet geauthoriseerd om dit te doen.'
|
||||
NOT_FOUND: 'Bron niet gevonden'
|
||||
organization_contact_email: 'Organisatie e-mailadres is niet geldig.'
|
||||
organization_name: 'Organisatienaam kan alleen cijfers en letters bevatten.'
|
||||
password: 'Wachtwoord moet minimaal 8 karakters lang zijn'
|
||||
PAGE_NOT_AVAILABLE_ROLE: 'Deze pagina is enkel voor toegankelijk voor teamleden. Neem contact op met een admin als je toegang tot dit team wilt.'
|
||||
PASSWORD_INCORRECT: 'Je huidige wachtwoord is incorrect ingevuld'
|
||||
PASSWORD_LENGTH: 'Wachtwoord is te kort'
|
||||
PASSWORD_REQUIRED: 'Je moet een wachtwoord invoeren.'
|
||||
PASSWORD_RESET_TOKEN_INVALID: 'Je wachtwoord-herstel token is ongeldig.'
|
||||
PROFANITY_ERROR: 'Gebruikersnamen mogen niet aanstootgevend zijn. Neem contact op met de administrator wanneer je denkt dat dit niet klopt.'
|
||||
RATE_LIMIT_EXCEEDED: 'Gebruikslimiet overschreden'
|
||||
required_field: 'Dit veld is verplicht'
|
||||
SAME_USERNAME_PROVIDED: 'Je moet een andere gebruikersnaam invoeren.'
|
||||
temporarily_suspended: 'Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions.'
|
||||
temporarily_suspended: 'Je account is momenteel geschorst. Deze zal worden gereactiveerd {0}. Neem s.v.p. contact met ons op voor vragen.'
|
||||
unexpected: 'Onverwachte fout opgetreden. Sorry!'
|
||||
username: 'Gebruikersnamen kunnen alleen cijfers, letters en _ bevatten.'
|
||||
USERNAME_IN_USE: 'Gebruikersnaam al in gebruik'
|
||||
USERNAME_REQUIRED: 'Gebruikersnaam is vereist'
|
||||
flag_comment: 'Rapporteer reactie'
|
||||
flag_reason: 'Reden voor rapporteren (optioneel)'
|
||||
flag_reasons:
|
||||
username:
|
||||
impersonating: 'Deze gebruiker doet zich voor als iemand anders'
|
||||
nolike: 'Ik vind deze gebruikersnaam niet leuk'
|
||||
offensive: 'Deze gebruikersnaam is aanstootgevend'
|
||||
other: Andere
|
||||
spam: 'Dit lijkt op een advertentie/marketing'
|
||||
flag_username: 'Rapporteer gebruikersnaam'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} heeft gebruikersnaam {1} goedgekeurd'
|
||||
@@ -258,6 +302,7 @@ nl_NL:
|
||||
changed_name:
|
||||
msg: 'De verandering van je gebruikersnaam wordt door ons moderatieteam gecontroleerd.'
|
||||
comment: reactie
|
||||
comment_is_deleted: 'De reageerder heeft zijn account verwijderd.'
|
||||
comment_is_hidden: 'Deze reactie is niet beschikbaar.'
|
||||
comment_is_ignored: 'Deze reactie is verborgen omdat je de gebruiker negeert.'
|
||||
comment_is_rejected: 'Je hebt deze reactie afgewezen.'
|
||||
@@ -306,6 +351,17 @@ nl_NL:
|
||||
title: 'Toegestane domeinnamen'
|
||||
like: Like
|
||||
loading_results: 'Resultaten worden geladen'
|
||||
login:
|
||||
email_address: E-mailadres
|
||||
forgot_password: 'Wachtwoord vergeten?'
|
||||
go_back: 'Ga terug'
|
||||
sign_in: Inloggen
|
||||
sign_in_button: Inloggen
|
||||
sign_in_message: 'Log in om te communiceren met je community'
|
||||
password: Wachtwoord
|
||||
reset_password_send_button: 'Wachtwoord ophalen'
|
||||
request_passowrd: 'Vraag een nieuwe aan.'
|
||||
team_sign_in: 'Team login'
|
||||
marketing: 'Dit lijkt op een advertentie/marketing'
|
||||
moderate_this_stream: 'Modereer deze conversatie'
|
||||
modqueue:
|
||||
@@ -350,6 +406,8 @@ nl_NL:
|
||||
show_shortcuts: 'Toon sneltoetsen'
|
||||
singleview: Zen-modus
|
||||
sort: Sorteer
|
||||
suspend: 'Schors gebruiker'
|
||||
system_withheld: 'Achtergehouden door systeem'
|
||||
thismenu: 'Open dit menu'
|
||||
thousand: k
|
||||
toggle_search: 'Open zoekvenster'
|
||||
@@ -363,6 +421,7 @@ nl_NL:
|
||||
other: Ander
|
||||
password_reset:
|
||||
change_password: 'Verander wachtwoord'
|
||||
change_password_help: 'Voer een nieuw wachtwoord in om mee in te loggen. Gebruik een veilig wachtwoord!'
|
||||
confirm_new_password: 'Bevestig nieuw wachtwoord'
|
||||
new_password: 'Nieuw wachtwoord'
|
||||
new_password_help: 'Wachtwoord moet minimaal 8 karakters lang zijn.'
|
||||
@@ -384,6 +443,13 @@ nl_NL:
|
||||
username: gebruikersnaam
|
||||
write_message: 'Schrijf een bericht'
|
||||
yes_suspend: 'Ja, schors'
|
||||
reject_username_dialog:
|
||||
cancel: Annuleren
|
||||
description: 'Help ons dit te begrijpen'
|
||||
message: 'Reden voor rapporteren (Optioneel)'
|
||||
reason: Reden
|
||||
reject_username: 'Gebruikersnaam afwijzen'
|
||||
title: 'Gebruikersnaam afwijzen'
|
||||
reply: Beantwoord
|
||||
report: Rapporteer
|
||||
report_notif: 'Dank voor het rapporteren van deze reactie. Deze wordt zo snel mogelijk gemodereerd.'
|
||||
@@ -443,30 +509,43 @@ nl_NL:
|
||||
user_detail:
|
||||
all: Alle
|
||||
ban: 'Gebruiker verbannen'
|
||||
email: Email
|
||||
email: E-mailadres
|
||||
id: ID
|
||||
karma: Karma
|
||||
karma_docs_link: 'https://docs.coralproject.net/talk/trust/#user-karma-score'
|
||||
learn_more: 'Meer informatie'
|
||||
member_since: 'Lid sinds'
|
||||
reject_rate: 'Beoordeling verwijderen'
|
||||
reject_username: 'Gebruikersnaam afwijzen'
|
||||
rejected: Afgewezen
|
||||
remove_ban: 'Verbanning verwijderen'
|
||||
remove_suspension: 'Schorsing opheffen'
|
||||
suspend: 'Gebruiker schorsen'
|
||||
suspended: Geschorst
|
||||
total_comments: 'Totaal aantal reacties'
|
||||
unreliable: Onbetrouwbaar
|
||||
user_history: Accountgeschiedenis
|
||||
user_karma_score: 'Gebruiker Karma Score'
|
||||
username: Gebruikersnaam
|
||||
username_needs_approval: 'Gebruikersnaam heeft goedkeuring nodig'
|
||||
username_rejected: 'Gebruikersnaam afgewezen'
|
||||
user_history:
|
||||
action: Action
|
||||
ban_removed: 'Ban removed'
|
||||
date: Date
|
||||
suspended: 'Suspended, {0}'
|
||||
suspension_removed: 'Suspension removed'
|
||||
system: System
|
||||
taken_by: 'Taken By'
|
||||
user_banned: 'User banned'
|
||||
username_status: 'Username {0}'
|
||||
user_impersonating: 'Deze gebruiker imiteert'
|
||||
action: Actie
|
||||
ban_removed: 'Schorsing opgeheven'
|
||||
date: Datum
|
||||
suspended: 'Geschorst, {0}'
|
||||
suspension_removed: 'Schorsing opgeheven'
|
||||
system: Systeem
|
||||
taken_by: 'Genomen door'
|
||||
user_banned: 'Gebruiker geschorst'
|
||||
username_status: 'Gebruikersnaam {0}'
|
||||
user_impersonating: 'Deze gebruiker doet zich voor als iemand anders'
|
||||
user_no_comment: 'Je hebt nog niet eerder gereageerd. Laat je mening horen!'
|
||||
username_offensive: 'Dit is een aanstootgevende gebruikersnaam.'
|
||||
validators:
|
||||
confirm_password: 'Wachtwoorden komen niet overeen. Controleer opnieuw.'
|
||||
confirm_email: 'E-mailadressen komen niet overeen. Controleer opnieuw.'
|
||||
confirm_password: Wachtwoorden komen niet overeen. Controleer opnieuw.
|
||||
required: 'Dit veld is verplicht'
|
||||
verify_email: 'Geen geldig e-mailadres'
|
||||
verify_organization_name: 'Organisatienaam kan alleen cijfers en letters bevatten.'
|
||||
verify_password: 'Wachtwoord moet minimaal 8 karakters lang zijn'
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const staticServer = require('express-static-gzip');
|
||||
const { merge } = require('lodash');
|
||||
const { EMBED_EXPIRY_TIME, STATIC_EXPIRY_TIME } = require('../config');
|
||||
|
||||
// EMBED_CACHE_CONTROL_HEADER is the header sent when the file is embed.js.
|
||||
const EMBED_CACHE_CONTROL_HEADER = [
|
||||
'public',
|
||||
`max-age=${Math.floor(EMBED_EXPIRY_TIME / 1000)}`,
|
||||
'immutable',
|
||||
].join(', ');
|
||||
|
||||
// Define the options to be applied to all static files, the embed itself has a
|
||||
// separate override.
|
||||
const defaultOpts = {
|
||||
maxAge: STATIC_EXPIRY_TIME,
|
||||
immutable: true,
|
||||
setHeaders: (res, path) => {
|
||||
if (path.includes('/dist/embed.js')) {
|
||||
// embed.js has a different max-age then the rest of the static files.
|
||||
res.setHeader('Cache-Control', EMBED_CACHE_CONTROL_HEADER);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Setup the configuration for the compression options on how to serve files.
|
||||
const compressionOpts = {
|
||||
indexFromEmptyFile: false,
|
||||
enableBrotli: true,
|
||||
customCompressions: [{ encodingName: 'deflate', fileExtension: 'zz' }],
|
||||
};
|
||||
|
||||
// Serve the directories under ../dist.
|
||||
const dist = path.resolve(path.join(__dirname, '../dist'));
|
||||
|
||||
/**
|
||||
* middleware in production will serve compressed files if available, otherwise
|
||||
* it will use express's static middleware.
|
||||
*/
|
||||
module.exports =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? staticServer(dist, merge(compressionOpts, defaultOpts))
|
||||
: express.static(dist, defaultOpts);
|
||||
+16
-6
@@ -1,11 +1,21 @@
|
||||
const { HTTP_X_REQUEST_ID } = require('../config');
|
||||
const uuid = require('uuid/v1');
|
||||
|
||||
// Trace middleware attaches a request id to each incoming request.
|
||||
module.exports = (req, res, next) => {
|
||||
req.id = uuid();
|
||||
module.exports = HTTP_X_REQUEST_ID
|
||||
? (req, res, next) => {
|
||||
req.id = req.get(HTTP_X_REQUEST_ID) || uuid();
|
||||
|
||||
// Add the context ID to the request as an HTTP header.
|
||||
res.set('X-Talk-Trace-ID', req.id);
|
||||
// Add the context ID to the request as an HTTP header.
|
||||
res.set('X-Talk-Trace-ID', req.id);
|
||||
|
||||
next();
|
||||
};
|
||||
next();
|
||||
}
|
||||
: (req, res, next) => {
|
||||
req.id = uuid();
|
||||
|
||||
// Add the context ID to the request as an HTTP header.
|
||||
res.set('X-Talk-Trace-ID', req.id);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.6.6",
|
||||
"version": "4.6.11",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
@@ -58,6 +58,7 @@
|
||||
"dependencies": {
|
||||
"@coralproject/gql-merge": "^0.1.0",
|
||||
"@coralproject/graphql-anywhere-optimized": "^0.1.0",
|
||||
"@ungap/url-search-params": "^0.1.2",
|
||||
"apollo-client": "^1.9.1",
|
||||
"apollo-engine": "^0.8.1",
|
||||
"apollo-server-express": "^1.2.0",
|
||||
@@ -105,7 +106,7 @@
|
||||
"eventemitter2": "^4.1.2",
|
||||
"exports-loader": "^0.6.4",
|
||||
"express": "4.16.0",
|
||||
"express-static-gzip": "^0.3.1",
|
||||
"express-static-gzip": "^1.1.1",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^0.11.2",
|
||||
"final-form": "^4.8.1",
|
||||
@@ -130,6 +131,7 @@
|
||||
"imports-loader": "^0.7.1",
|
||||
"inquirer": "^3.2.2",
|
||||
"inquirer-autocomplete-prompt": "^0.12.1",
|
||||
"intersection-observer": "^0.5.1",
|
||||
"ioredis": "3.1.4",
|
||||
"ip": "^1.1.5",
|
||||
"jest": "^23.0.0",
|
||||
@@ -210,7 +212,6 @@
|
||||
"url-join": "^2.0.2",
|
||||
"url-loader": "^0.6.0",
|
||||
"url-parse": "^1.4.3",
|
||||
"url-search-params": "^0.9.0",
|
||||
"uuid": "^3.1.0",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-manifest-plugin": "^2.0.0-rc.2",
|
||||
|
||||
@@ -297,49 +297,58 @@ fr:
|
||||
write_your_username: "Modifier votre nom d'utilisateur"
|
||||
your_username: "Votre nom d'utilisateur apparaît sur chaque commentaire que vous publiez."
|
||||
nl_NL:
|
||||
sign_in:
|
||||
email_verify_cta: "Controleer je e-mailadres."
|
||||
request_new_verify_email: "Vraag nieuwe bevestigingsemail aan"
|
||||
verify_email: "Bedankt voor het aanmaken van een account! We hebben een email verstuurd naar het adres dat je hebt opgegeven om je account te verifiëren."
|
||||
verify_email2: "Je account moet worden geverifiëerd voordat je kunt deelnemen in de community."
|
||||
not_you: "Ben je dit niet?"
|
||||
logged_in_as: "Ingelogd als"
|
||||
facebook_sign_in: "Inloggen met Facebook"
|
||||
facebook_sign_up: "Registreren met Facebook"
|
||||
logout: "Uitloggen"
|
||||
sign_in: "Inloggen"
|
||||
sign_in_to_join: "Log in om deel te nemen"
|
||||
or: "Of"
|
||||
email: "E-mailadres"
|
||||
password: "Wachtwoord"
|
||||
forgot_your_pass: "Wachtwoord vergeten?"
|
||||
need_an_account: "Heb je een account nodig?"
|
||||
register: "Registreren"
|
||||
sign_up: "Aanmelden"
|
||||
confirm_password: "Bevestig wachtwoord"
|
||||
username: "Gebruikersnaam"
|
||||
already_have_an_account: "Heb je al een account?"
|
||||
recover_password: "Wachtwoord herstellen"
|
||||
email_in_use: "E-mailadres is al in gebruik"
|
||||
email_or_username_in_use: "E-mailadres of gebruikersnaam is al in gebruik"
|
||||
required_field: "Dit is een vereist veld"
|
||||
passwords_dont_match: "Wachtwoorden komen niet overeen"
|
||||
special_characters: "Gebruikersnamen kunnen alleen letters, cijfers en _ bevatten"
|
||||
sign_in_to_comment: "Aanmelden om te reageren"
|
||||
check_the_form: "Het formulier bevat ongeldige invoer, controleer je invoer"
|
||||
createdisplay:
|
||||
check_the_form: "Het formulier bevat ongeldige invoer, controleer je invoer"
|
||||
continue: "Doorgaan met dezelfde Facebook gebruikersnaam"
|
||||
error_create: "Fout opgetreden bij het wijzigen van de gebruikersnaam"
|
||||
fake_comment_body: "Dit is een voorbeeldreactie. Lezers kunnen hun gedachten en meningen met newsrooms delen in het reactie-gedeelte"
|
||||
fake_comment_date: "1 minuut geleden"
|
||||
if_you_dont_change_your_name: "Wanneer je je gebruikersnaam nu niet wijzigt, zal je Facebook naam bij al je reacties komen te staan."
|
||||
required_field: "Vereist veld"
|
||||
save: Opslaan
|
||||
special_characters: "Gebruikersnamen kunnen alleen letters, cijfers en _ bevatten"
|
||||
username: Gebruikersnaam
|
||||
write_your_username: "Wijzig je gebruikersnaam"
|
||||
your_username: "Je gebruikersnaam verschijnt bij al je reacties."
|
||||
talk-plugin-auth:
|
||||
login:
|
||||
email_verify_cta: "Controleer je e-mailadres."
|
||||
request_new_verify_email: "Vraag nieuwe bevestigingsemail aan"
|
||||
verify_email: "Bedankt voor het aanmaken van een account! We hebben een email verstuurd naar het adres dat je hebt opgegeven om je account te verifiëren."
|
||||
verify_email2: "Je account moet worden geverifiëerd voordat je kunt deelnemen in de community."
|
||||
not_you: "Ben je dit niet?"
|
||||
logged_in_as: "Ingelogd als"
|
||||
logout: "Uitloggen"
|
||||
sign_in: "Inloggen"
|
||||
sign_in_to_join: "Log in om deel te nemen"
|
||||
or: "Of"
|
||||
email: "E-mailadres"
|
||||
password: "Wachtwoord"
|
||||
password_error: "Wachtwoord moet minstens 8 karakters bevatten."
|
||||
forgot_your_pass: "Wachtwoord vergeten?"
|
||||
need_an_account: "Heb je een account nodig?"
|
||||
register: "Registreren"
|
||||
sign_up: "Aanmelden"
|
||||
confirm_password: "Bevestig wachtwoord"
|
||||
username: "Gebruikersnaam"
|
||||
already_have_an_account: "Heb je al een account?"
|
||||
recover_password: "Wachtwoord herstellen"
|
||||
email_in_use: "E-mailadres is al in gebruik"
|
||||
email_or_username_in_use: "E-mailadres of gebruikersnaam is al in gebruik"
|
||||
required_field: "Dit is een vereist veld"
|
||||
passwords_dont_match: "Wachtwoorden komen niet overeen"
|
||||
special_characters: "Gebruikersnamen kunnen alleen letters, cijfers en _ bevatten"
|
||||
sign_in_to_comment: "Aanmelden om te reageren"
|
||||
check_the_form: "Het formulier bevat ongeldige invoer, controleer je invoer"
|
||||
set_username_dialog:
|
||||
check_the_form: "Het formulier bevat ongeldige invoer, controleer je invoer"
|
||||
continue: "Doorgaan met dezelfde Facebook gebruikersnaam"
|
||||
error_create: "Fout opgetreden bij het wijzigen van de gebruikersnaam"
|
||||
fake_comment_body: "Dit is een voorbeeldreactie. Lezers kunnen hun gedachten en meningen met newsrooms delen in het reactie-gedeelte"
|
||||
fake_comment_date: "1 minuut geleden"
|
||||
if_you_dont_change_your_name: "Wanneer je je gebruikersnaam nu niet wijzigt, zal je Facebook naam bij al je reacties komen te staan."
|
||||
required_field: "Vereist veld"
|
||||
save: Opslaan
|
||||
special_characters: "Gebruikersnamen kunnen alleen letters, cijfers en _ bevatten"
|
||||
username: Gebruikersnaam
|
||||
write_your_username: "Wijzig je gebruikersnaam"
|
||||
your_username: "Je gebruikersnaam verschijnt bij al je reacties."
|
||||
change_password:
|
||||
change_password: "Wachtwoord Wijzigen"
|
||||
passwords_dont_match: "Wachtwoorden komen niet overeen"
|
||||
required_field: "Dit veld is verplicht"
|
||||
forgot_password: "Wachtwoord vergeten?"
|
||||
save: "Opslaan"
|
||||
cancel: "Annuleren"
|
||||
edit: "Wijzigen"
|
||||
changed_password_msg: "Je wachtwoord is succesvol gewijzigd"
|
||||
pt_BR:
|
||||
talk-plugin-auth:
|
||||
login:
|
||||
|
||||
@@ -2,6 +2,10 @@ ar:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "تسجيل الدخول عبر حساب الفيسبوك"
|
||||
sign_up: "اشترك عبر حساب الفيسبوك"
|
||||
de:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "Mit Facebook anmelden"
|
||||
sign_up: "Mit Facebook registrieren"
|
||||
en:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "Sign in with Facebook"
|
||||
@@ -14,6 +18,10 @@ fr:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "Connectez-vous avec Facebook"
|
||||
sign_up: "Inscrivez-vous avec Facebook"
|
||||
nl_NL:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "Inloggen met Facebook"
|
||||
sign_up: "Registreren met Facebook"
|
||||
zh_CN:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "使用 Facebook 帐号"
|
||||
@@ -22,7 +30,3 @@ zh_TW:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "使用 Facebook 帳號"
|
||||
sign_up: "使用 Facebook 帳號"
|
||||
de:
|
||||
talk-plugin-facebook-auth:
|
||||
sign_in: "Mit Facebook anmelden"
|
||||
sign_up: "Mit Facebook registrieren"
|
||||
|
||||
@@ -14,6 +14,10 @@ fr:
|
||||
talk-plugin-google-auth:
|
||||
sign_in: "Connectez-vous avec Google"
|
||||
sign_up: "Inscrivez-vous avec Google"
|
||||
nl_NL:
|
||||
talk-plugin-google-auth:
|
||||
sign_in: "Inloggen met Google"
|
||||
sign_up: "Registeren met Google"
|
||||
zh_CN:
|
||||
talk-plugin-google-auth:
|
||||
sign_in: "使用 Google 帐号"
|
||||
|
||||
@@ -315,8 +315,8 @@ es:
|
||||
save: "Salvar"
|
||||
cancel: "Cancelar"
|
||||
edit: "Editar"
|
||||
changed_password_msg: "Senha alterada - Sua senha foi alterada com sucesso"
|
||||
forgot_password_sent: "Esqueceu a senha - Nós enviamos um email para recuperação da senha"
|
||||
changed_password_msg: "Contraseña cambiada - Su contraseña ha sido cambiado"
|
||||
forgot_password_sent: "Contraseña olvidada - Enviamos un email para recuperar la contraseña"
|
||||
change_password: "Cambiar Contraseña"
|
||||
passwords_dont_match: "Las contraseñas no coinciden"
|
||||
required_field: "Este campo es requerido"
|
||||
@@ -376,3 +376,79 @@ es:
|
||||
description_2: "Puedes cambiar la configuración de tu cuenta visitando"
|
||||
path: "Mi perfil > Configuración"
|
||||
alert: "¡Correo electrónico agregado!"
|
||||
nl_NL:
|
||||
email:
|
||||
email_change_original:
|
||||
subject: E-mailadres wijziging
|
||||
body: "Je e-mailadres is gewijzigd van {0} naar {1}. Als je deze wijziging niet hebt aangevraagd, neem dan s.v.p. contact op: {2}."
|
||||
error:
|
||||
NO_LOCAL_PROFILE: Er is geen bestaand e-mailadres geassocieerd met dit account.
|
||||
LOCAL_PROFILE: Er is reeds een e-mailadres geassocieerd met dit account.
|
||||
INCORRECT_PASSWORD: Het opgegeven wachtwoord is onjuist.
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
change_password: "Wachtwoord wijzigen"
|
||||
passwords_dont_match: "Wachtwoorden komen niet overeen"
|
||||
required_field: "Dit veld is verplicht"
|
||||
forgot_password: "Wachtwoord vergeten?"
|
||||
old_password: "Oud Wachtwoord"
|
||||
new_password: "Nieuw Wachtwoord"
|
||||
confirm_new_password: "Bevestig Nieuw Wachtwoord"
|
||||
save: "Opslaan"
|
||||
cancel: "Annuleren"
|
||||
edit: "Wijzigen"
|
||||
changed_password_msg: "Wachtwoord Gewijzigd - Je wachtwoord is succesvol gewijzigd"
|
||||
forgot_password_sent: "Wachtwoord Vergeten - We hebben je een e-mail gestuurd om je wachtwoord te herstellen"
|
||||
change_username:
|
||||
change_username_note: "Gebruikersnamen kunnen eens per 14 dagen worden gewijzigd."
|
||||
is_not_eligible: "Je kunt je gebruikersnaam momenteel niet wijzigen."
|
||||
save: "Opslaan"
|
||||
edit_profile: "Profiel wijzigen"
|
||||
cancel: "Annuleren"
|
||||
confirm_username_change: "Bevestig Gebruikersnaam Wijziging"
|
||||
description: "Je probeert je gebruikersnaam te wijzigen. Je nieuwe gebruikersnaam zal verschijnen bij al je huidige en toekomstige reacties."
|
||||
old_username: "Oude Gebruikersnaam"
|
||||
new_username: "Nieuwe Gebruikersnaam"
|
||||
re_enter: "Voer je nieuwe gebruikersnaam opnieuw in"
|
||||
bottom_note: "Let op: Je kan je gebruikersnaam niet opnieuw wijzigen in de komende 14 dagen."
|
||||
confirm_changes: "Bevestig Wijzigingen"
|
||||
username_does_not_match: "Gebruikersnaam komt niet overeen"
|
||||
cant_be_equal: "Je nieuwe {0} moet verschillen van je huidige"
|
||||
changed_username_success_msg: "Gebruikersnaam Gewijzigd - Je gebruikersnaam is succesvol gewijzigd. Je kan je gebruikersnaam niet wijzigen voor de komende 14 dagen."
|
||||
change_username_attempt: "Gebruikersnaam kan niet worden gewijzigd. Gebruikersnamen kunnen eens per 14 dagen worden gewijzigd."
|
||||
change_email:
|
||||
confirm_email_change: "Bevestig E-mailadres Wijziging"
|
||||
description: "Je probeert je e-mailadres te wijzigen. Je nieuwe e-mailadres zal worden gebruikt om in te loggen en voor het ontvangen van account notificaties."
|
||||
old_email: "Oude E-mailadres"
|
||||
new_email: "Nieuwe E-mailadres"
|
||||
enter_password: "Wachtwoord Invoeren"
|
||||
incorrect_password: "Ongeldig Wachtwoord"
|
||||
confirm_change: "Bevestig Wijziging"
|
||||
cancel: "Annuleren"
|
||||
change_email_msg: "E-mailadres Gewijzigd. Dit E-mailadres zal nu worden gebruikt om in te loggen en voor het ontvangen van account notificaties."
|
||||
add_email:
|
||||
add_email_address: "E-mailadres Toevoegen"
|
||||
enter_email_address: "E-mailadres Invoeren:"
|
||||
invalid_email_address: "Ongeldig E-mailadres"
|
||||
confirm_email_address: "Bevestig E-mailadres:"
|
||||
email_does_not_match: "E-mailadres komt niet overeen"
|
||||
insert_password: "Wachtwoord Invoeren:"
|
||||
confirm_password: "Wachtwoord Bevestigen:"
|
||||
required_field: "Dit veld is verplicht"
|
||||
done: "Klaar"
|
||||
content:
|
||||
title: "Voeg een e-mailadres toe"
|
||||
description: "Voor je veiligheid vragen we gebruikers om een e-mailadres toe te voegen aan hun account. Je e-mailadres zal worden gebruikt om:"
|
||||
item_1: "Updates te ontvangen omtrent wijzigingen in je account (e-mailadres, gebruikersnaam, wachtwoord, etc.)"
|
||||
item_2: "Je reacties te kunnen downloaden."
|
||||
item_3: "Reactie notificaties te sturen indien je hebt gekozen deze te ontvangen."
|
||||
verify:
|
||||
title: "Bevestig Je E-mailadres"
|
||||
description: "We hebben een e-mail gestuurd aan {0} om je account te bevestigen. Je moet je e-mailadres verifiëren zodat deze gebruikt kan worden voor bevestigingen omtrent account wijzigingen en voor notificaties."
|
||||
added:
|
||||
title: "E-mailadres Toegevoegd"
|
||||
description: "Je e-mailadres is toegevoegd aan je account."
|
||||
subtitle: "Wil je je e-mailadres wijzigen?"
|
||||
description_2: "Je kan je account instellingen wijzigen door te gaan naar"
|
||||
path: "Mijn Profiel > Instellingen"
|
||||
alert: "E-mailadres Toegevoegd!"
|
||||
|
||||
@@ -24,8 +24,8 @@ fr:
|
||||
loved: Loved
|
||||
nl_NL:
|
||||
talk-plugin-love:
|
||||
love: Ik hou er van
|
||||
loved: Geliefd
|
||||
love: Love
|
||||
loved: Loved
|
||||
pt_BR:
|
||||
talk-plugin-love:
|
||||
love: Love
|
||||
|
||||
@@ -18,7 +18,7 @@ fr:
|
||||
member_since: "Member Since"
|
||||
nl_NL:
|
||||
talk-plugin-member-since:
|
||||
member_since: "Gebruiker sinds"
|
||||
member_since: "Lid sinds"
|
||||
pt_BR:
|
||||
talk-plugin-member-since:
|
||||
member_since: "Membro desde"
|
||||
|
||||
@@ -10,3 +10,6 @@ es:
|
||||
de:
|
||||
talk-plugin-notifications-category-reply:
|
||||
toggle_description: Jemand antwortet auf meinen Kommentar
|
||||
nl_NL:
|
||||
talk-plugin-notifications-category-reply:
|
||||
toggle_description: Iemand antwoord op mijn reactie
|
||||
|
||||
@@ -10,3 +10,6 @@ es:
|
||||
de:
|
||||
talk-plugin-notifications-category-staff:
|
||||
toggle_description: Ein Redaktionsmitglied antwortet auf meinen Kommentar
|
||||
nl_NL:
|
||||
talk-plugin-notifications-category-staff:
|
||||
toggle_description: Een redactielid antwoord op mijn reactie
|
||||
|
||||
@@ -23,3 +23,9 @@ de:
|
||||
staff:
|
||||
subject: "Jemand hat bei {0} auf Ihren Kommentar geantwortet"
|
||||
body: "{0}\n{1} arbeitet für {2} und hat auf Ihren Kommentar geantwortet: {3}"
|
||||
nl_NL:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
staff:
|
||||
subject: "Iemand bij {0} heeft geantwoord op je reactie"
|
||||
body: "{0}\n{1} werkt voor {2} en heeft geantwoord op je reactie: {3}"
|
||||
|
||||
@@ -14,3 +14,7 @@ de:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
DAILY: Einmal täglich
|
||||
nl_NL:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
DAILY: In een dagelijkse samenvatting
|
||||
|
||||
@@ -14,3 +14,7 @@ de:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
HOURLY: Stündlich
|
||||
nl_NL:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
HOURLY: In een uurlijkse samenvatting
|
||||
|
||||
@@ -70,3 +70,21 @@ de:
|
||||
digest_option: Benachrichtigungen senden
|
||||
digest_enum:
|
||||
NONE: Sofort
|
||||
nl_NL:
|
||||
talk-plugin-notifications:
|
||||
settings_title: Notificaties
|
||||
settings_subtitle: Ontvang notificaties wanneer
|
||||
turn_off_all: Ik wil geen notificaties ontvangen
|
||||
banner_info:
|
||||
title: E-mail bevestiging vereist
|
||||
text: Je moet een geverifieerd e-mailadres hebben om e-mail notificaties te ontvangen
|
||||
verify_now: Verifieer je e-mailadres nu
|
||||
banner_success:
|
||||
title: E-mail verificatie verzonden
|
||||
text: Een e-mail met verificatielink is verstuurd naar {0}.
|
||||
banner_error:
|
||||
title: Fout
|
||||
text: Er is een fout opgetreden tijdens het versturen van je verificatie e-mail. Probeer het later nog eens.
|
||||
digest_option: Notificaties versturen
|
||||
digest_enum:
|
||||
NONE: Onmiddelijk
|
||||
|
||||
@@ -40,3 +40,17 @@ de:
|
||||
confirm: "Bestätigen"
|
||||
are_unsubscribed: "Sie haben haben alle Benachrichtigungen erfolgreich abbestellt."
|
||||
token_invalid: "Der Abbestell-Link ist ungültig. Klicken Sie den Link einer neueren E-Mail oder gehen Sie zu einem Kommentarbereich, melden Sie sich an und ändern Sie dort Ihre Benachrichtigungseinstellungen"
|
||||
nl_NL:
|
||||
talk-plugin-notifications:
|
||||
templates:
|
||||
digest:
|
||||
subject: "Je recente reactie-activiteit op {0}"
|
||||
footer: "Je hebt deze notificatie ontvangen omdat je een reageerder bent op {0} en je hebt je aangemeld om notificaties te ontvangen."
|
||||
links:
|
||||
unsubscribe: "Afmelden voor reactie notificaties"
|
||||
unsubscribe_page:
|
||||
unsubscribe: "Afmelden voor reactie notificaties"
|
||||
click_to_confirm: "Klik onderstaande om te bevestigen dat je je wilt afmelden voor alle notificaties"
|
||||
confirm: "Bevestigen"
|
||||
are_unsubscribed: "Je bent nu afgemeld voor alle notificaties."
|
||||
token_invalid: "Afmeldlink is ongeldig, klik de link van een recentere e-mail of bezoek een pagina met reacties en log in om je notificatie voorkeuren te wijzigen"
|
||||
|
||||
+9
-20
@@ -10,7 +10,7 @@ const compression = require('compression');
|
||||
const plugins = require('../services/plugins');
|
||||
const staticTemplate = require('../middleware/staticTemplate');
|
||||
const nonce = require('../middleware/nonce');
|
||||
const staticServer = require('express-static-gzip');
|
||||
const staticFiles = require('../middleware/staticFiles');
|
||||
const { DISABLE_STATIC_SERVER } = require('../config');
|
||||
const { passport } = require('../services/passport');
|
||||
const { MOUNT_PATH } = require('../url');
|
||||
@@ -32,6 +32,8 @@ if (!DISABLE_STATIC_SERVER) {
|
||||
|
||||
/**
|
||||
* Redirect old embed calls.
|
||||
*
|
||||
* TODO: (wyattjoh) remove this on the next minor release
|
||||
*/
|
||||
const oldEmbed = url.resolve(MOUNT_PATH, 'embed.js');
|
||||
const newEmbed = url.resolve(MOUNT_PATH, 'static/embed.js');
|
||||
@@ -43,28 +45,15 @@ if (!DISABLE_STATIC_SERVER) {
|
||||
});
|
||||
|
||||
/**
|
||||
* Serve the directories under dist.
|
||||
* Setup static file serving.
|
||||
*/
|
||||
const dist = path.resolve(path.join(__dirname, '../dist'));
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
router.use(
|
||||
'/static',
|
||||
staticServer(dist, {
|
||||
indexFromEmptyFile: false,
|
||||
enableBrotli: true,
|
||||
customCompressions: [
|
||||
{
|
||||
encodingName: 'deflate',
|
||||
fileExtension: 'zz',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
} else {
|
||||
router.use('/static', express.static(dist));
|
||||
}
|
||||
router.use('/static', staticFiles);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Shared Middleware
|
||||
//==============================================================================
|
||||
|
||||
// Add the i18n middleware to all routes.
|
||||
router.use(i18n);
|
||||
|
||||
|
||||
+30
-13
@@ -159,6 +159,9 @@ const config = {
|
||||
'process.env': {
|
||||
VERSION: `"${require('./package.json').version}"`,
|
||||
NODE_ENV: `${JSON.stringify(process.env.NODE_ENV)}`,
|
||||
TALK_DEFAULT_LAZY_RENDER: `${JSON.stringify(
|
||||
process.env.TALK_DEFAULT_LAZY_RENDER
|
||||
)}`,
|
||||
},
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
@@ -307,19 +310,32 @@ const applyConfig = (entries, root = {}) =>
|
||||
config,
|
||||
{
|
||||
entry: entries.reduce(
|
||||
(entry, { name, path: modulePath, disablePolyfill = false }) => {
|
||||
const entries = [
|
||||
path.join(
|
||||
__dirname,
|
||||
'client/coral-framework/helpers/webpackGlobals'
|
||||
),
|
||||
];
|
||||
if (disablePolyfill) {
|
||||
entries.push(modulePath);
|
||||
} else {
|
||||
entries.unshift('babel-polyfill');
|
||||
entries.push(modulePath);
|
||||
(
|
||||
entry,
|
||||
{
|
||||
name,
|
||||
path: modulePath,
|
||||
disablePolyfill = false,
|
||||
disableWebpackGlobals = false,
|
||||
}
|
||||
) => {
|
||||
// Create all the entries to be added to the final build target.
|
||||
const entries = [];
|
||||
|
||||
if (!disablePolyfill) {
|
||||
entries.push('babel-polyfill');
|
||||
}
|
||||
|
||||
if (!disableWebpackGlobals) {
|
||||
entries.push(
|
||||
path.join(
|
||||
__dirname,
|
||||
'client/coral-framework/helpers/webpackGlobals'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
entries.push(modulePath);
|
||||
|
||||
entry[name] = entries;
|
||||
|
||||
@@ -347,7 +363,8 @@ module.exports = [
|
||||
{
|
||||
name: 'embed',
|
||||
path: path.join(__dirname, 'client/coral-embed/src/index'),
|
||||
disablePolyfill: process.env.TALK_DISABLE_EMBED_POLYFILL === 'TRUE',
|
||||
disablePolyfill: true,
|
||||
disableWebpackGlobals: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
|
||||
@@ -221,6 +221,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@ungap/url-search-params@^0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/url-search-params/-/url-search-params-0.1.2.tgz#8ba8c0527543fe675d1c29ae0a2daca842e8ee4f"
|
||||
integrity sha512-WVk5+lJ+AoNLh2sIDMhnEAgLsVQuI067hWLJCzirErH1GYiy1gs09q4+XZxYWSvdAsslKsaO4q1iXXMx2c72dA==
|
||||
|
||||
JSONStream@^1.0.7:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.3.tgz#27b4b8fbbfeab4e71bcf551e7f27be8d952239bf"
|
||||
@@ -4277,10 +4282,10 @@ exports-loader@^0.6.4:
|
||||
loader-utils "^1.0.2"
|
||||
source-map "0.5.x"
|
||||
|
||||
express-static-gzip@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-0.3.2.tgz#89ede84547a5717de3146315f62dc996c071a88d"
|
||||
integrity sha512-xFOW5Lxrh4xLey5i6gGWHOFznJayGCxazUau0kq7ElUh1t7q2B6IlvWv4d3UJwJej+aXEu9os/VpzPvRchdNiA==
|
||||
express-static-gzip@^1.1.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-1.1.3.tgz#345ea02637d9d5865777d6fb57ccc0884abcda65"
|
||||
integrity sha512-k8Q4Dx4PDpzEb8kth4uiPWrBeJWJYSgnWMzNdjQUOsEyXfYKbsyZDkU/uXYKcorRwOie5Vzp4RMEVrJLMfB6rA==
|
||||
dependencies:
|
||||
serve-static "^1.12.3"
|
||||
|
||||
@@ -6042,6 +6047,11 @@ interpret@^1.0.0, interpret@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
|
||||
integrity sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=
|
||||
|
||||
intersection-observer@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.5.1.tgz#e340fc56ce74290fe2b2394d1ce88c4353ac6dfa"
|
||||
integrity sha512-Zd7Plneq82kiXFixs7bX62YnuZ0BMRci9br7io88LwDyF3V43cQMI+G5IiTlTNTt+LsDUppl19J/M2Fp9UkH6g==
|
||||
|
||||
invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
@@ -13357,11 +13367,6 @@ url-regex@~4.1.1:
|
||||
ip-regex "^1.0.1"
|
||||
tlds "^1.187.0"
|
||||
|
||||
url-search-params@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/url-search-params/-/url-search-params-0.9.0.tgz#e71d7764a6503533cbfe9771b2963cb61ea1c225"
|
||||
integrity sha1-5x13ZKZQNTPL/pdxspY8th6hwiU=
|
||||
|
||||
url@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||
|
||||
Reference in New Issue
Block a user