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:
okbel
2018-05-24 13:09:13 -03:00
105 changed files with 1478 additions and 1173 deletions
@@ -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)}>
&ge; {reliable}
</span>
</li>
<li>
<span>Neutral</span>{' '}
<span className={cn(styles.label, styles.neutral)}>
&lt; {reliable}, &gt; {unreliable}
</span>
</li> */}
<li>
<span>{t('user_detail.unreliable')}</span>{' '}
<span className={cn(styles.label, styles.unreliable)}>
&le; {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;
}
+22 -27
View File
@@ -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 -9
View File
@@ -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">
+57 -74
View File
@@ -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;