mirror of
https://github.com/wassname/talk.git
synced 2026-07-07 19:10:30 +08:00
Merge branch 'karma' of github.com:coralproject/talk into karma
* 'karma' of github.com:coralproject/talk: (32 commits) fixed translations "Trust" flag to "Karma" and updated translations adjusted karma tooltip field renamed patches to reliability computation based on settings add from to import statement Remuve dupe "apply" Fix broken link to tokenusernotfound Make it so low threshold is included in karma calc Fix incorrect statement regarding GDPR simplified language negotiation Docs typo relying on ip req not properly passed German translations for the email notification plugins. Small modification doc patch fix for custom css fixes to csp policy disable by default ...
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
const express = require('express');
|
||||
const nunjucks = require('nunjucks');
|
||||
const cons = require('consolidate');
|
||||
const trace = require('./middleware/trace');
|
||||
const logging = require('./middleware/logging');
|
||||
const path = require('path');
|
||||
@@ -72,7 +74,26 @@ app.use(
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
// configure the default views directory.
|
||||
const views = path.join(__dirname, 'views');
|
||||
app.set('views', views);
|
||||
|
||||
// reconfigure nunjucks.
|
||||
cons.requires.nunjucks = nunjucks.configure(views, {
|
||||
autoescape: true,
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true,
|
||||
watch: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
// assign the nunjucks engine to .njk files.
|
||||
app.engine('njk', cons.nunjucks);
|
||||
|
||||
// assign the ejs engine to .ejs and .html files.
|
||||
app.engine('ejs', cons.ejs);
|
||||
app.engine('html', cons.ejs);
|
||||
|
||||
// set .ejs as the default extension.
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from 'coral-ui/components/Label';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import { t } from 'coral-framework/services/i18n';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
@@ -63,10 +64,14 @@ const CommentLabels = ({
|
||||
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
|
||||
)}
|
||||
{hasSuspectedWords(actions) && (
|
||||
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
|
||||
<FlagLabel iconName="sms_failed">
|
||||
{t('flags.reasons.comment.suspect_word')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
{hasHistoryFlag(actions) && (
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">
|
||||
{t('flags.reasons.comment.trust')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
</div>
|
||||
<Slot
|
||||
|
||||
@@ -54,6 +54,34 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
font-size: .9em;
|
||||
line-height: normal;
|
||||
letter-spacing: .4px;
|
||||
min-width: 25px;
|
||||
display: block;
|
||||
|
||||
/* &.reliable { background-color: #03AB61; } */
|
||||
/* &.neutral { background-color: #616161; } */
|
||||
&.unreliable { background-color: #F44336; }
|
||||
}
|
||||
|
||||
.descriptionList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -72,4 +100,4 @@
|
||||
color: #2B7EB5;
|
||||
text-decoration: underline;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'coral-ui';
|
||||
import styles from './KarmaTooltip.css';
|
||||
@@ -8,6 +9,13 @@ import t from 'coral-framework/services/i18n';
|
||||
const initialState = { menuVisible: false };
|
||||
|
||||
class KarmaTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
settings: PropTypes.shape({
|
||||
reliable: PropTypes.number.isRequired,
|
||||
unreliable: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
state = initialState;
|
||||
|
||||
toogleMenu = () => {
|
||||
@@ -19,6 +27,7 @@ class KarmaTooltip extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings: { unreliable } } = this.props;
|
||||
const { menuVisible } = this.state;
|
||||
|
||||
return (
|
||||
@@ -34,6 +43,26 @@ class KarmaTooltip extends React.Component {
|
||||
{menuVisible && (
|
||||
<div className={cn(styles.menu, 'talk-admin-karma-tooltip-menu')}>
|
||||
<strong>{t('user_detail.user_karma_score')}</strong>
|
||||
<ul>
|
||||
{/* <li>
|
||||
<span>Reliable</span>{' '}
|
||||
<span className={cn(styles.label, styles.reliable)}>
|
||||
≥ {reliable}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Neutral</span>{' '}
|
||||
<span className={cn(styles.label, styles.neutral)}>
|
||||
< {reliable}, > {unreliable}
|
||||
</span>
|
||||
</li> */}
|
||||
<li>
|
||||
<span>{t('user_detail.unreliable')}</span>{' '}
|
||||
<span className={cn(styles.label, styles.unreliable)}>
|
||||
≤ {unreliable}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<a
|
||||
className={styles.link}
|
||||
href={t('user_detail.karma_docs_link')}
|
||||
|
||||
@@ -148,3 +148,7 @@
|
||||
border-color: #E45241;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.userDetailItem {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,7 @@ import styles from './UserDetail.css';
|
||||
import UserHistory from './UserHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
import {
|
||||
getReliability,
|
||||
isSuspended,
|
||||
isBanned,
|
||||
getKarma,
|
||||
} from 'coral-framework/utils/user';
|
||||
import { isSuspended, isBanned, getKarma } from 'coral-framework/utils/user';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import {
|
||||
@@ -81,7 +76,7 @@ class UserDetail extends React.Component {
|
||||
renderLoaded() {
|
||||
const {
|
||||
root,
|
||||
root: { me, user, totalComments, rejectedComments },
|
||||
root: { me, user, totalComments, rejectedComments, settings: { karma } },
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
toggleSelect,
|
||||
@@ -179,7 +174,7 @@ class UserDetail extends React.Component {
|
||||
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.member_since')}:
|
||||
@@ -187,11 +182,24 @@ class UserDetail extends React.Component {
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ id }) => (
|
||||
<li key={id}>
|
||||
<Icon name="email" />
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{user.email}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={user.email}
|
||||
/>
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ provider, id }) => (
|
||||
<li key={id} className={styles.userDetailItem}>
|
||||
<Icon name="device_hub" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
{capitalize(provider)} {t('user_detail.id')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
@@ -218,19 +226,6 @@ class UserDetail extends React.Component {
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reports')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statReportResult,
|
||||
styles[getReliability(user.reliable.flagger)]
|
||||
)}
|
||||
>
|
||||
{capitalize(getReliability(user.reliable.flagger))}
|
||||
</span>
|
||||
</li>
|
||||
<li className={cn(styles.stat, styles.karmaStat)}>
|
||||
<div>
|
||||
<span className={styles.statItem}>
|
||||
@@ -242,10 +237,10 @@ class UserDetail extends React.Component {
|
||||
styles[getKarma(user.reliable.commenter)]
|
||||
)}
|
||||
>
|
||||
{user.reliable.commenterScore}
|
||||
{user.reliable.commenterKarma}
|
||||
</span>
|
||||
</div>
|
||||
<KarmaTooltip />
|
||||
<KarmaTooltip settings={karma.comment} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -179,14 +179,14 @@ export const withUserDetailQuery = withQuery(
|
||||
id
|
||||
username
|
||||
created_at
|
||||
email
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
reliable {
|
||||
flagger
|
||||
commenter
|
||||
commenterScore
|
||||
commenterKarma
|
||||
}
|
||||
state {
|
||||
status {
|
||||
@@ -227,6 +227,14 @@ export const withUserDetailQuery = withQuery(
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
settings {
|
||||
karma {
|
||||
comment {
|
||||
reliable
|
||||
unreliable
|
||||
}
|
||||
}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
|
||||
@@ -24,14 +24,23 @@ const userRoleFragment = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const toBoolean = value => {
|
||||
if (value === 0) {
|
||||
return null;
|
||||
} else if (value > 0) {
|
||||
/**
|
||||
* calculateReliability will determine the reliability of a karma score based on
|
||||
* the settings for the karma type.
|
||||
*
|
||||
* @param {Number} karma - the current karma value/score for the given user
|
||||
* @param {Object} thresholds - the karma thresholds to base the karma computation on
|
||||
*/
|
||||
const calculateReliability = (karma, { reliable, unreliable }) => {
|
||||
if (karma >= reliable) {
|
||||
return true;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (karma <= unreliable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -312,9 +321,12 @@ export default {
|
||||
user: {
|
||||
reliable: {
|
||||
commenter: {
|
||||
$set: toBoolean(prev.user.reliable.commenterScore - 1),
|
||||
$set: calculateReliability(
|
||||
prev.user.reliable.commenterKarma - 1,
|
||||
prev.settings.karma.comment
|
||||
),
|
||||
},
|
||||
commenterScore: {
|
||||
commenterKarma: {
|
||||
$apply: count => count - 1,
|
||||
},
|
||||
},
|
||||
@@ -328,9 +340,12 @@ export default {
|
||||
user: {
|
||||
reliable: {
|
||||
commenter: {
|
||||
$set: toBoolean(prev.user.reliable.commenterScore + 1),
|
||||
$set: calculateReliability(
|
||||
prev.user.reliable.commenterKarma + 1,
|
||||
prev.settings.karma.comment
|
||||
),
|
||||
},
|
||||
commenterScore: {
|
||||
commenterKarma: {
|
||||
$apply: count => count + 1,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -130,7 +130,9 @@ class People extends React.Component {
|
||||
{user.username}
|
||||
</button>
|
||||
<span className={styles.email}>
|
||||
{user.profiles.map(({ id }) => id)}
|
||||
{user.email
|
||||
? user.email
|
||||
: user.profiles.map(p => p.id).join(', ')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import ta from 'timeago.js';
|
||||
import { negotiateLanguages } from 'fluent-langneg/compat';
|
||||
|
||||
import has from 'lodash/has';
|
||||
import get from 'lodash/get';
|
||||
import merge from 'lodash/merge';
|
||||
import first from 'lodash/first';
|
||||
import isUndefined from 'lodash/isUndefined';
|
||||
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/ar';
|
||||
@@ -12,8 +15,8 @@ import 'moment/locale/fr';
|
||||
import 'moment/locale/nl';
|
||||
import 'moment/locale/pt-br';
|
||||
|
||||
import { createStorage } from 'coral-framework/services/storage';
|
||||
|
||||
// timeago
|
||||
import ta from 'timeago.js';
|
||||
import arTA from 'timeago.js/locales/ar';
|
||||
import daTA from 'timeago.js/locales/da';
|
||||
import deTA from 'timeago.js/locales/de';
|
||||
@@ -24,6 +27,7 @@ import pt_BRTA from 'timeago.js/locales/pt_BR';
|
||||
import zh_CNTA from 'timeago.js/locales/zh_CN';
|
||||
import zh_TWTA from 'timeago.js/locales/zh_TW';
|
||||
|
||||
// locales
|
||||
import ar from '../../../locales/ar.yml';
|
||||
import en from '../../../locales/en.yml';
|
||||
import da from '../../../locales/da.yml';
|
||||
@@ -35,8 +39,9 @@ import pt_BR from '../../../locales/pt_BR.yml';
|
||||
import zh_CN from '../../../locales/zh_CN.yml';
|
||||
import zh_TW from '../../../locales/zh_TW.yml';
|
||||
|
||||
const defaultLanguage = process.env.TALK_DEFAULT_LANG;
|
||||
const translations = {
|
||||
export const defaultLocale = process.env.TALK_DEFAULT_LANG.replace(/-/g, '_');
|
||||
|
||||
export const translations = {
|
||||
...ar,
|
||||
...en,
|
||||
...da,
|
||||
@@ -49,84 +54,62 @@ const translations = {
|
||||
...zh_TW,
|
||||
};
|
||||
|
||||
let lang;
|
||||
let timeagoInstance;
|
||||
export const supportedLocales = Object.keys(translations);
|
||||
|
||||
function setLocale(storage, locale) {
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
let LOCALE;
|
||||
let TIMEAGO_INSTANCE;
|
||||
|
||||
// detectLanguage will try to get the locale from storage if available,
|
||||
// otherwise will try to get it from the navigator, otherwise, it will fallback
|
||||
// to the default language.
|
||||
function detectLanguage(storage) {
|
||||
try {
|
||||
const lang = storage.getItem('locale') || navigator.language;
|
||||
if (lang) {
|
||||
return lang;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Error while trying to detect language, will fallback to',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
console.warn('Could not detect language, will fallback to', defaultLanguage);
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
// getLocale will get the users locale from the local detector and parse it to a
|
||||
// format we can work with.
|
||||
function getLocale(storage) {
|
||||
// Get the language from the local detector.
|
||||
const lang = detectLanguage(storage);
|
||||
|
||||
// Some language strings come with additional subtags as defined in:
|
||||
//
|
||||
// https://www.ietf.org/rfc/bcp/bcp47.txt
|
||||
//
|
||||
// So we should strip that off if we find it.
|
||||
return lang.split('-')[0];
|
||||
}
|
||||
const detectLanguage = () =>
|
||||
first(
|
||||
negotiateLanguages(navigator.languages, supportedLocales, {
|
||||
defaultLocale,
|
||||
strategy: 'lookup',
|
||||
})
|
||||
);
|
||||
|
||||
export function setupTranslations() {
|
||||
// Setup the translation framework with the storage.
|
||||
const storage = createStorage('localStorage');
|
||||
// locale
|
||||
LOCALE = detectLanguage();
|
||||
|
||||
const locale = getLocale(storage);
|
||||
setLocale(storage, locale);
|
||||
|
||||
// Setting moment
|
||||
moment.locale(locale);
|
||||
|
||||
// Extract language key.
|
||||
lang = locale.split('-')[0];
|
||||
|
||||
// Check if we have a translation in this language.
|
||||
if (!(lang in translations)) {
|
||||
lang = defaultLanguage;
|
||||
}
|
||||
// moment
|
||||
moment.locale(LOCALE);
|
||||
|
||||
// timeago
|
||||
ta.register('ar', arTA);
|
||||
ta.register('es', esTA);
|
||||
ta.register('da', daTA);
|
||||
ta.register('de', deTA);
|
||||
ta.register('fr', frTA);
|
||||
ta.register('nl_NL', nlTA);
|
||||
ta.register('pt_BR', pt_BRTA);
|
||||
ta.register('zh_CN', zh_CNTA);
|
||||
ta.register('zh_TW', zh_TWTA);
|
||||
|
||||
timeagoInstance = ta();
|
||||
ta.register('nl-NL', nlTA);
|
||||
ta.register('pt-BR', pt_BRTA);
|
||||
ta.register('zh-CN', zh_CNTA);
|
||||
ta.register('zh-TW', zh_TWTA);
|
||||
TIMEAGO_INSTANCE = ta();
|
||||
}
|
||||
|
||||
/**
|
||||
* loadTranslations will load the new language pack into the existing ones.
|
||||
*
|
||||
* @param {Object} newTranslations translation object to merge into the existing
|
||||
* languages.
|
||||
*/
|
||||
export function loadTranslations(newTranslations) {
|
||||
// Merge the new translations into the existing translations.
|
||||
merge(translations, newTranslations);
|
||||
|
||||
// Push new languages into the supportedLocales array.
|
||||
Object.keys(newTranslations).forEach(language => {
|
||||
if (!supportedLocales.includes(language)) {
|
||||
supportedLocales.push(language);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function timeago(time) {
|
||||
return timeagoInstance.format(new Date(time), lang);
|
||||
return TIMEAGO_INSTANCE.format(new Date(time), LOCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,24 +123,24 @@ export function timeago(time) {
|
||||
*/
|
||||
export function t(key, ...replacements) {
|
||||
let translation;
|
||||
if (has(translations[lang], key)) {
|
||||
translation = get(translations[lang], key);
|
||||
if (has(translations[LOCALE], key)) {
|
||||
translation = get(translations[LOCALE], key);
|
||||
} else if (has(translations['en'], key)) {
|
||||
translation = get(translations['en'], key);
|
||||
console.warn(`${lang}.${key} language key not set`);
|
||||
console.warn(`${LOCALE}.${key} language key not set`);
|
||||
}
|
||||
|
||||
if (translation) {
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${lang}.${key} and en.${key} language key not set`);
|
||||
if (!translation) {
|
||||
console.warn(`${LOCALE}.${key} and en.${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// Handle replacements in the translation string.
|
||||
return translation.replace(
|
||||
/{(\d+)}/g,
|
||||
(match, number) =>
|
||||
!isUndefined(replacements[number]) ? replacements[number] : match
|
||||
);
|
||||
}
|
||||
|
||||
export default t;
|
||||
|
||||
@@ -48,6 +48,10 @@ const CONFIG = {
|
||||
// request all of the records. Otherwise, minimum limits of 0 are enforced.
|
||||
ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE',
|
||||
|
||||
// ENABLE_STRICT_CSP enables strict CSP enforcement, and will enforce as well
|
||||
// as report CSP violations.
|
||||
ENABLE_STRICT_CSP: process.env.TALK_ENABLE_STRICT_CSP === 'TRUE',
|
||||
|
||||
// LOGGING_LEVEL specifies the logging level used by the bunyan logger.
|
||||
LOGGING_LEVEL: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes(
|
||||
process.env.TALK_LOGGING_LEVEL
|
||||
|
||||
@@ -497,6 +497,14 @@ tracing of GraphQL requests.
|
||||
|
||||
**Note: Apollo Engine is a premium service, charges may apply.**
|
||||
|
||||
## TALK_ENABLE_STRICT_CSP
|
||||
|
||||
Setting this to `TRUE` will enforce the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|
||||
(or CSP). By default, this configuration is set to
|
||||
[report only](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#Testing_your_policy)
|
||||
where the policy is not enforced, but any violations are reported to a provided
|
||||
URI. (Default `FALSE`)
|
||||
|
||||
## ALLOW_NO_LIMIT_QUERIES
|
||||
|
||||
Setting this to `TRUE` will allow queries to execute without a limit (returns
|
||||
|
||||
@@ -25,7 +25,7 @@ If their next comment is also rejected, their user karma score is now `-2`, and
|
||||
|
||||
We strongly recommend not telling your community how this system works, or where the threshholds lie. Firstly, they might try to game the system to meet approval, and secondly, it makes it harder for you to change the threshhold in the future. We suggest using language such as "We hold back comments for approval for a variety of reasons, including content, account history, and more."
|
||||
|
||||
If you see that a high proportion of first-time commenters on your site are abusive, you might want to change the threshhold to `0`, at least temporarily. You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your site configuration.
|
||||
If you see that a high proportion of first-time commenters on your site are abusive, you might want to change the threshhold to `0`, at least temporarily. You can configure your own Trust thresholds by using [TRUST_THRESHOLDS](/talk/advanced-configuration/#trust-thresholds) in your site configuration.
|
||||
|
||||
|
||||
## Reliable and Unreliable Flaggers
|
||||
@@ -49,7 +49,7 @@ Here are the default thresholds:
|
||||
0 to +1: Neutral
|
||||
+2 and higher: Reliable
|
||||
```
|
||||
You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your
|
||||
You can configure your own Trust thresholds by using [TRUST_THRESHOLDS](/talk/advanced-configuration/#trust-thresholds) in your
|
||||
configuration.
|
||||
|
||||
Note: Report Karma doesn't include reports of "I don't agree with this comment".
|
||||
|
||||
@@ -11,9 +11,9 @@ can enable the following plugins:
|
||||
- [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association
|
||||
- [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion
|
||||
|
||||
Even if you don't reside in a location where GDPR will apply, it is recommended
|
||||
to enable these features as a best practice to provide your users with control over their
|
||||
own data.
|
||||
Even if GDPR will not apply to you, it is recommended to enable these
|
||||
features as a best practice to provide your users with control over their own
|
||||
data.
|
||||
|
||||
## GPDR Feature Overview
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ Coral UI is a set of components to help you build your UI. This powers our core.
|
||||
|
||||
### Import
|
||||
```js
|
||||
import {Button} 'plugin-api/beta/components/ui';
|
||||
import {Button} from 'plugin-api/beta/components/ui';
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
@@ -81,5 +81,5 @@ 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/reference/server/#tokenUserNotFound)
|
||||
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound)
|
||||
reference.
|
||||
|
||||
@@ -15,6 +15,7 @@ const DontAgreeActionSummary = require('./dont_agree_action_summary');
|
||||
const FlagAction = require('./flag_action');
|
||||
const FlagActionSummary = require('./flag_action_summary');
|
||||
const GenericUserError = require('./generic_user_error');
|
||||
const KarmaThreshold = require('./karma_threshold');
|
||||
const LocalUserProfile = require('./local_user_profile');
|
||||
const RootMutation = require('./root_mutation');
|
||||
const RootQuery = require('./root_query');
|
||||
@@ -48,6 +49,7 @@ let resolvers = {
|
||||
FlagAction,
|
||||
FlagActionSummary,
|
||||
GenericUserError,
|
||||
KarmaThreshold,
|
||||
LocalUserProfile,
|
||||
RootMutation,
|
||||
RootQuery,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
const { property } = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
reliable: property('RELIABLE'),
|
||||
unreliable: property('UNRELIABLE'),
|
||||
};
|
||||
@@ -1,8 +1,13 @@
|
||||
const { VIEW_PROTECTED_SETTINGS } = require('../../perms/constants');
|
||||
|
||||
const { decorateWithPermissionCheck } = require('./util');
|
||||
|
||||
const Settings = {};
|
||||
const Settings = {
|
||||
karma: (
|
||||
settings,
|
||||
args,
|
||||
{ connectors: { services: { Karma: { THRESHOLDS } } } }
|
||||
) => THRESHOLDS,
|
||||
};
|
||||
|
||||
// PROTECTED_SETTINGS are the settings keys that must be protected for only some
|
||||
// eyes.
|
||||
@@ -11,6 +16,7 @@ const PROTECTED_SETTINGS = {
|
||||
autoCloseStream: [VIEW_PROTECTED_SETTINGS],
|
||||
wordlist: [VIEW_PROTECTED_SETTINGS],
|
||||
domains: [VIEW_PROTECTED_SETTINGS],
|
||||
karma: [VIEW_PROTECTED_SETTINGS],
|
||||
};
|
||||
|
||||
// decorate the fields on the settings resolver with a permission check.
|
||||
|
||||
+30
-4
@@ -20,17 +20,17 @@ type Reliability {
|
||||
# `null` if the reliability cannot be determined.
|
||||
flagger: Boolean
|
||||
|
||||
# flaggerScore will contains the number of agreed flags vs disagred flag
|
||||
# flaggerKarma will contains the number of agreed flags vs disagred flag
|
||||
# count.
|
||||
flaggerScore: Int!
|
||||
flaggerKarma: Int!
|
||||
|
||||
# Commenter will be `true` when the commenter is reliable, `false` if not, or
|
||||
# `null` if the reliability cannot be determined.
|
||||
commenter: Boolean
|
||||
|
||||
# commenterScore the number of approved comments (not untouched) subtracted by
|
||||
# commenterKarma the number of approved comments (not untouched) subtracted by
|
||||
# the number of rejected comments.
|
||||
commenterScore: Int!
|
||||
commenterKarma: Int!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -801,6 +801,29 @@ type Domains {
|
||||
whitelist: [String!]!
|
||||
}
|
||||
|
||||
# KarmaThreshold defines the bounds for which a User will become unreliable or
|
||||
# reliable based on their karma score. If the score is equal or less than the
|
||||
# unreliable value, they are unreliable. If the score is equal or more than the
|
||||
# reliable value, they are reliable. If they are neither reliable or unreliable
|
||||
# then they are neutral.
|
||||
type KarmaThreshold {
|
||||
reliable: Int!
|
||||
unreliable: Int!
|
||||
}
|
||||
|
||||
# KarmaThresholds contains the currently set thresholds for triggering Trust
|
||||
# beheviour.
|
||||
type KarmaThresholds {
|
||||
|
||||
# flag represents karma settings in relation to how well a User's flagging
|
||||
# ability aligns with the moderation decicions made by moderators.
|
||||
flag: KarmaThreshold!
|
||||
|
||||
# comment represents the karma setting in relation to how well a User's
|
||||
# comments are moderated.
|
||||
comment: KarmaThreshold!
|
||||
}
|
||||
|
||||
# Settings stores the global settings for a given installation.
|
||||
type Settings {
|
||||
|
||||
@@ -873,6 +896,9 @@ type Settings {
|
||||
|
||||
# domains will return a given list of domains.
|
||||
domains: Domains
|
||||
|
||||
# karma contains the currently set thresholds for triggering Trust beheviour.
|
||||
karma: KarmaThresholds
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
+1
-148
@@ -292,55 +292,7 @@ ar:
|
||||
suspect_word: "كلمة مشتبهة"
|
||||
banned_word: "كلمة محظورة"
|
||||
body_count: "يتجاوز النص الحد الأقصى للطول المسموح"
|
||||
trust: "ثقة"
|
||||
links: "رابط"
|
||||
modqueue:
|
||||
account: "account flags"
|
||||
actions: Actions
|
||||
all: all
|
||||
all_streams: "All Streams"
|
||||
notify_edited: '{0} edited comment "{1}"'
|
||||
notify_accepted: '{0} accepted comment "{1}"'
|
||||
notify_rejected: '{0} rejected comment "{1}"'
|
||||
notify_flagged: '{0} flagged comment "{1}"'
|
||||
notify_reset: '{0} reset status of comment "{1}"'
|
||||
approve: "Approve"
|
||||
approved: "Approved"
|
||||
ban_user: "Ban"
|
||||
billion: B
|
||||
close: Close
|
||||
empty_queue: "No more comments to moderate! You're all caught up. Go have some ☕️"
|
||||
flagged: flagged
|
||||
reported: reported
|
||||
less_detail: "Less detail"
|
||||
likes: likes
|
||||
million: M
|
||||
mod_faster: "Moderate faster with keyboard shortcuts"
|
||||
moderate: "Moderate →"
|
||||
more_detail: "More detail"
|
||||
new: New
|
||||
newest_first: "Newest First"
|
||||
navigation: Navigation
|
||||
next_comment: "Go to the next comment"
|
||||
toggle_search: "Open search"
|
||||
next_queue: "Switch queues"
|
||||
oldest_first: "Oldest First"
|
||||
premod: pre-mod
|
||||
prev_comment: "Go to the previous comment"
|
||||
reject: "Reject"
|
||||
rejected: "Rejected"
|
||||
reply: "Reply"
|
||||
select_stream: "Select Stream"
|
||||
shift_key: "⇧"
|
||||
shortcuts: "Shortcuts"
|
||||
sort: "Sort"
|
||||
show_shortcuts: "Show Shortcuts"
|
||||
singleview: "Zen mode"
|
||||
thismenu: "Open this menu"
|
||||
jump_to_queue: "Jump to specific queue"
|
||||
thousand: k
|
||||
try_these: "Try these"
|
||||
view_more_shortcuts: "View more shortcuts"
|
||||
my_comment_history: "سجل التعليقات"
|
||||
name: اسم
|
||||
no_agree_comment: "لا أوافق على هذا التعليق"
|
||||
@@ -358,9 +310,6 @@ ar:
|
||||
report_notif: "شكرا على الإبلاغ عن هذا التعليق. تم إبلاغ فريق الإشراف لدينا وسيراجعه قريبًا."
|
||||
report_notif_remove: "لقد تمت إزالة بلاغك."
|
||||
reported: بلغ عنه
|
||||
comment_history_blank:
|
||||
title: You have not written any comments
|
||||
info: A history of your comments will appear here
|
||||
settings:
|
||||
from_settings_page: "من صفحة الملف الشخصي يمكنك مشاهدة سجل التعليقات."
|
||||
my_comment_history: "سجل التعليقات"
|
||||
@@ -378,104 +327,8 @@ ar:
|
||||
step_1_header: "بلغ عن مشكلة"
|
||||
step_2_header: "ساعدنا على الفهم"
|
||||
step_3_header: "شكرا لك على المساهمة الخاصة بك"
|
||||
streams:
|
||||
all: All
|
||||
article: Story
|
||||
closed: Closed
|
||||
empty_result: "No assets match this search. Maybe try widening your search?"
|
||||
filter_streams: "Filter Streams"
|
||||
newest: Newest
|
||||
oldest: Oldest
|
||||
open: Open
|
||||
pubdate: "Publication Date"
|
||||
search: Search
|
||||
sort_by: "Sort By"
|
||||
status: "Stream Status"
|
||||
stream_status: "Stream Status"
|
||||
suspenduser:
|
||||
title_suspend: "Suspend User"
|
||||
description_suspend: "You are suspending {0}. This comment will go to the Rejected queue, and {0} will not be allowed to like, report, reply or post until the suspension time is complete."
|
||||
select_duration: "Select suspension duration"
|
||||
one_hour: "1 hour"
|
||||
hours: "{0} hours"
|
||||
days: "{0} days"
|
||||
hour: "{0} hours"
|
||||
day: "{0} days"
|
||||
cancel: "Cancel"
|
||||
suspend_user: "Suspend User"
|
||||
email_message_suspend: "Dear {0},\n\nIn accordance with {1}’s community guidelines, your account has been temporarily suspended. During the suspension, you will be unable to comment, flag or engage with fellow commenters. Please rejoin the conversation {2}."
|
||||
title_notify: "Notify the user of their temporary suspension"
|
||||
notify_suspend_until: "User {0} has been temporarily suspended. This suspension will automatically end {1}."
|
||||
description_notify: "Suspending this user will temporarily disable their account."
|
||||
write_message: "Write a message"
|
||||
send: Send
|
||||
reject_username:
|
||||
username: username
|
||||
no_cancel: "No cancel"
|
||||
description_reject: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily suspend this user until they rewrite their {0}."
|
||||
title_notify: "Notify the user of their temporary suspension"
|
||||
description_notify: "Suspending this user will temporarily disable their account."
|
||||
title_reject: "We noticed you rejected a username"
|
||||
suspend_user: "Suspend User"
|
||||
yes_suspend: "Yes suspend"
|
||||
email_message_reject: "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please e-mail us if you have any questions or concerns."
|
||||
write_message: "Write a message"
|
||||
send: Send
|
||||
thank_you: "نحن نقدر سلامتك وردود الفعل. سيراجع المشرف التقرير الخاص بك"
|
||||
user:
|
||||
bio_flags: "flags for this bio"
|
||||
user_bio: "User Bio"
|
||||
username_flags: "flags for this username"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
suspended: "Suspended, {0}"
|
||||
suspension_removed: "Suspension removed"
|
||||
system: "System"
|
||||
date: "Date"
|
||||
action: "Action"
|
||||
taken_by: "Taken By"
|
||||
user_impersonating: "هذا المستخدم ينتحل شخصية"
|
||||
user_no_comment: "لم تترك تعليقا مطلقا. إنضم إلى المحادثة!"
|
||||
username_offensive: "اسم المستخدم هذا مسيء"
|
||||
view_conversation: "عرض المحادثة"
|
||||
install:
|
||||
initial:
|
||||
description: "Let's set up your Talk community in just a few short steps."
|
||||
submit: "Get Started"
|
||||
add_organization:
|
||||
description: "Please tell us the name of your organization. This will appear in emails when inviting new team members."
|
||||
label: "Organization Name"
|
||||
save: "Save"
|
||||
create:
|
||||
email: "Email address"
|
||||
username: "Username"
|
||||
password: "Password"
|
||||
confirm_password: "Confirm Password"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
save: "Save"
|
||||
permitted_domains:
|
||||
title: "Permitted domains"
|
||||
description: "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)."
|
||||
submit: "Finish install"
|
||||
final:
|
||||
description: "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now."
|
||||
launch: "Launch Talk"
|
||||
close: "Close this Installer"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
view_conversation: "عرض المحادثة"
|
||||
@@ -211,7 +211,6 @@ da:
|
||||
NO_SPECIAL_CHARACTERS: "Brugernavne kan kun indeholder bogstaver og _"
|
||||
PASSWORD_LENGTH: "Adgangskoden er for kort"
|
||||
PROFANITY_ERROR: "Brugernavne må ikke inholde stødende indhold. Kontakt venligst administratoren, hvis du mener at dette er en fejl."
|
||||
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
|
||||
USERNAME_IN_USE: "Brugernavnet er allerede i brug"
|
||||
USERNAME_REQUIRED: "Du skal indtaste et brugernavn"
|
||||
EMAIL_NOT_VERIFIED: "E-mail address not verified"
|
||||
@@ -287,10 +286,8 @@ da:
|
||||
comment_spam: "Spam"
|
||||
comment_noagree: "Uenig"
|
||||
comment_other: "Andre"
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Forbudt ord"
|
||||
body_count: "Body overstiger max længde"
|
||||
trust: "Stol"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "konto flag"
|
||||
|
||||
+48
-13
@@ -20,10 +20,13 @@ de:
|
||||
bio_offensive: "Diese Biographie ist unangemessen"
|
||||
cancel: "Abbrechen"
|
||||
confirm_email:
|
||||
email_confirmation: "E-Mail-Bestätigung"
|
||||
click_to_confirm: "Unten klicken, um E-Mail-Adresse zu bestätigen"
|
||||
confirm: "Bestätigen"
|
||||
password_reset:
|
||||
mail_sent: 'Falls Sie eine registriertes Konto haben, wurde Ihnen ein Zurücksetzen-Link an diese E-Mail-Adresse geschickt'
|
||||
set_new_password: "Passwort ändern"
|
||||
change_password_help: "Bitte geben Sie ein neues Passwort ein. Benutzen Sie ein sicheres!"
|
||||
new_password: "Neues Passwort"
|
||||
new_password_help: "Das Passwort benötigt mindestens 8 Zeichen"
|
||||
confirm_new_password: "Neues Passwort bestätigen"
|
||||
@@ -120,11 +123,12 @@ de:
|
||||
custom_css_url: "Benutzerdefinierte CSS-URL"
|
||||
custom_css_url_desc: "URL eines CSS-Stylesheets zum Überschreiben des Standard-Designs"
|
||||
days: Tage
|
||||
description: "Als Administrator können Sie die Einstellungen für den Kommentarbereich dieses Artikels anpassen:"
|
||||
description: "Ändern Sie die Einstellungen für den Kommentarbereich dieses Artikels."
|
||||
disable_commenting_title: "Kommentieren global deaktivieren"
|
||||
disable_commenting_desc: "Verfassen Sie eine Nachricht, die angezeigt wird, solange das Kommentieren deaktiviert ist."
|
||||
domain_list_text: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll, z.B. für lokale Test- oder Produktionsumgebungen (Bsp.: localhost:3000 staging.domain.com domain.com)."
|
||||
domain_list_title: "Zugelassene Domains"
|
||||
edit_info: "Information bearbeiten"
|
||||
edit_comment_timeframe_heading: "Zeitlimit zur Bearbeitung von Kommentaren"
|
||||
edit_comment_timeframe_text_pre: "Kommentatoren haben"
|
||||
edit_comment_timeframe_text_post: "Sekunden Zeit, um ihre Kommentare zu bearbeiten."
|
||||
@@ -150,17 +154,31 @@ de:
|
||||
open_stream_configuration: "Dieser Kommentarbereich ist momentan geöffnet. Nach dem Schließen dieses Kommentarbereich wird es nicht mehr möglich sein, zu kommentieren. Bestehende Kommentare bleiben sichtbar."
|
||||
require_email_verification: "E-Mail-Bestätigung erforderlich"
|
||||
require_email_verification_text: "Neue Nutzer müssen ihre E-Mail-Adresse bestätigen."
|
||||
save: "Speichern"
|
||||
save_changes: "Änderungen speichern"
|
||||
shortcuts: Tastaturkürzel
|
||||
sign_out: "Abmelden"
|
||||
stories: Artikel
|
||||
stream_settings: "Einstellungen Kommentarbereich"
|
||||
access_message: "Sie müssen Administrator sein, um auf die Einstellungen zuzugreifen. Fragen Sie ggf. einen Administrator, der Ihnen mehr Recht zuweisen kann!"
|
||||
suspect_word_title: "Liste verdächtiger Wörter"
|
||||
suspect_word_text: "Kommentare, die diese Wörter oder Phrasen enthalten (unabhängig von Groß-/Kleinschreibung), werden im Kommentarbereich markiert. Geben Sie ein Wort ein und bestätigen Sie mit Eingabetaste oder Tab. Es ist auch möglich, einen komma-separierten Text einzufügen."
|
||||
tech_settings: "Technische Einstellungen"
|
||||
organization_information: "Über die Organisation"
|
||||
organization_info_copy: "Wir verwenden diese Informationen in automatisierten E-Mail-Benachrichtigungen, die Talk versendet. Damit können Nutzer Ihre Organisation identifizieren und sie haben die Möglichkeit bei Problemen in Kontakt mit Ihnen zu treten."
|
||||
organization_info_copy_2: "Wir empfehlen, einee generische E-Mail-Adresse (z.B. community@yournewsroom.com) für diesen Zweck einzurichten. Die kann über die Zeit gleich bleiben, und gibt nach außen keine Namen preis, die von Nutzern im Fall von Konflikten für persönliche Angriffe missbraucht werden könnten."
|
||||
organization_details: "Details zur Organisation"
|
||||
organization_name: "Name der Organisation"
|
||||
organization_contact_email: "E-Mail-Adresse der Organisation"
|
||||
title: "Kommentarbereich konfigurieren"
|
||||
weeks: Wochen
|
||||
wordlist: "Gesperrte Wörter"
|
||||
save_changes_dialog:
|
||||
unsaved_changes: "Ungespeicherte Änderungen"
|
||||
copy: "Sie haben einen oder mehrere Änderungen vorgenommen, ohne zu speichern. Möchten Sie jetzt speichern oder die Änderungen verwerfen?"
|
||||
save_settings: "Einstellungen speichern"
|
||||
discard: "Verwerfen"
|
||||
cancel: "Abbrechen"
|
||||
continue: "Fortfahren"
|
||||
createdisplay:
|
||||
check_the_form: "Ungültige Eingabe. Bitte prüfen Sie die Felder."
|
||||
@@ -202,11 +220,17 @@ de:
|
||||
we_received_a_request: "Wir haben eine Anfrage erhalten, Ihr Passwort zurückzusetzen. Sollten Sie dies nicht angefordert haben, können Sie diese Nachricht ignorieren."
|
||||
if_you_did: "Falls doch,"
|
||||
please_click: "klicken Sie bitte hier zum Zurücksetzen"
|
||||
subject: "Passwort zurücksetzen"
|
||||
password_change:
|
||||
subject: "{0} Passwort-Änderung"
|
||||
body: "Das Passwort Ihres Benutzerkontos wurde geändert.\n\nFalls Sie diese Änderung nicht angefordert haben, kontaktieren Sie uns bitte unter {0}."
|
||||
embedlink:
|
||||
copy: "In die Zwischenablage kopieren"
|
||||
error:
|
||||
PASSWORD_INCORRECT: "Ihr bestehendes Passwort wurde falsch eingegeben"
|
||||
COMMENT_PARENT_NOT_VISIBLE: "Der Kommentar, auf den Sie antworten möchten, wurde entfernt oder existiert nicht."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Code zur E-Mail-Bestätigung ist ungültig."
|
||||
EMAIL_ALREADY_VERIFIED: "E-Mail-Adresse ist bereits bestätigt."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "Ihr Link zum Passwort zurücksetzen ist ungültig."
|
||||
COMMENT_TOO_SHORT: "Kommentare sollten mehr als ein Zeichen enthalten, bitte überprüfen Sie Ihren Kommentar und probieren Sie es erneut."
|
||||
NOT_AUTHORIZED: "Sie sind nicht berechtigt, diese Aktion auszuführen."
|
||||
@@ -230,15 +254,20 @@ de:
|
||||
ALREADY_EXISTS: "Ressource existiert bereits"
|
||||
INVALID_ASSET_URL: "Asset-URL ist ungültig"
|
||||
CANNOT_IGNORE_STAFF: "Mitarbeiter können nicht ignoriert werden."
|
||||
email: "E-Mail-Adresse ungültig"
|
||||
INCORRECT_PASSWORD: "Falsches Passwort"
|
||||
email: "Bitte geben Sie eine gültige E-Mail-Adresse ein."
|
||||
DELETION_NOT_SCHEDULED: "Löschvorgang wurde nicht geplant"
|
||||
confirm_password: "Passwörter nicht identisch. Bitte erneut überprüfen"
|
||||
network_error: "Server-Verbindung fehlgeschlagen. Bitte überprüfen Sie ihre Internetverbindung und versuchen Sie es erneut."
|
||||
email_not_verified: "E-Mail-Adresse {0} nicht bestätigt."
|
||||
email_password: "E-Mail und/oder Passwort inkorrekt."
|
||||
organization_name: "Namen von Organisationen dürfen nur Buchstaben und Zahlen enthalten."
|
||||
organization_contact_email: "E-Mail-Adresse der Organisation ist ungültig."
|
||||
password: "Passwort muss mindestens 8 Zeichen enthalten"
|
||||
username: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
|
||||
unexpected: "Unerwarteter Fehler aufgetreten. Es tut uns leid!"
|
||||
required_field: "Dieses Feld ist erforderlich"
|
||||
temporarily_suspended: "Ihr Konto ist vorübergehend gesperrt. Es wird wieder aktiviert am {0}. Bei Fragen setzen Sie sich mit uns in Kontakt."
|
||||
flag_comment: "Kommentar melden"
|
||||
flag_reason: "Grund der Meldung (optional)"
|
||||
flag_username: "Nutzername melden"
|
||||
@@ -248,6 +277,7 @@ de:
|
||||
comment: Kommentar
|
||||
comment_is_ignored: "Dieser Kommentar ist nicht sichtbar, da Sie den Nutzer ignorieren."
|
||||
comment_is_rejected: "Sie haben diesen Kommentar abgelehnt."
|
||||
comment_is_deleted: "Der Kommentar wurde vom Nutzer gelöscht."
|
||||
comment_is_hidden: "Dieser Kommentar ist nicht verfügbar."
|
||||
comments: Kommentare
|
||||
configure_stream: "Konfigurieren"
|
||||
@@ -292,7 +322,6 @@ de:
|
||||
suspect_word: "Verdächtiges Wort"
|
||||
banned_word: "Unzulässiges Wort"
|
||||
body_count: "Text überschreitet Zeichenlimit"
|
||||
trust: "Vertrauen"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "Konto-Markierungen"
|
||||
@@ -336,6 +365,7 @@ de:
|
||||
sort: "Sortieren"
|
||||
show_shortcuts: "Tastaturkürzel anzeigen"
|
||||
singleview: "Zen-Modus"
|
||||
system_withheld: "System Withheld"
|
||||
thismenu: "Dieses Menü öffnen"
|
||||
jump_to_queue: "Zu bestimmter Liste springen"
|
||||
thousand: T
|
||||
@@ -358,6 +388,9 @@ de:
|
||||
report_notif: "Vielen Dank für Ihre Meldung. Unsere Moderatoren wurden informiert und werden sich in Kürze darum kümmern."
|
||||
report_notif_remove: "Ihre Meldung wurde entfernt."
|
||||
reported: Gemeldet
|
||||
comment_history_blank:
|
||||
title: Sie haben noch keine Kommentare verfasst
|
||||
info: Hier wird ein Verlauf Ihrer verfassten Kommentare erscheinen
|
||||
settings:
|
||||
from_settings_page: "Sie können auf Ihrer Profilseite Ihren Kommentarverlauf einsehen."
|
||||
my_comment_history: "Mein Kommentarverlauf"
|
||||
@@ -369,7 +402,7 @@ de:
|
||||
stream:
|
||||
all_comments: "Alle Kommentare"
|
||||
temporarily_suspended: "Entsprechend der Community-Regeln von {0} wurde Ihr Konto vorübergehend gesperrt. Nehmen Sie {1} wieder an der Diskussion teil."
|
||||
comment_not_found: "Kommentar nicht gefunden"
|
||||
comment_not_found: "Dieser Kommentar wurde entfernt oder existiert nicht."
|
||||
no_comments: "Es gibt noch keine Kommentare. Schreiben Sie doch einen..."
|
||||
no_comments_and_closed: "Es gab zu diesem Artikel keine Kommentare."
|
||||
step_1_header: "Ein Problem melden"
|
||||
@@ -396,6 +429,8 @@ de:
|
||||
one_hour: "1 Stunde"
|
||||
hours: "{0} Stunden"
|
||||
days: "{0} Tage"
|
||||
hour: "{0} hours"
|
||||
day: "{0} days"
|
||||
cancel: "Abbrechen"
|
||||
suspend_user: "Nutzer vorübergehend sperren"
|
||||
email_message_suspend: "Sehr geehrte/r {0}, entsprechend der Community-Richtlinien von {1} wurde Ihr Konto vorübergehend gesperrt. Während der Sperrung können Sie weder kommentieren noch andere Aktionen ausführen. Nehmen Sie {2} wieder an der Diskussion teil."
|
||||
@@ -435,15 +470,15 @@ de:
|
||||
rejected: "Abgelehnte"
|
||||
user_history: "Konto-Verlauf"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
suspended: "Suspended, {0}"
|
||||
suspension_removed: "Suspension removed"
|
||||
user_banned: "Nutzer gesperrt"
|
||||
ban_removed: "Sperrung aufgehoben"
|
||||
username_status: "Nutzername {0}"
|
||||
suspended: "Vorübergehend gesperrt, {0}"
|
||||
suspension_removed: "Vorübergehende Sperrung aufgehoben"
|
||||
system: "System"
|
||||
date: "Date"
|
||||
action: "Action"
|
||||
taken_by: "Taken By"
|
||||
date: "Datum"
|
||||
action: "Aktion"
|
||||
taken_by: "Durch"
|
||||
user_impersonating: "Gibt sich für jemand anderen aus"
|
||||
user_no_comment: "Sie haben noch keinen Kommentar abgegeben. Teilen Sie Ihre Meinung mit uns!"
|
||||
username_offensive: "Dieser Nutzername ist unangemessen"
|
||||
@@ -461,7 +496,7 @@ de:
|
||||
username: "Nutzername"
|
||||
password: "Passwort"
|
||||
confirm_password: "Passwort bestätigen"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
organization_contact_email: "Kontakt-Adresse der Organisation"
|
||||
save: "Speichern"
|
||||
permitted_domains:
|
||||
title: "Zugelassene Domains"
|
||||
|
||||
+3
-2
@@ -322,7 +322,7 @@ en:
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Banned Word"
|
||||
body_count: "Body exceeds max length"
|
||||
trust: "Trust"
|
||||
trust: "Karma"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "account flags"
|
||||
@@ -466,14 +466,15 @@ en:
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
user_history: "User History"
|
||||
unreliable: "Unreliable"
|
||||
karma: "Karma"
|
||||
learn_more: "Learn More"
|
||||
user_karma_score: "User Karma Score"
|
||||
karma_docs_link: "https://docs.coralproject.net/talk/trust/#user-karma-score"
|
||||
id: "ID"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
|
||||
@@ -310,7 +310,6 @@ es:
|
||||
suspect_word: "Palabra sospechosa"
|
||||
banned_word: "Palabra prohibida"
|
||||
body_count: "El texto exede el límite permitido"
|
||||
trust: "Trust"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "reportes de cuentas"
|
||||
|
||||
+2
-3
@@ -292,7 +292,6 @@ fi_FI:
|
||||
suspect_word: "Epäilyttävä sana"
|
||||
banned_word: "Kielletty sana"
|
||||
body_count: "Liian pitkä viesti"
|
||||
trust: "Luotettava"
|
||||
links: "Linkki"
|
||||
modqueue:
|
||||
account: "Liputuksia"
|
||||
@@ -413,7 +412,7 @@ fi_FI:
|
||||
title_reject: "Huomasimme sinun hylänneen käyttäjänimen"
|
||||
suspend_user: "Aseta väliaikainen käyttökielto"
|
||||
yes_suspend: "Kyllä, sulje väliaikaisesti"
|
||||
email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää."
|
||||
email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää."
|
||||
write_message: "Kirjoita viesti"
|
||||
send: Lähetä
|
||||
thank_you: "Arvostamme palautettasi. Moderaattorimme käy läpi tekemäsi ilmiannon."
|
||||
@@ -462,4 +461,4 @@ fi_FI:
|
||||
close: "Sulje asennusnäkymä"
|
||||
admin_sidebar:
|
||||
view_options: "Näytä asetukset"
|
||||
sort_comments: "Järjestä kommentit"
|
||||
sort_comments: "Järjestä kommentit"
|
||||
|
||||
@@ -300,7 +300,6 @@ fr:
|
||||
suspect_word: "Mot suspect"
|
||||
banned_word: "Mot banni"
|
||||
body_count: "Le texte dépasse la longueur maximale"
|
||||
trust: "Trust"
|
||||
links: "Lien"
|
||||
modqueue:
|
||||
account: "Signalements du compte"
|
||||
|
||||
@@ -290,7 +290,6 @@ nl_NL:
|
||||
suspect_word: "Verdacht woord"
|
||||
banned_word: "Geblokeerd woord"
|
||||
body_count: "Tekst is te lang"
|
||||
trust: "Vertrouwen"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "account meldingen"
|
||||
|
||||
@@ -61,11 +61,6 @@ pt_BR:
|
||||
reaction: 'Reação'
|
||||
reactions: 'Reações'
|
||||
story: 'Conversas'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} approved username {1}'
|
||||
notify_rejected: '{0} rejected username {1}'
|
||||
notify_flagged: '{0} reported username {1}'
|
||||
notify_changed: 'user {0} changed their username to {1}'
|
||||
community:
|
||||
account_creation_date: "Data de criação da conta"
|
||||
active: Ativo
|
||||
@@ -273,24 +268,6 @@ pt_BR:
|
||||
loading_results: "Carregando resultados"
|
||||
marketing: "Isso parece um anúncio/marketing"
|
||||
moderate_this_stream: "Moderar comentários"
|
||||
flags:
|
||||
reasons:
|
||||
user:
|
||||
username_offensive: "Offensive"
|
||||
username_nolike: "Dislike"
|
||||
username_impersonating: "Impersonation"
|
||||
username_spam: "Spam"
|
||||
username_other: "Other"
|
||||
comment:
|
||||
comment_offensive: "Offensive"
|
||||
comment_spam: "Spam"
|
||||
comment_noagree: "Disagree"
|
||||
comment_other: "Other"
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Banned Word"
|
||||
body_count: "Body exceeds max length"
|
||||
trust: "Trust"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "contas marcadas"
|
||||
actions: Ações
|
||||
@@ -418,29 +395,6 @@ pt_BR:
|
||||
bio_flags: "Marcadas para este perfil"
|
||||
user_bio: "Perfil do usuário"
|
||||
username_flags: "Marcadas para este usuário"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
suspended: "Suspended, {0}"
|
||||
suspension_removed: "Suspension removed"
|
||||
system: "System"
|
||||
date: "Date"
|
||||
action: "Action"
|
||||
taken_by: "Taken By"
|
||||
user_impersonating: "Este usuário está representando"
|
||||
user_no_comment: "Você nunca deixou um comentário. Participe da conversa!"
|
||||
username_offensive: "Esse nome de usuário é ofensivo"
|
||||
@@ -468,6 +422,3 @@ pt_BR:
|
||||
description: "Obrigado por instalar o Talk! Enviamos um e-mail para verificar seu endereço de e-mail. Enquanto você terminar de configurar a conta, você pode começar a se envolver com seus leitores agora."
|
||||
launch: "Iniciar Talk"
|
||||
close: "Feche este instalador"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
|
||||
+1
-67
@@ -12,22 +12,11 @@ zh_CN:
|
||||
note_reject_comment: "封禁该用户将使这条评论被移入“被拒”队列。"
|
||||
note_ban_user: "封禁该用户将使其无法编辑或删除评论。"
|
||||
yes_ban_user: "是的,封禁该用户"
|
||||
write_a_message: "Write a message"
|
||||
send: "Send"
|
||||
notify_ban_headline: "Notify the user of ban"
|
||||
notify_ban_description: "This will notify the user by email that they have been banned from the community"
|
||||
email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team."
|
||||
bio_offensive: "该简介含有冒犯言语"
|
||||
cancel: "取消"
|
||||
confirm_email:
|
||||
click_to_confirm: "Click below to confirm your email address"
|
||||
confirm: "Confirm"
|
||||
password_reset:
|
||||
set_new_password: "Change Your Password"
|
||||
new_password: "New Password"
|
||||
new_password_help: "Password must be at least 8 characters"
|
||||
confirm_new_password: "Confirm New Password"
|
||||
change_password: "Change Password"
|
||||
characters_remaining: "字符剩余可用"
|
||||
comment:
|
||||
anon: "匿名"
|
||||
@@ -61,11 +50,6 @@ zh_CN:
|
||||
reaction: '回应'
|
||||
reactions: '回应'
|
||||
story: '文章'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} approved username {1}'
|
||||
notify_rejected: '{0} rejected username {1}'
|
||||
notify_flagged: '{0} reported username {1}'
|
||||
notify_changed: 'user {0} changed their username to {1}'
|
||||
community:
|
||||
account_creation_date: "账户创建日期"
|
||||
active: "活动中"
|
||||
@@ -203,9 +187,6 @@ zh_CN:
|
||||
embedlink:
|
||||
copy: "复制到粘贴板"
|
||||
error:
|
||||
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
|
||||
COMMENT_TOO_SHORT: "评论至少应有一个字符。请修改您的评论,再度尝试。"
|
||||
NOT_AUTHORIZED: "您没有权限进行该操作"
|
||||
NO_SPECIAL_CHARACTERS: "用户名只能包含字母、数字跟下划线"
|
||||
@@ -237,7 +218,6 @@ zh_CN:
|
||||
username: "用户名只能包含字母、数字跟下划线"
|
||||
unexpected: "发生了异常错误。对不起!"
|
||||
required_field: "该字段必填"
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "举报评论"
|
||||
flag_reason: "举报理由(可选)"
|
||||
flag_username: "举报用户名"
|
||||
@@ -256,8 +236,6 @@ zh_CN:
|
||||
error: "用户名只能包含字母、数字跟下划线"
|
||||
label: "新用户名"
|
||||
msg: "由于您的用户名不当,您的帐号目前被暂停使用。如要恢复您的帐户,请输入一个新的用户名。如有任何疑问,请与我们联系。"
|
||||
changed_name:
|
||||
msg: "Your username change is under review by our moderation team."
|
||||
my_comments: "我的评论"
|
||||
my_profile: "我的资料"
|
||||
new_count: "查看 {0} 更多 {1}"
|
||||
@@ -275,24 +253,6 @@ zh_CN:
|
||||
loading_results: "加载结果中"
|
||||
marketing: "这看起来像是广告"
|
||||
moderate_this_stream: "审查该流"
|
||||
flags:
|
||||
reasons:
|
||||
user:
|
||||
username_offensive: "Offensive"
|
||||
username_nolike: "Dislike"
|
||||
username_impersonating: "Impersonation"
|
||||
username_spam: "Spam"
|
||||
username_other: "Other"
|
||||
comment:
|
||||
comment_offensive: "Offensive"
|
||||
comment_spam: "Spam"
|
||||
comment_noagree: "Disagree"
|
||||
comment_other: "Other"
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Banned Word"
|
||||
body_count: "Body exceeds max length"
|
||||
trust: "Trust"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "帐户标记"
|
||||
actions: "操作"
|
||||
@@ -420,29 +380,6 @@ zh_CN:
|
||||
bio_flags: "对简介的举报"
|
||||
user_bio: "用户简介"
|
||||
username_flags: "对用户名的举报"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
suspended: "Suspended, {0}"
|
||||
suspension_removed: "Suspension removed"
|
||||
system: "System"
|
||||
date: "Date"
|
||||
action: "Action"
|
||||
taken_by: "Taken By"
|
||||
user_impersonating: "冒名用户"
|
||||
user_no_comment: "您未曾发表评论。现在就来加入对话吧!"
|
||||
username_offensive: "用户名有冒犯性"
|
||||
@@ -468,7 +405,4 @@ zh_CN:
|
||||
final:
|
||||
description: "感谢您安装 Talk!我们已向您的邮箱发送一封验证邮件。当您进行帐号设置时,您可以开始跟您的读者开始互动。"
|
||||
launch: "启动 Talk"
|
||||
close: "关闭安装程序"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
close: "关闭安装程序"
|
||||
@@ -12,22 +12,8 @@ zh_TW:
|
||||
note_reject_comment: "封禁該用戶將使這條評論被移入“被拒”列表。"
|
||||
note_ban_user: "封禁該用戶將使其無法編輯或刪除評論。"
|
||||
yes_ban_user: "是的,封禁該用戶"
|
||||
write_a_message: "Write a message"
|
||||
send: "Send"
|
||||
notify_ban_headline: "Notify the user of ban"
|
||||
notify_ban_description: "This will notify the user by email that they have been banned from the community"
|
||||
email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team."
|
||||
bio_offensive: "該介紹包含具有攻擊性的內容。"
|
||||
cancel: "取消"
|
||||
confirm_email:
|
||||
click_to_confirm: "Click below to confirm your email address"
|
||||
confirm: "Confirm"
|
||||
password_reset:
|
||||
set_new_password: "Change Your Password"
|
||||
new_password: "New Password"
|
||||
new_password_help: "Password must be at least 8 characters"
|
||||
confirm_new_password: "Confirm New Password"
|
||||
change_password: "Change Password"
|
||||
characters_remaining: "剩餘字符數"
|
||||
comment:
|
||||
anon: "匿名用戶"
|
||||
@@ -61,11 +47,6 @@ zh_TW:
|
||||
reaction: '回應'
|
||||
reactions: '回應'
|
||||
story: '故事'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} approved username {1}'
|
||||
notify_rejected: '{0} rejected username {1}'
|
||||
notify_flagged: '{0} reported username {1}'
|
||||
notify_changed: 'user {0} changed their username to {1}'
|
||||
community:
|
||||
account_creation_date: "賬戶創建日期"
|
||||
active: 激活
|
||||
@@ -203,9 +184,6 @@ zh_TW:
|
||||
embedlink:
|
||||
copy: "覆制到剪貼板"
|
||||
error:
|
||||
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
|
||||
COMMENT_TOO_SHORT: "評論長度必須超過一個字符,請您修改評論後重試。"
|
||||
NOT_AUTHORIZED: "您無權執行該操作。"
|
||||
NO_SPECIAL_CHARACTERS: "用戶名只能包含字母、數字和下劃線"
|
||||
@@ -237,7 +215,6 @@ zh_TW:
|
||||
username: "用戶名只能包含字母、數字和下劃線。"
|
||||
unexpected: "發生了意外錯誤。抱歉!"
|
||||
required_field: "該字段必填"
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "舉報評論"
|
||||
flag_reason: "舉報原因(可選)"
|
||||
flag_username: "舉報用戶名"
|
||||
@@ -256,8 +233,6 @@ zh_TW:
|
||||
error: "用戶名只能包含字母、數字和下劃線。"
|
||||
label: "新用戶名"
|
||||
msg: "由於您的用戶名不當,您的帳號目前已被暫停使用。如要恢復您的帳戶,請輸入一個新的用戶名。如有任何疑問,請與我們聯繫。"
|
||||
changed_name:
|
||||
msg: "Your username change is under review by our moderation team."
|
||||
my_comments: "我的評論"
|
||||
my_profile: "我的概況"
|
||||
new_count: "查看{0}更多{1}"
|
||||
@@ -275,24 +250,6 @@ zh_TW:
|
||||
loading_results: "加載結果"
|
||||
marketing: "這看起來像是廣告/營銷"
|
||||
moderate_this_stream: "審核這個流"
|
||||
flags:
|
||||
reasons:
|
||||
user:
|
||||
username_offensive: "Offensive"
|
||||
username_nolike: "Dislike"
|
||||
username_impersonating: "Impersonation"
|
||||
username_spam: "Spam"
|
||||
username_other: "Other"
|
||||
comment:
|
||||
comment_offensive: "Offensive"
|
||||
comment_spam: "Spam"
|
||||
comment_noagree: "Disagree"
|
||||
comment_other: "Other"
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Banned Word"
|
||||
body_count: "Body exceeds max length"
|
||||
trust: "Trust"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "帳戶標記"
|
||||
actions: 操作
|
||||
@@ -345,7 +302,6 @@ zh_TW:
|
||||
no_agree_comment: "我不同意這個評論"
|
||||
no_like_bio: "我不喜歡這個個人簡介"
|
||||
no_like_username: "我不喜歡這個用戶名"
|
||||
already_flagged_username: "You have already flagged this username."
|
||||
other: 其他
|
||||
permalink: 分享
|
||||
personal_info: "該評論洩露了個人身份資訊"
|
||||
@@ -420,29 +376,6 @@ zh_TW:
|
||||
bio_flags: "該簡介的標記"
|
||||
user_bio: "用戶簡介"
|
||||
username_flags: "該用戶名的標記"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
suspended: "Suspended, {0}"
|
||||
suspension_removed: "Suspension removed"
|
||||
system: "System"
|
||||
date: "Date"
|
||||
action: "Action"
|
||||
taken_by: "Taken By"
|
||||
user_impersonating: "此用戶正在冒充"
|
||||
user_no_comment: "您尚未評論過。加入對話吧!"
|
||||
username_offensive: "這個用戶名有冒犯性"
|
||||
@@ -469,6 +402,3 @@ zh_TW:
|
||||
description: "感謝安裝Talk!我們給您發送了一封郵件以驗證您的電子郵箱地址。在完成帳戶設置後,您即可開始與您的讀者互動。"
|
||||
launch: "啟動Talk"
|
||||
close: "關閉安裝程序"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
const helmet = require('helmet');
|
||||
const { WEBSOCKET_LIVE_URI, ENABLE_STRICT_CSP } = require('../config');
|
||||
const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url');
|
||||
const { URL } = require('url');
|
||||
|
||||
// websocketUri represents the host where we can connect for websocket requests.
|
||||
const websocketUri = new URL(WEBSOCKET_LIVE_URI || BASE_URL);
|
||||
websocketUri.protocol = websocketUri.protocol.startsWith('https')
|
||||
? 'wss'
|
||||
: 'ws';
|
||||
const { origin: websocketSrc } = websocketUri;
|
||||
|
||||
// staticSrc represents any static asset hosted on the static host.
|
||||
const { host: staticSrc } = new URL(STATIC_URL);
|
||||
|
||||
// nonceSrc represents the nonce source that is used to indicate a safe resource
|
||||
// to load.
|
||||
const nonceSrc = (req, res) => `'nonce-${res.locals.nonce}'`;
|
||||
|
||||
module.exports = helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
reportUri: `${BASE_PATH}api/v1/csp`, // report all policy violations to our reporting uri
|
||||
defaultSrc: ["'none'"], // by default, do not allow anything at all
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
'https://ajax.googleapis.com', // for jquery
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
styleSrc: [
|
||||
"'self'",
|
||||
'https://maxcdn.bootstrapcdn.com', // for bootstrap css
|
||||
'https://fonts.googleapis.com', // for google fonts
|
||||
'https://code.getmdl.io', // for mdl css
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
connectSrc: ["'self'", websocketSrc],
|
||||
fontSrc: [
|
||||
"'self'",
|
||||
'https://maxcdn.bootstrapcdn.com', // for font-awesome
|
||||
'https://fonts.gstatic.com', // for google fonts
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
imgSrc: [
|
||||
"'self'",
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
},
|
||||
browserSniff: false,
|
||||
// Allow the configuration to disable strict enforcement of CSP.
|
||||
reportOnly: !ENABLE_STRICT_CSP,
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
const uuid = require('uuid/v4');
|
||||
|
||||
// nonce is designed to create a random value that can be used in conjunction
|
||||
// with the csp middleware.
|
||||
module.exports = (req, res, next) => {
|
||||
res.locals.nonce = uuid();
|
||||
|
||||
next();
|
||||
};
|
||||
+2
-1
@@ -58,7 +58,6 @@
|
||||
"dependencies": {
|
||||
"@coralproject/gql-merge": "^0.1.0",
|
||||
"@coralproject/graphql-anywhere-optimized": "^0.1.0",
|
||||
"accepts": "^1.3.4",
|
||||
"apollo-client": "^1.9.1",
|
||||
"apollo-engine": "^0.8.1",
|
||||
"apollo-server-express": "^1.2.0",
|
||||
@@ -91,6 +90,7 @@
|
||||
"common-tags": "^1.4.0",
|
||||
"compression": "1.7.1",
|
||||
"compression-webpack-plugin": "^1.0.0",
|
||||
"consolidate": "0.14.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"copy-webpack-plugin": "^4.0.0",
|
||||
"cross-spawn": "^5.1.0",
|
||||
@@ -108,6 +108,7 @@
|
||||
"express-static-gzip": "^0.3.1",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^0.11.2",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"form-data": "^2.3.1",
|
||||
"fs-extra": "^4.0.1",
|
||||
"graphql": "^0.10.1",
|
||||
|
||||
@@ -28,8 +28,11 @@ let enabled = true;
|
||||
module.exports = {
|
||||
RootMutation: {
|
||||
createComment: {
|
||||
async pre(_, { input }, { loaders, parent: req }) {
|
||||
// If the key validation failed, then we can't run with the client.
|
||||
async pre(_, { input }, ctx) {
|
||||
const req = ctx.parent.parent;
|
||||
const loaders = ctx.loaders;
|
||||
|
||||
//If the key validation failed, then we can't run with the client.
|
||||
if (!enabled) {
|
||||
debug('not enabled, passing');
|
||||
return;
|
||||
|
||||
@@ -69,6 +69,76 @@ en:
|
||||
description_2: "You can change your account settings by visiting"
|
||||
path: "My Profile > Settings"
|
||||
alert: "Email Added!"
|
||||
de:
|
||||
email:
|
||||
email_change_original:
|
||||
subject: Änderung Ihrer E-Mail-Adresse
|
||||
body: Ihre E-Mail-Adresse wurde von {0} zu {1} geändert. Falls Sie diese Änderung nicht selbst vorgenommen haben, kontaktieren Sie bitte zur Sicherheit {2}.
|
||||
error:
|
||||
NO_LOCAL_PROFILE: Mit diesem Benutzerkonto ist keine E-Mail-Adresse verbunden.
|
||||
LOCAL_PROFILE: Es ist bereits eine bestätigte E-Mail-Adresse mit diesem Benutzerkonto verbunden.
|
||||
INCORRECT_PASSWORD: Das Passwort war nicht korrekt.
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
change_password: "Passwort ändern"
|
||||
passwords_dont_match: "Die Passwörter stimmen nicht überein"
|
||||
required_field: "Diese Angabe ist erforderlich"
|
||||
forgot_password: "Passwort vergessen?"
|
||||
save: "Speichern"
|
||||
cancel: "Abbrechen"
|
||||
edit: "Ändern"
|
||||
changed_password_msg: "Passwort geändert - Ihr Passwort wurde erfolgreich geändert"
|
||||
forgot_password_sent: "Passwort vergessen - Wir haben Ihnen eine E-Mail zum Zurücksetzen des Passwortes geschickt"
|
||||
change_username:
|
||||
change_username_note: "Nutzernamen können nur alle 14 Tage geändert werden. Ihr Nutzername ist zur Zeit nicht editierbar."
|
||||
save: "Speichern"
|
||||
edit_profile: "Profil ändern"
|
||||
cancel: "Abbrechen"
|
||||
confirm_username_change: "Änderung des Nutzernamens bestätigen"
|
||||
description: "Sie möchten Ihren Nutzernamen ändern: der neue Nutzername wird an allen alten und neuen Kommentaren erscheinen."
|
||||
old_username: "Alter Nutzername"
|
||||
new_username: "Neuer Nutzername"
|
||||
bottom_note: "Achtung: die nächste Änderung des Nutzernamens ist erst nach 14 Tagen möglich"
|
||||
confirm_changes: "Änderung bestätigen"
|
||||
username_does_not_match: "Die Nutzernamen stimmen nicht überein"
|
||||
cant_be_equal: "Der neue Nutzername {0} muss sich vom alten unterscheiden."
|
||||
changed_username_success_msg: "Nutzername geändert - Ihr Nutzername wurde erfolgreich aktualisiert. Die nächste Änderung des Nutzernamens ist erst nach 14 Tagen möglich."
|
||||
change_username_attempt: "Der Nutzername kann zur Zeit nicht aktualisiert werden. Änderungen sind nur nach jeweils 14 Tagen möglich."
|
||||
change_email:
|
||||
confirm_email_change: "Änderung der E-Mail-Adresse bestätigen"
|
||||
description: "Sie versuchen, Ihre E-Mail-Adresse ändern: die neue E-Mail-Adresse wird zum Login sowie für Benachrichtigungen bzgl. Ihres Benutzerkontos verwendet."
|
||||
old_email: "Alte E-Mail-Adresse"
|
||||
new_email: "Neue E-Mail-Adresse"
|
||||
enter_password: "Passwort"
|
||||
incorrect_password: "Passwort nicht korrekt"
|
||||
confirm_change: "Änderung bestätigen"
|
||||
cancel: "Abbrechen"
|
||||
change_email_msg: "E-Mail-Adresse erfolgreich aktualisiert - die neue E-Mail-Adresse ab sofort zum Anmelden und für Benachrichtigungen verwendet."
|
||||
add_email:
|
||||
add_email_address: "E-Mail-Adresse hinzufügen"
|
||||
enter_email_address: "E-Mail-Adresse:"
|
||||
invalid_email_address: "Ungültige E-Mail-Adresse"
|
||||
confirm_email_address: "Bestätigung der E-Mail-Adresse:"
|
||||
email_does_not_match: "Die E-Mail-Adressen stimmen nicht überein"
|
||||
insert_password: "Passwort auswählen:"
|
||||
required_field: "Dieses Feld ist erforderlich"
|
||||
done: "Fertig"
|
||||
content:
|
||||
title: "E-Mail-Adresse hinzufügen"
|
||||
description: "Aus Sicherheitsgründen benötigen wir eine E-Mail-Adresse zu jedem Benutzerkonto. Ihre E-Mail-Adresse wird für folgendes verwendet:"
|
||||
item_1: "Benachrichtigungen über Änderungen am Benutzerkonto (Nutzername, E-Mail-Adresse, Passwort)"
|
||||
item_2: "Ermöglicht den Download des eigenen Kommentar-Archivs"
|
||||
item_3: "Kommentar-Benachrichtigungen erhalten, die Sie explizit angefordert haben"
|
||||
verify:
|
||||
title: "E-Mail-Adresse bestätigen"
|
||||
description: "Wir haben einen E-Mail an {0} geschickt. Bitte bestätigen Sie Ihre E-Mail-Adresse, um damit Benachrichtigungen über Änderungen am Benutzerkonto zu erhalten."
|
||||
added:
|
||||
title: "E-Mail-Adresse hinzugefügt"
|
||||
description: "Ihre E-Mail-Adresse wurde dem Benutzerkonto hinzugefügt."
|
||||
subtitle: "Sie möchten Ihre E-Mail-Adresse ändern?"
|
||||
description_2: "Sie können Ihre Konto-Einstellugen ändern unter"
|
||||
path: "Mein Profil > Profil-Einstellungen"
|
||||
alert: "E-Mail-Adresse hinzugefügt!"
|
||||
es:
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
en:
|
||||
talk-plugin-notifications-category-featured:
|
||||
toggle_description: My comment is featured
|
||||
de:
|
||||
talk-plugin-notifications-category-featured:
|
||||
toggle_description: Mein Kommentar wird empfohlen
|
||||
|
||||
@@ -3,4 +3,10 @@ en:
|
||||
categories:
|
||||
featured:
|
||||
subject: "One of your comments was featured on {0}"
|
||||
body: "{0}\nA member of our team has selected this comment to be featured for other readers: {1}"
|
||||
body: "{0}\nA member of our team has selected this comment to be featured for other readers: {1}"
|
||||
de:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
featured:
|
||||
subject: "Einer Ihrer Kommentare wurde auf {0} hervorgehoben"
|
||||
body: "{0}\nEin Mitglied unseres Teams hat diesen Kommentar ausgewählt, er wird jetzt für andere Leser besonders hervorgehoben: {1}"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
en:
|
||||
talk-plugin-notifications-category-reply:
|
||||
toggle_description: My comment receives a reply
|
||||
de:
|
||||
talk-plugin-notifications-category-reply:
|
||||
toggle_description: Jemand antwortet auf meinen Kommentar
|
||||
|
||||
@@ -3,4 +3,10 @@ en:
|
||||
categories:
|
||||
reply:
|
||||
subject: "Someone has replied to your comment on {0}"
|
||||
body: "{0}\n{1} replied to your comment: {2}"
|
||||
body: "{0}\n{1} replied to your comment: {2}"
|
||||
de:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
reply:
|
||||
subject: "Jemand hat bei {0} auf Ihren Kommentar geantwortet"
|
||||
body: "{0}\n{1} antwortete auf Ihren Kommentar: {2}"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
en:
|
||||
talk-plugin-notifications-category-staff:
|
||||
toggle_description: A staff member replies to my comment
|
||||
de:
|
||||
talk-plugin-notifications-category-staff:
|
||||
toggle_description: Ein Redaktionsmitglied antwortet auf meinen Kommentar
|
||||
|
||||
@@ -3,4 +3,10 @@ en:
|
||||
categories:
|
||||
staff:
|
||||
subject: "Someone at {0} has replied to your comment"
|
||||
body: "{0}\n{1} works for {2} and has replied to your comment: {3}"
|
||||
body: "{0}\n{1} works for {2} and has replied to your comment: {3}"
|
||||
de:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
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}"
|
||||
|
||||
@@ -2,3 +2,7 @@ en:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
DAILY: In a daily digest
|
||||
de:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
DAILY: Einmal täglich
|
||||
|
||||
@@ -2,3 +2,7 @@ en:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
HOURLY: In an hourly digest
|
||||
de:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
HOURLY: Stündlich
|
||||
|
||||
@@ -16,3 +16,21 @@ en:
|
||||
digest_option: Send notifications
|
||||
digest_enum:
|
||||
NONE: Immediately
|
||||
de:
|
||||
talk-plugin-notifications:
|
||||
settings_title: Benachrichtigungen
|
||||
settings_subtitle: Benachrichtige mich wenn
|
||||
turn_off_all: Ich möchte keine Benachrichtigungen erhalten
|
||||
banner_info:
|
||||
title: Bestätigte E-Mail-Adresse benötigt
|
||||
text: Um E-Mail-Benachrichtigungen zu erhalten, müssen Sie eine bestätigte E-Mail-Adresse haben.
|
||||
verify_now: E-Mail-Adresse jetzt bestätigen
|
||||
banner_success:
|
||||
title: E-Mail-Bestätigungsanfrage verschickt
|
||||
text: Eine E-Mail mit einem Bestätigungslink wurde an {0} geschickt.
|
||||
banner_error:
|
||||
title: Fehler
|
||||
text: Beim Versand der Bestätigungsmail gab es einen Fehler. Bitte versuchen Sie es später erneut.
|
||||
digest_option: Benachrichtigungen senden
|
||||
digest_enum:
|
||||
NONE: Sofort
|
||||
|
||||
@@ -4,7 +4,7 @@ const { get, isEmpty, reduce } = require('lodash');
|
||||
|
||||
module.exports = router => {
|
||||
router.get('/account/unsubscribe-notifications', (req, res) => {
|
||||
res.render(path.join(__dirname, 'views/unsubscribe-notifications'));
|
||||
res.render(path.join(__dirname, 'views/unsubscribe-notifications.njk'));
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,4 +11,18 @@ en:
|
||||
click_to_confirm: "Click below to confirm that you would like to unsubscribe from all notifications"
|
||||
confirm: "Confirm"
|
||||
are_unsubscribed: "You are now unsubscribed from all notifications."
|
||||
token_invalid: "Unsubscribe link is invalid, click the link from a more recent email or visit a comment stream and login to change your notification preferences"
|
||||
token_invalid: "Unsubscribe link is invalid, click the link from a more recent email or visit a comment stream and login to change your notification preferences"
|
||||
de:
|
||||
talk-plugin-notifications:
|
||||
templates:
|
||||
digest:
|
||||
subject: "Ihre Kommentaraktivität bei {0}"
|
||||
footer: "Sie erhalten diese Benachrichtigung, weil Sie Community-Mitglied bei {0} sind und diese E-Mails abonniert haben."
|
||||
links:
|
||||
unsubscribe: "E-Mail-Benachrichtigungen abbestellen"
|
||||
unsubscribe_page:
|
||||
unsubscribe: "Kommentar-Benachrichtigungen abbestellen"
|
||||
click_to_confirm: "Klicken Sie folgenden Link, um zu bestätigen, dass Sie alle Benachrichtigungen abbestellen möchten"
|
||||
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"
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title><%= t('talk-plugin-notifications.unsubscribe_page.unsubscribe') %></title>
|
||||
<%- include(root + '/partials/head') %>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
</head>
|
||||
<body class="confirm-email-page">
|
||||
<div id="root">
|
||||
<div class="error-console container"><%= t('talk-plugin-notifications.unsubscribe_page.token_invalid') %></div>
|
||||
<div id="success" style="display:none;" class="legend container"><%= t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') %></div>
|
||||
<form id="unsubscribe-form" class="container">
|
||||
<legend class="legend"><%= t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') %></legend>
|
||||
<button type="submit"><%= t('talk-plugin-notifications.unsubscribe_page.confirm') %></button>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var submitting = false;
|
||||
var payload = JSON.stringify({token: location.hash.replace('#', '')});
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/unsubscribe-notifications',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
}).then(function (success) {
|
||||
$('#unsubscribe-form').fadeOut(function () {
|
||||
$('#success').fadeIn();
|
||||
});
|
||||
}).catch(function () {
|
||||
submitting = false;
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/unsubscribe-notifications/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
.then(function () {
|
||||
$('#unsubscribe-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function () {
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('talk-plugin-notifications.unsubscribe_page.unsubscribe') }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ super() }}
|
||||
<style nonce="{{ nonce }}" type="text/css">
|
||||
#success { display:none; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<div class="error-console container">{{ t('talk-plugin-notifications.unsubscribe_page.token_invalid') }}</div>
|
||||
<div id="success" class="legend container">{{ t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') }}</div>
|
||||
<form id="unsubscribe-form" class="container">
|
||||
<legend class="legend">{{ t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') }}</legend>
|
||||
<button type="submit">{{ t('talk-plugin-notifications.unsubscribe_page.confirm') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
var submitting = false;
|
||||
var payload = JSON.stringify({token: location.hash.replace('#', '')});
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/unsubscribe-notifications',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
}).then(function (success) {
|
||||
$('#unsubscribe-form').fadeOut(function () {
|
||||
$('#success').fadeIn();
|
||||
});
|
||||
}).catch(function () {
|
||||
submitting = false;
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/unsubscribe-notifications/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
.then(function () {
|
||||
$('#unsubscribe-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function () {
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -63,17 +63,18 @@ class DeleteMyAccount extends React.Component {
|
||||
<p className="talk-plugin-auth--delete-my-account-description">
|
||||
{t('delete_request.delete_my_account_description')}
|
||||
</p>
|
||||
<p className="talk-plugin-auth--delete-my-account-description">
|
||||
{scheduledDeletionDate &&
|
||||
t(
|
||||
'delete_request.already_submitted_request_description',
|
||||
moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a')
|
||||
)}
|
||||
</p>
|
||||
{scheduledDeletionDate ? (
|
||||
<Button onClick={this.cancelAccountDeletion}>
|
||||
{t('delete_request.cancel_account_deletion_request')}
|
||||
</Button>
|
||||
<div>
|
||||
<p className="talk-plugin-auth--delete-my-account-description">
|
||||
{t(
|
||||
'delete_request.already_submitted_request_description',
|
||||
moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a')
|
||||
)}
|
||||
</p>
|
||||
<Button onClick={this.cancelAccountDeletion}>
|
||||
{t('delete_request.cancel_account_deletion_request')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button icon="delete" onClick={this.showDialog}>
|
||||
{t('delete_request.delete_my_account')}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
.button {
|
||||
margin: 0;
|
||||
|
||||
&:disabled {
|
||||
/* TODO: better optimize for mobile so this styling is not necessary */
|
||||
height: auto;
|
||||
}
|
||||
i {
|
||||
font-size: inherit;
|
||||
vertical-align: sub;
|
||||
|
||||
@@ -49,3 +49,54 @@ en:
|
||||
subtitle: "Are you sure you want to delete your account?"
|
||||
description: "To confirm you would like to delete your account please type in the following phrase into the text box below:"
|
||||
type_to_confirm: "Type phrase below to confirm"
|
||||
de:
|
||||
download_request:
|
||||
section_title: "Mein Kommentar-Archiv herunterladen"
|
||||
you_will_get_a_copy: "Sie werden eine E-Mail mit einem Download-Link erhalten. Sie können"
|
||||
download_rate: "eine Download-Anfrage alle {0} Tage stellen"
|
||||
most_recent_request: "Ihre letzte Anfrage"
|
||||
request: "Kommentar-Archiv anfordern"
|
||||
rate_limit: "Sie können die nächste Anfrage stellen in {0}"
|
||||
hours: "{0} Stunden"
|
||||
days: "{0} Tagen"
|
||||
hour: "{0} Stunde"
|
||||
day: "{0} Tag"
|
||||
download_preparing: "Bereite Konto-Download vor - Überprüfen Sie Ihr E-Mail-Postfach, sie sollten den Download-Link demnächst erhalten"
|
||||
delete_request:
|
||||
account_deletion_cancelled: 'Konto-Löschung abgebrochen - Ihre Anfrage, Ihr Konto zu löschen wurde abgebrochen.'
|
||||
account_deletion_requested: 'Konto-Löschung angefordert'
|
||||
received_on: "Die Anfrage, Ihr Konto zu löschen haben wir erhalten am "
|
||||
cancel_request_description: "Fall Sie ihr Konto wieder aktivieren möchten, können Sie die Lösch-Anfrage hier abbrechen"
|
||||
before: "vorher"
|
||||
cancel_account_deletion_request: "Konto-Lösch-Anfrage abbrechen"
|
||||
delete_my_account: "Mein Benutzerkonto löschen"
|
||||
delete_my_account_description: "Mit dem Löschen Ihres Kontos werden Ihr Profil sowie alle Ihre Kommentare dauerhaft von dieser Website entfernt."
|
||||
already_submitted_request_description: "Sie haben bereits eine Lösch-Anfrage gestellt. Ihr Konto wird nach dem {0} gelöscht. Bis zu diesem Zeitpunkt können Sie die Anfrage noch abbrechen"
|
||||
your_request_submitted_description: "Ihre Lösch-Anfrage wurde übermittelt und eine Bestätigungsanfrage an die dem Konto zugehörige E-Mail-Adresse geschickt."
|
||||
your_account_deletion_scheduled: "Ihr Benutzerkonto wird gelöscht nach dem:"
|
||||
changed_your_mind: "Haben Sie Ihre Meinung geändert?"
|
||||
simply_go_to: "Gehen Sie einfach vor dem Zeitpunkt zu Ihrem Konto und klicken Sie"
|
||||
tell_us_why: "Sagen Sie uns warum"
|
||||
feedback_copy: "Wir würden gern erfahren, warum Sie sich entschieden haben, Ihr Konto zu löschen. Schicken Sie uns eine E-Mail mit Feedback an"
|
||||
done: "Fertig"
|
||||
cancel: "Abbrechen"
|
||||
proceed: "Fortfahren"
|
||||
input_is_not_correct: "Die Eingabe ist nicht korrekt"
|
||||
step_0:
|
||||
you_are_attempting: "Sie versuchen Ihr Konto zu löschen. Das bedeutet:"
|
||||
item_1: "Alle Ihre Kommentare werden von der Website entfernt"
|
||||
item_2: "Alle Ihre Kommentare werden aus unserer Datenbank gelöscht"
|
||||
item_3: "Ihr Nutzername und Ihre E-Mail-Adresse werden aus unserem System gelöscht"
|
||||
step_1:
|
||||
subtitle: "Wann wird mein Benutzerkonto entfernt?"
|
||||
description: "Ihr Konto wird {0} Stunden nachdem Sie die Anfrage gestellt haben gelöscht."
|
||||
subtitle_2: "Kann ich weiterhin Kommentare schreiben, bis mein Konto gelöscht wird?"
|
||||
description_2: "Ja, Sie können kommentieren, antworten usw. bis die {0} Stunden abgelaufen sind."
|
||||
step_2:
|
||||
description: "Bevor Ihr Konto gelöscht wird, empfehlen wir Ihnen, Ihr Kommentar-Archiv herunterzuladen. Nach der Konto-Löschung ist dies nicht mehr möglich."
|
||||
to_download: "Um Ihr Konto-Archiv herunterzuladen gehen sie zu:"
|
||||
path: "Profil > Mein Kommentar-Archiv herunterladen"
|
||||
step_3:
|
||||
subtitle: "Sind Sie sicher, dass Sie Ihr Benutzerkonto löschen möchten?"
|
||||
description: "Um zu bestätigen, dass Sie Ihr Konto löschen möchten, geben Sie bitte folgende Zeichen in das Textfeld ein:"
|
||||
type_to_confirm: "Zur Bestätigung Zeichenfolge eingeben"
|
||||
|
||||
@@ -102,7 +102,7 @@ async function loadComments(ctx, userID, archive, latestContentDate) {
|
||||
module.exports = router => {
|
||||
// /account/download will render the download page.
|
||||
router.get('/account/download', (req, res) => {
|
||||
res.render(path.join(__dirname, 'views', 'download'));
|
||||
res.render(path.join(__dirname, 'views', 'download.njk'));
|
||||
});
|
||||
|
||||
// /api/v1/account/download will send back a zipped archive of the users
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('download_landing.download_your_account') %></title>
|
||||
<%- include(root + '/partials/account') %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('download_landing.download_your_account') %></h1>
|
||||
<p><%= t('download_landing.download_details') %></p>
|
||||
<p><%= t('download_landing.all_information_included') %></p>
|
||||
<ul class="check_list">
|
||||
<li><%= t('download_landing.information_included.date') %></li>
|
||||
<li><%= t('download_landing.information_included.url') %></li>
|
||||
<li><%= t('download_landing.information_included.body') %></li>
|
||||
<li><%= t('download_landing.information_included.asset_url') %></li>
|
||||
</ul>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="download-form" method="post" action="<%= BASE_PATH %>api/v1/account/download">
|
||||
<button type="submit"><%= t('download_landing.confirm') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
var token = location.hash.replace('#', '');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/download',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: token, check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#download-form').append('<input name="token" type="hidden" value="' + token + '"/>').fadeIn();
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('download_landing.download_your_account') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('download_landing.download_your_account') }}</h1>
|
||||
<p>{{ t('download_landing.download_details') }}</p>
|
||||
<p>{{ t('download_landing.all_information_included') }}</p>
|
||||
<ul class="check_list">
|
||||
<li>{{ t('download_landing.information_included.date') }}</li>
|
||||
<li>{{ t('download_landing.information_included.url') }}</li>
|
||||
<li>{{ t('download_landing.information_included.body') }}</li>
|
||||
<li>{{ t('download_landing.information_included.asset_url') }}</li>
|
||||
</ul>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="download-form" method="post" action="{{ BASE_PATH }}api/v1/account/download">
|
||||
<button type="submit">{{ t('download_landing.confirm') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
var token = location.hash.replace('#', '');
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/download',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: token, check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#download-form').append('<input name="token" type="hidden" value="' + token + '"/>').fadeIn();
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -35,3 +35,40 @@ en:
|
||||
body: "You have cancelled your account deletion request for {0}. Your account is now reactivated."
|
||||
error:
|
||||
DOWNLOAD_TOKEN_INVALID: "Your download link is not valid."
|
||||
de:
|
||||
download_landing:
|
||||
download_your_account: "Mein Kommentar-Archiv herunterladen"
|
||||
download_details: "Ihr Kommentar-Archiv wird als ZIP-Datei bereitgestellt. Nach dem Entpacken erhalten Sie eine CSV-Datei, die einfach in ein Tabellenkalkulationsprogramm importiert werden kann."
|
||||
all_information_included: "Für jeden Ihrer Kommentare sind folgende Informationen enthalten:"
|
||||
information_included:
|
||||
date: "Wann Sie den Kommentar geschrieben haben"
|
||||
url: "Die dauerhafte URL (Internetadresse) des Kommentars"
|
||||
body: "Der Kommentar-Text"
|
||||
asset_url: "Die URL (Internetadresse) des Artikels an dem der Kommentar erscheint"
|
||||
confirm: "Kommentar-Archiv herunterladen"
|
||||
email:
|
||||
download:
|
||||
subject: "Ihre Kommentare sind zum Download bereit: {0}"
|
||||
download_link_ready: "Hier klicken, um Ihre Kommentare von {0} bis {1} herunterzuladen:"
|
||||
download_archive: "Archiv herunterladen"
|
||||
delete:
|
||||
subject: "Ihr Benutzerkonto bei {0} ist zur Löschung vorgesehen"
|
||||
body: |
|
||||
Wir haben eine Anfrage erhalten, Ihr Benutzerkonto zu löschen. Die Löschung ist geplant für den {1}.
|
||||
|
||||
Nach diesem Zeitpunkt werden alle Ihre Kommentare von der Website und aus unserer Datenbank gelöscht. Außerdem werden Ihr Nutzername und Ihre E-Mail-Adresse aus unserem System enfernt.
|
||||
|
||||
Falls Sie es sich noch anders überlegen, können Sie sich bis spätestens zum angegebenen Lösch-Zeitpunkt einloggen und die Lösch-Anfrage abbrechen.
|
||||
deleted:
|
||||
subject: "Ihre Benutzerkonto bei {0} wurde gelöscht"
|
||||
body: |
|
||||
Ihr Kommentar-Konto bei {0} ist nun gelöscht. Schade, auf Wiedersehen!
|
||||
|
||||
Falls Sie sich in Zukunft erneut an der Diskussion beteiligen möchten, können Sie jederzeit ein neues Benutzerkonto einrichten.
|
||||
|
||||
Wenn Sie Lust haben, schreiben Sie uns doch eine Rückmeldung, Feedback, oder Kritik an {1}, damit wir unsere Community verbessern können. Vielen Dank!
|
||||
cancelDelete:
|
||||
subject: "Die Lösch-Anfrage für Ihr Benutzerkonto bei {0} wurde abgebrochen"
|
||||
body: "Sie haben die Lösch-Anfrage für Ihr Benutzerkonto bei {0} abgebrochen. Das Konto ist nun wieder aktiv."
|
||||
error:
|
||||
DOWNLOAD_TOKEN_INVALID: "Der Download-Link ist ungültig."
|
||||
|
||||
@@ -7,21 +7,17 @@ class Button extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
onClick,
|
||||
children,
|
||||
active,
|
||||
activeClassName,
|
||||
disabled,
|
||||
...rest
|
||||
} = this.props;
|
||||
return (
|
||||
<button
|
||||
className={cn(className, styles.button, {
|
||||
[cn(styles.active, activeClassName)]: active,
|
||||
})}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
@@ -32,11 +28,8 @@ class Button extends React.Component {
|
||||
Button.propTypes = {
|
||||
className: PropTypes.string,
|
||||
activeClassName: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '../components/Button';
|
||||
import bowser from 'bowser';
|
||||
|
||||
/**
|
||||
* createToggle creates a button that can be active, inactive or disabled
|
||||
@@ -32,7 +33,17 @@ const createToggle = (
|
||||
this.execCommand();
|
||||
};
|
||||
|
||||
// Detect whether there was focus on the RTE before the click.
|
||||
hadFocusBeforeClick = false;
|
||||
handleMouseDown = () => (this.hadFocusBeforeClick = this.props.api.focused);
|
||||
|
||||
handleClick = () => {
|
||||
// Skip IOS when the focus was not there before.
|
||||
// IOS fails to focus to the RTE correctly and scrolls to nirvana.
|
||||
// See https://www.pivotaltracker.com/story/show/157607216
|
||||
if (!this.hadFocusBeforeClick && bowser.ios) {
|
||||
return;
|
||||
}
|
||||
this.props.api.focus();
|
||||
this.formatToggle();
|
||||
this.props.api.focus();
|
||||
@@ -62,6 +73,7 @@ const createToggle = (
|
||||
<Button
|
||||
className={className}
|
||||
title={title}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onClick={this.handleClick}
|
||||
active={this.state.active}
|
||||
disabled={disabled || this.state.disabled}
|
||||
|
||||
@@ -381,9 +381,13 @@ export function isBogusBR(node) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all nodes after `node`.
|
||||
* Returns an array of all nodes after `node` in a _line_.
|
||||
*/
|
||||
export function getRightOfNode(node) {
|
||||
if (node.tagName === 'BR') {
|
||||
return [];
|
||||
}
|
||||
|
||||
let result = [];
|
||||
let cur = node;
|
||||
while (
|
||||
@@ -515,10 +519,10 @@ export function outdentBlock(node, changeSelection) {
|
||||
|
||||
// A new lines to substitute the missing block element.
|
||||
const needLineAfter =
|
||||
node.nextSibling &&
|
||||
!isBlockElement(node.nextSibling) &&
|
||||
node.lastChild &&
|
||||
!isBlockElement(node.lastChild);
|
||||
!node.lastChild ||
|
||||
(node.nextSibling &&
|
||||
!isBlockElement(node.nextSibling) &&
|
||||
!isBlockElement(node.lastChild));
|
||||
const needLineBefore =
|
||||
node.previousSibling &&
|
||||
!isBlockElement(node.previousSibling) &&
|
||||
|
||||
@@ -2,11 +2,11 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/email/confirm', (req, res) => {
|
||||
res.render('account/email/confirm');
|
||||
res.render('account/email/confirm.njk');
|
||||
});
|
||||
|
||||
router.get('/password/reset', (req, res) => {
|
||||
res.render('account/password/reset');
|
||||
res.render('account/password/reset.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('admin');
|
||||
res.render('admin.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const express = require('express');
|
||||
const Joi = require('joi');
|
||||
const { logger } = require('../../../services/logging');
|
||||
const router = express.Router();
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
'csp-report': Joi.object().keys({
|
||||
'document-uri': Joi.string(),
|
||||
referrer: Joi.string(),
|
||||
'blocked-uri': Joi.string(),
|
||||
'violated-directive': Joi.string(),
|
||||
'original-policy': Joi.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const json = express.json({ type: 'application/csp-report' });
|
||||
|
||||
router.post('/', json, async (req, res, next) => {
|
||||
const { value, error: err } = Joi.validate(req.body, schema, {
|
||||
stripUnknown: true,
|
||||
presence: 'required',
|
||||
});
|
||||
if (err) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error({ report: value }, 'csp violation reported');
|
||||
|
||||
res.status(202).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -10,7 +10,7 @@ router.use('/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Interactive graphiql interface.
|
||||
router.use('/iql', staticTemplate, (req, res) => {
|
||||
res.render('api/graphiql', {
|
||||
res.render('api/graphiql.njk', {
|
||||
endpointURL: 'api/v1/graph/ql',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ router.get('/', (req, res) => {
|
||||
});
|
||||
|
||||
router.use('/account', require('./account'));
|
||||
router.use('/csp', require('./csp'));
|
||||
router.use('/assets', require('./assets'));
|
||||
router.use('/auth', require('./auth'));
|
||||
router.use('/graph', require('./graph'));
|
||||
|
||||
@@ -11,7 +11,7 @@ router.get('/id/:asset_id', async (req, res, next) => {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
res.render('dev/article', {
|
||||
res.render('dev/article.njk', {
|
||||
title: asset.title,
|
||||
asset_id: asset.id,
|
||||
asset_url: asset.url,
|
||||
@@ -28,7 +28,7 @@ router.get('/random', (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/title/:asset_title', (req, res) => {
|
||||
res.render('dev/article', {
|
||||
res.render('dev/article.njk', {
|
||||
title: req.params.asset_title.split('-').join(' '),
|
||||
asset_url: '',
|
||||
asset_id: null,
|
||||
@@ -48,7 +48,7 @@ router.get('/', async (req, res, next) => {
|
||||
Asset.count(),
|
||||
]);
|
||||
|
||||
res.render('dev/articles', {
|
||||
res.render('dev/articles.njk', {
|
||||
skip,
|
||||
limit,
|
||||
count,
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ router.get('/', staticTemplate, async (req, res) => {
|
||||
await SetupService.isAvailable();
|
||||
return res.redirect(url.resolve(MOUNT_PATH, 'admin/install'));
|
||||
} catch (e) {
|
||||
return res.render('dev/article', {
|
||||
return res.render('dev/article.njk', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
asset_id: '',
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/stream', (req, res) => {
|
||||
res.render('embed/stream');
|
||||
res.render('embed/stream.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+10
-6
@@ -9,7 +9,9 @@ const path = require('path');
|
||||
const compression = require('compression');
|
||||
const plugins = require('../services/plugins');
|
||||
const staticTemplate = require('../middleware/staticTemplate');
|
||||
const staticMiddleware = require('express-static-gzip');
|
||||
const contentSecurityPolicy = require('../middleware/contentSecurityPolicy');
|
||||
const nonce = require('../middleware/nonce');
|
||||
const staticServer = require('express-static-gzip');
|
||||
const { DISABLE_STATIC_SERVER } = require('../config');
|
||||
const { passport } = require('../services/passport');
|
||||
const { MOUNT_PATH } = require('../url');
|
||||
@@ -48,7 +50,7 @@ if (!DISABLE_STATIC_SERVER) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
router.use(
|
||||
'/static',
|
||||
staticMiddleware(dist, {
|
||||
staticServer(dist, {
|
||||
indexFromEmptyFile: false,
|
||||
enableBrotli: true,
|
||||
customCompressions: [
|
||||
@@ -74,10 +76,12 @@ router.use(compression());
|
||||
// STATIC ROUTES
|
||||
//==============================================================================
|
||||
|
||||
router.use('/admin', staticTemplate, require('./admin'));
|
||||
router.use('/account', staticTemplate, require('./account'));
|
||||
router.use('/login', staticTemplate, require('./login'));
|
||||
router.use('/embed', staticTemplate, require('./embed'));
|
||||
const staticMiddleware = [staticTemplate, nonce, contentSecurityPolicy];
|
||||
|
||||
router.use('/admin', ...staticMiddleware, require('./admin'));
|
||||
router.use('/account', ...staticMiddleware, require('./account'));
|
||||
router.use('/login', ...staticMiddleware, require('./login'));
|
||||
router.use('/embed', ...staticMiddleware, require('./embed'));
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('login');
|
||||
res.render('login.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+4
-6
@@ -2,15 +2,13 @@ const express = require('express');
|
||||
const debug = require('debug')('talk:routes:plugins');
|
||||
const plugins = require('../services/plugins');
|
||||
const staticTemplate = require('../middleware/staticTemplate');
|
||||
const contentSecurityPolicy = require('../middleware/contentSecurityPolicy');
|
||||
const nonce = require('../middleware/nonce');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Routes mounted from plugins won't have access to our internal partials
|
||||
// directory, so we should make that available.
|
||||
router.use(staticTemplate, (req, res, next) => {
|
||||
res.locals.root = res.app.get('views');
|
||||
next();
|
||||
});
|
||||
// Apply the middleware.
|
||||
router.use(staticTemplate, nonce, contentSecurityPolicy);
|
||||
|
||||
// Inject server route plugins.
|
||||
plugins.get('server', 'router').forEach(plugin => {
|
||||
|
||||
+45
-31
@@ -1,8 +1,11 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const debug = require('debug')('talk:services:i18n');
|
||||
const accepts = require('accepts');
|
||||
const { get, has, merge } = require('lodash');
|
||||
const {
|
||||
acceptedLanguages,
|
||||
negotiateLanguages,
|
||||
} = require('fluent-langneg/compat');
|
||||
const { first, get, has, merge, isUndefined } = require('lodash');
|
||||
const yaml = require('yamljs');
|
||||
const plugins = require('./plugins');
|
||||
const { DEFAULT_LANG } = require('../config');
|
||||
@@ -11,7 +14,7 @@ const resolve = (...paths) =>
|
||||
path.resolve(path.join(__dirname, '..', 'locales', ...paths));
|
||||
|
||||
// Load all the translations.
|
||||
let translations = fs
|
||||
const translations = fs
|
||||
.readdirSync(resolve())
|
||||
|
||||
// Resolve all the filenames relative the the locales directory.
|
||||
@@ -23,26 +26,22 @@ let translations = fs
|
||||
// Load the translation files from disk.
|
||||
.map(filename => fs.readFileSync(filename, 'utf8'))
|
||||
|
||||
// Load the translation files.
|
||||
.reduce((packs, contents) => {
|
||||
const pack = yaml.parse(contents);
|
||||
|
||||
return merge(packs, pack);
|
||||
}, {});
|
||||
// Load the translation files and merge the yaml into the existing packs.
|
||||
.reduce((packs, contents) => merge(packs, yaml.parse(contents)), {});
|
||||
|
||||
// Create a list of all supported translations.
|
||||
const languages = Object.keys(translations);
|
||||
const supportedLocales = Object.keys(translations);
|
||||
|
||||
// Move the default language to the front.
|
||||
if (languages.includes(DEFAULT_LANG)) {
|
||||
const from = languages.indexOf(DEFAULT_LANG);
|
||||
languages.splice(from, 1);
|
||||
languages.splice(0, 0, DEFAULT_LANG);
|
||||
if (supportedLocales.includes(DEFAULT_LANG)) {
|
||||
const from = supportedLocales.indexOf(DEFAULT_LANG);
|
||||
supportedLocales.splice(from, 1);
|
||||
supportedLocales.splice(0, 0, DEFAULT_LANG);
|
||||
}
|
||||
debug(`loaded language sets for ${languages}`);
|
||||
debug(`loaded language sets for ${supportedLocales}`);
|
||||
|
||||
let loadedPluginTranslations = false;
|
||||
const loadPluginTranslations = () => {
|
||||
const lazyLoadPluginTranslations = () => {
|
||||
if (loadedPluginTranslations) {
|
||||
return;
|
||||
}
|
||||
@@ -55,7 +54,15 @@ const loadPluginTranslations = () => {
|
||||
|
||||
const pack = yaml.parse(fs.readFileSync(filename, 'utf8'));
|
||||
|
||||
translations = merge(translations, pack);
|
||||
// Merge the translations into the system translations.
|
||||
merge(translations, pack);
|
||||
|
||||
// Push new languages into the supportedLocales array.
|
||||
Object.keys(pack).forEach(language => {
|
||||
if (!supportedLocales.includes(language)) {
|
||||
supportedLocales.push(language);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
loadedPluginTranslations = true;
|
||||
@@ -64,7 +71,7 @@ const loadPluginTranslations = () => {
|
||||
const t = lang => (key, ...replacements) => {
|
||||
// Loads the translations into the translations array from plugins. This is
|
||||
// done lazily to ensure that we don't have an import cycle.
|
||||
loadPluginTranslations();
|
||||
lazyLoadPluginTranslations();
|
||||
|
||||
let translation;
|
||||
if (has(translations[lang], key)) {
|
||||
@@ -74,16 +81,17 @@ const t = lang => (key, ...replacements) => {
|
||||
console.warn(`${lang}.${key} language key not set`);
|
||||
}
|
||||
|
||||
if (translation) {
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
return translation;
|
||||
} else {
|
||||
if (!translation) {
|
||||
console.warn(`${lang}.${key} and en.${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// Handle replacements in the translation string.
|
||||
return translation.replace(
|
||||
/{(\d+)}/g,
|
||||
(match, number) =>
|
||||
!isUndefined(replacements[number]) ? replacements[number] : match
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -92,13 +100,19 @@ const t = lang => (key, ...replacements) => {
|
||||
*/
|
||||
const i18n = {
|
||||
request(req) {
|
||||
debug(`possible languages given request '${accepts(req).languages()}'`);
|
||||
const lang = accepts(req).language(languages);
|
||||
debug(`parsed request language as '${lang}'`);
|
||||
const language = lang ? lang : DEFAULT_LANG;
|
||||
debug(`decided language as '${language}'`);
|
||||
const acceptsLanguages = acceptedLanguages(req.headers['accept-language']);
|
||||
debug(`possible languages given request '${acceptsLanguages}'`);
|
||||
|
||||
return t(language);
|
||||
// negotiate the language.
|
||||
const lang = first(
|
||||
negotiateLanguages(acceptsLanguages, supportedLocales, {
|
||||
defaultLocale: DEFAULT_LANG,
|
||||
strategy: 'lookup',
|
||||
})
|
||||
);
|
||||
debug(`decided language as '${lang}'`);
|
||||
|
||||
return t(lang);
|
||||
},
|
||||
t: t(DEFAULT_LANG),
|
||||
};
|
||||
|
||||
+8
-11
@@ -84,7 +84,7 @@ class KarmaModel {
|
||||
return KarmaService.isReliable('flag', this.model);
|
||||
}
|
||||
|
||||
get flaggerScore() {
|
||||
get flaggerKarma() {
|
||||
return get(this.model, 'flag.karma', 0);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class KarmaModel {
|
||||
return KarmaService.isReliable('comment', this.model);
|
||||
}
|
||||
|
||||
get commenterScore() {
|
||||
get commenterKarma() {
|
||||
return get(this.model, 'comment.karma', 0);
|
||||
}
|
||||
}
|
||||
@@ -115,18 +115,14 @@ class KarmaService {
|
||||
/**
|
||||
* Inspects the reliability of a property and returns it if known.
|
||||
* @param {String} name - name of the property
|
||||
* @param {Object} trust - object possibly containing the propertys
|
||||
* @param {Object} trust - object possibly containing the properties
|
||||
*/
|
||||
static isReliable(name, trust) {
|
||||
if (trust && trust[name]) {
|
||||
if (trust[name].karma > THRESHOLDS[name].RELIABLE) {
|
||||
return true;
|
||||
} else if (trust[name].karma < THRESHOLDS[name].UNRELIABLE) {
|
||||
return false;
|
||||
}
|
||||
} else if (THRESHOLDS[name].RELIABLE < 0) {
|
||||
const karma = get(trust, [name, 'karma'], 0);
|
||||
|
||||
if (karma >= THRESHOLDS[name].RELIABLE) {
|
||||
return true;
|
||||
} else if (THRESHOLDS[name].UNRELIABLE > 0) {
|
||||
} else if (karma <= THRESHOLDS[name].UNRELIABLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -171,3 +167,4 @@ class KarmaService {
|
||||
}
|
||||
|
||||
module.exports = KarmaService;
|
||||
module.exports.THRESHOLDS = THRESHOLDS;
|
||||
|
||||
@@ -115,13 +115,13 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
|
||||
res.locals.encodeJSONForHTML = encodeJSONForHTML;
|
||||
|
||||
if (err) {
|
||||
return res.render('auth-callback', {
|
||||
return res.render('auth-callback.njk', {
|
||||
auth: { err, data: null },
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.render('auth-callback', {
|
||||
return res.render('auth-callback.njk', {
|
||||
auth: { err: new ErrNotAuthorized(), data: null },
|
||||
});
|
||||
}
|
||||
@@ -132,7 +132,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
|
||||
SetTokenForSafari(req, res, token);
|
||||
|
||||
// We logged in the user! Let's send back the user data.
|
||||
res.render('auth-callback', {
|
||||
res.render('auth-callback.njk', {
|
||||
auth: { err: null, data: { user, token } },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
const chai = require('chai');
|
||||
const { expect } = chai;
|
||||
const { merge } = require('lodash');
|
||||
const Karma = require('../../../services/karma');
|
||||
|
||||
const thresholdsBackup = {};
|
||||
const thresholdsOverride = {
|
||||
comment: {
|
||||
RELIABLE: 1,
|
||||
UNRELIABLE: -1,
|
||||
},
|
||||
flag: {
|
||||
RELIABLE: 1,
|
||||
UNRELIABLE: -1,
|
||||
},
|
||||
};
|
||||
|
||||
describe('services.Karma', () => {
|
||||
before(() => {
|
||||
// Backup the existing thresholds.
|
||||
merge(thresholdsBackup, Karma.THRESHOLDS);
|
||||
|
||||
// Configure the thresholds to a known value.
|
||||
merge(Karma.THRESHOLDS, thresholdsOverride);
|
||||
|
||||
expect(Karma.THRESHOLDS).to.deep.equal(thresholdsOverride);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Restore the thresholds.
|
||||
merge(Karma.THRESHOLDS, thresholdsBackup);
|
||||
|
||||
expect(Karma.THRESHOLDS).to.deep.equal(thresholdsBackup);
|
||||
});
|
||||
|
||||
describe('#isReliable', () => {
|
||||
it('neutral', () => {
|
||||
expect(Karma.isReliable('comment', {})).to.be.null;
|
||||
expect(Karma.isReliable('comment', { comment: {} })).to.be.null;
|
||||
expect(Karma.isReliable('comment', { comment: { karma: 0 } })).to.be.null;
|
||||
});
|
||||
it('unreliable', () => {
|
||||
expect(Karma.isReliable('comment', { comment: { karma: -1 } })).to.be
|
||||
.false;
|
||||
expect(Karma.isReliable('comment', { comment: { karma: -2 } })).to.be
|
||||
.false;
|
||||
});
|
||||
it('reliable', () => {
|
||||
expect(Karma.isReliable('comment', { comment: { karma: 1 } })).to.be.true;
|
||||
expect(Karma.isReliable('comment', { comment: { karma: 2 } })).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('confirm_email.email_confirmation') %></title>
|
||||
<%- include ../../partials/account %>
|
||||
</head>
|
||||
<body class="confirm-email-page">
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('confirm_email.email_confirmation') %></h1>
|
||||
<p><%= t('confirm_email.click_to_confirm') %></p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="verify-email-form">
|
||||
<button type="submit"><%= t('confirm_email.confirm') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirectUri;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#verify-email-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('confirm_email.email_confirmation') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('confirm_email.email_confirmation') }}</h1>
|
||||
<p>{{ t('confirm_email.click_to_confirm') }}</p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="verify-email-form">
|
||||
<button type="submit">{{ t('confirm_email.confirm') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirectUri;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#verify-email-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('password_reset.set_new_password') %></title>
|
||||
<%- include ../../partials/account %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('password_reset.set_new_password') %></h1>
|
||||
<p><%= t('password_reset.change_password_help') %></p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="reset-password-form">
|
||||
<label for="password">
|
||||
<%= t('password_reset.new_password') %>
|
||||
<input type="password" name="password" placeholder="<%= t('password_reset.new_password') %>" />
|
||||
<small><%= t('password_reset.new_password_help') %></small>
|
||||
</label>
|
||||
<label for="confirm-password">
|
||||
<%= t('password_reset.confirm_new_password') %>
|
||||
<input type="password" name="confirm-password" placeholder="<%= t('password_reset.confirm_new_password') %>" />
|
||||
</label>
|
||||
<button type="submit"><%= t('password_reset.change_password') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password === '' || password.length < 8) {
|
||||
showError('Passwords must be at least 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
showError('New password and confirm password must match');
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirect;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('password_reset.set_new_password') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('password_reset.set_new_password') }}</h1>
|
||||
<p>{{ t('password_reset.change_password_help') }}</p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="reset-password-form">
|
||||
<label for="password">
|
||||
{{ t('password_reset.new_password') }}
|
||||
<input type="password" name="password" placeholder="{{ t('password_reset.new_password') }}" />
|
||||
<small>{{ t('password_reset.new_password_help') }}</small>
|
||||
</label>
|
||||
<label for="confirm-password">
|
||||
{{ t('password_reset.confirm_new_password') }}
|
||||
<input type="password" name="confirm-password" placeholder="{{ t('password_reset.confirm_new_password') }}" />
|
||||
</label>
|
||||
<button type="submit">{{ t('password_reset.change_password') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password === '' || password.length < 8) {
|
||||
showError('Passwords must be at least 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
showError('New password and confirm password must match');
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirect;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Talk - Coral Admin</title>
|
||||
<%- include partials/head %>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-admin/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div id="root"></div>
|
||||
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="<%= resolve('coral-admin/bundle.js') %>" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk Admin{% endblock %}
|
||||
|
||||
{% 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">
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="{{ resolve('coral-admin/bundle.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,125 +0,0 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>GraphiQL</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<%- include ../partials/dev %>
|
||||
<style>
|
||||
html, body, #graphiql {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
</style>
|
||||
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<main id="graphiql"></main>
|
||||
<script>
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
window.location.search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
// Produce a Location query string from a parameter object.
|
||||
function locationQuery(params, location) {
|
||||
return (location ? location: '') + '?' + Object.keys(params).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(params[key]);
|
||||
}).join('&');
|
||||
}
|
||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||
var graphqlParamNames = {
|
||||
query: true,
|
||||
variables: true,
|
||||
operationName: true
|
||||
};
|
||||
var otherParams = {};
|
||||
for (var k in parameters) {
|
||||
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
|
||||
otherParams[k] = parameters[k];
|
||||
}
|
||||
}
|
||||
// We don't use safe-serialize for location, because it's not client input.
|
||||
var fetchURL = locationQuery(otherParams, '<%= BASE_URL %><%= endpointURL %>');
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
var headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
try {
|
||||
let token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return fetch(fetchURL, {
|
||||
method: 'post',
|
||||
headers: headers,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared.
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
function updateURL() {
|
||||
history.replaceState(null, null, locationQuery(parameters));
|
||||
}
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
query: null,
|
||||
response: null,
|
||||
variables: null,
|
||||
operationName: null,
|
||||
}),
|
||||
document.querySelector("#graphiql")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,126 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}GraphiQL{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{# Include the base development pieces #}
|
||||
{{ super() }}
|
||||
<style type="text/css">
|
||||
html, body, #graphiql {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
</style>
|
||||
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
window.location.search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
// Produce a Location query string from a parameter object.
|
||||
function locationQuery(params, location) {
|
||||
return (location ? location: '') + '?' + Object.keys(params).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(params[key]);
|
||||
}).join('&');
|
||||
}
|
||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||
var graphqlParamNames = {
|
||||
query: true,
|
||||
variables: true,
|
||||
operationName: true
|
||||
};
|
||||
var otherParams = {};
|
||||
for (var k in parameters) {
|
||||
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
|
||||
otherParams[k] = parameters[k];
|
||||
}
|
||||
}
|
||||
// We don't use safe-serialize for location, because it's not client input.
|
||||
var fetchURL = locationQuery(otherParams, '{{ BASE_URL }}{{ endpointURL }}');
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
var headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
try {
|
||||
let token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return fetch(fetchURL, {
|
||||
method: 'post',
|
||||
headers: headers,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared.
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
function updateURL() {
|
||||
history.replaceState(null, null, locationQuery(parameters));
|
||||
}
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
query: null,
|
||||
response: null,
|
||||
variables: null,
|
||||
operationName: null,
|
||||
}),
|
||||
document.querySelector("#graphiql")
|
||||
);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<main id="graphiql"></main>
|
||||
{% endblock %}
|
||||
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<%- include partials/data %>
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/json" id="auth"><%- encodeJSONForHTML(auth) %></script>
|
||||
<script type="text/javascript" src="<%= resolve('coral-auth-callback/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% include "partials/data.njk" %}
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/json" id="auth">{{ encodeJSONForHTML(auth) }}</script>
|
||||
<script type="text/javascript" src="{{ resolve('coral-auth-callback/bundle.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,52 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="<%= title %>" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
<meta property="article:author" content="A. J. Ournalist" />
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:image" content="https://coralproject.net/images/splash-md.jpg">
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
<title><%= title %></title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<h1 class="mt-3"><%= title %></h1>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script src="<%= resolve('embed.js') %>"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '<%= BASE_URL %>',
|
||||
asset_url: '<%= asset_url ? asset_url : '' %>',
|
||||
asset_id: '<%= asset_id ? asset_id : '' %>',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta property="og:title" content="{{ title }}" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
<meta property="article:author" content="A. J. Ournalist" />
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:image" content="https://coralproject.net/images/splash-md.jpg">
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div class="container">
|
||||
<h1 class="mt-3">{{ title }}</h1>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ resolve('embed.js') }}"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '{{ BASE_URL }}',
|
||||
asset_url: '{{ asset_url }}',
|
||||
asset_id: '{{ asset_id }}',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,40 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>All Assets</title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
|
||||
<h1 class="mb-0">All Assets</h1>
|
||||
<span class="text-muted"><%= skip + 1 %> - <%= skip + assets.length %> of <%= count %> Assets</span>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<% if (skip === 0) { %><a href="<%= BASE_PATH %>dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a><% } %>
|
||||
<% assets.forEach(function (asset) { %>
|
||||
<a href="<%= BASE_PATH %>dev/assets/id/<%= asset.id %>" class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1"><%= asset.title %></h5>
|
||||
<small>Created <%= asset.created_at.toLocaleString('en-US') %></small>
|
||||
</div>
|
||||
<small><%= asset.url %></small>
|
||||
</a>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% if (count !== assets.length) { %>
|
||||
<nav aria-label="Page navigation example" class="mt-2">
|
||||
<ul class="pagination justify-content-center">
|
||||
<% let page = 1; for (let i = 0; i < count; i += limit) { %>
|
||||
<% if (i === skip) { %>
|
||||
<li class="page-item disabled"><a class="page-link" href="#"><%= page %></a></li>
|
||||
<% } else { %>
|
||||
<li class="page-item"><a class="page-link" href="?skip=<%= i %>&limit=<%= limit %>"><%= page %></a></li>
|
||||
<% } %>
|
||||
<% page++; } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}All Assets{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div class="container">
|
||||
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
|
||||
<h1 class="mb-0">All Assets</h1>
|
||||
<span class="text-muted">{{ skip + 1 }} - {{ skip + assets.length }} of {{ count }} Assets</span>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
{% if skip === 0 %}<a href="{{ BASE_PATH }}dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a>{% endif %}
|
||||
{% for asset in assets %}
|
||||
<a href="{{ BASE_PATH }}dev/assets/id/{{ asset.id }}" class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ asset.title }}</h5>
|
||||
<small>Created {{ asset.created_at.toLocaleString('en-US') }}</small>
|
||||
</div>
|
||||
<small>{{ asset.url }}</small>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if count !== assets.length %}
|
||||
<nav aria-label="Page navigation example" class="mt-2">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% for i in range(0, count, limit) %}
|
||||
{% if i === skip %}
|
||||
<li class="page-item disabled"><a class="page-link" href="#">{{ i / limit + 1 | round }}</a></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{{ BASE_PATH }}dev/assets?skip={{ i }}&limit={{ limit }}">{{ i / limit + 1 | round }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<%- include ../partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/default.css') %>">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/bundle.css') %>">
|
||||
<%- include ../partials/custom-css %>
|
||||
</head>
|
||||
<body class="embed-stream-page">
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
<script src="<%= resolve('embed/stream/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{{ resolve('embed/stream/default.css')}}" rel="stylesheet">
|
||||
<link href="{{ resolve('embed/stream/bundle.css')}}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ resolve('embed/stream/bundle.js')}}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<%- include partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-login/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="talk-login-container"></div>
|
||||
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="<%= resolve('coral-login/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk - Login{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{{ resolve('coral-login/bundle.css')}}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="talk-login-container"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="{{ resolve('coral-login/bundle.js')}}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,4 +0,0 @@
|
||||
<%- include head %>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
<%- include custom-css %>
|
||||
@@ -1 +0,0 @@
|
||||
<% if (locals.customCssUrl) { %><link href="<%= customCssUrl %>" rel="stylesheet" type="text/css"><% } %>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% if customCssUrl %}
|
||||
<link nonce="{{ nonce }}" href="{{ customCssUrl }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
@@ -1,3 +0,0 @@
|
||||
<%_ if (data != null) { _%>
|
||||
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
|
||||
<%_ } _%>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% if data %}
|
||||
<script id="data" type="application/json">{{ data | dump | safe }}</script>
|
||||
{% endif %}
|
||||
@@ -1,8 +0,0 @@
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="<%= BASE_PATH %>dev"><%= organizationName %> <span class="text-muted">Organization</span></a>
|
||||
<form class="form-inline mb-0">
|
||||
<a href="<%= BASE_PATH %>admin" class="btn btn-outline-primary mr-2"><i class="fa fa-lock" aria-hidden="true"></i> Admin</a>
|
||||
<a href="<%= BASE_PATH %>api/v1/graph/iql" class="btn btn-outline-secondary mr-2 graphiql"><i class="fa fa-terminal" aria-hidden="true"></i> Graph<em>i</em>QL</a>
|
||||
<a href="<%= BASE_PATH %>dev/assets" class="btn btn-outline-secondary"><i class="fa fa-list" aria-hidden="true"></i> All Assets</a>
|
||||
</form>
|
||||
</nav>
|
||||
@@ -0,0 +1,14 @@
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="{{ STATIC_URL }}public/img/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{{ STATIC_URL }}public/img/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ STATIC_URL }}public/img/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_URL }}public/img/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="{{ STATIC_URL }}public/img/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_URL }}public/img/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="{{ STATIC_URL }}public/img/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_URL }}public/img/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_URL }}public/img/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_URL }}public/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{{ STATIC_URL }}public/img/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_URL }}public/img/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{ STATIC_URL }}public/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
@@ -1,23 +0,0 @@
|
||||
<meta charset="utf-8">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= STATIC_URL %>public/img/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= STATIC_URL %>public/img/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= STATIC_URL %>public/img/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= STATIC_URL %>public/img/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= STATIC_URL %>public/img/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= STATIC_URL %>public/img/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= STATIC_URL %>public/img/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= STATIC_URL %>public/img/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= STATIC_URL %>public/img/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= STATIC_URL %>public/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="<%= STATIC_URL %>public/img/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= STATIC_URL %>public/img/favicon-16x16.png">
|
||||
<link rel="manifest" href="<%= STATIC_URL %>public/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
|
||||
<%- include data %>
|
||||
|
||||
<base href="<%= BASE_URL %>"/>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user