mirror of
https://github.com/wassname/talk.git
synced 2026-08-03 13:20:59 +08:00
Merge branch 'master' into master
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
:global {
|
||||
html, body, #root, #root > div {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #FAFAFA;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ToastContainer from './ToastContainer';
|
||||
import './App.css';
|
||||
import 'material-design-lite';
|
||||
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
@@ -14,7 +14,8 @@ const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className
|
||||
className,
|
||||
'talk-admin-approve-button'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
|
||||
@@ -19,7 +19,7 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
handleMessageChange = e => {
|
||||
const { value: message } = e;
|
||||
const { target: { value: message } } = e;
|
||||
this.setState({ message });
|
||||
};
|
||||
|
||||
@@ -30,6 +30,12 @@ class BanUserDialog extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
this.props.onPerform({
|
||||
message: this.state.message,
|
||||
});
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const { onCancel, username, info } = this.props;
|
||||
|
||||
@@ -63,7 +69,7 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const { onCancel, onPerform } = this.props;
|
||||
const { onCancel } = this.props;
|
||||
const { message } = this.state;
|
||||
|
||||
return (
|
||||
@@ -95,7 +101,7 @@ class BanUserDialog extends React.Component {
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.send')}
|
||||
|
||||
@@ -29,7 +29,7 @@ const CommentAnimatedEdit = ({ children, body }) => {
|
||||
|
||||
CommentAnimatedEdit.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string,
|
||||
body: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CommentAnimatedEdit;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.tombstone {
|
||||
background-color: #f0f0f0;
|
||||
padding: 1em;
|
||||
color: #1a212f;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import styles from './CommentDeletedTombstone.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CommentDeletedTombstone = () => (
|
||||
<div className={styles.tombstone}>{t('framework.comment_is_deleted')}</div>
|
||||
);
|
||||
|
||||
export default CommentDeletedTombstone;
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.external {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.separator h5 {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.slot > * {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './External.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
const External = ({ slot }) => (
|
||||
<IfSlotIsNotEmpty slot={slot}>
|
||||
<div>
|
||||
<div className={styles.external}>
|
||||
<Slot fill={slot} className={styles.slot} />
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h5>Or</h5>
|
||||
</div>
|
||||
</div>
|
||||
</IfSlotIsNotEmpty>
|
||||
);
|
||||
|
||||
External.propTypes = {
|
||||
slot: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default External;
|
||||
@@ -7,7 +7,6 @@ import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Logo } from './Logo';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
@@ -32,7 +31,6 @@ const CoralHeader = ({
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
<ModerationIndicator root={root} data={data} />
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
background-color: #E46D59;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -4px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
.karmaTooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 2px 4px 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #0D5B8F;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0);
|
||||
|
||||
> i {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background-color: white;
|
||||
border: solid 1px #999;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
left: -100px;
|
||||
width: 150px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.menu::before{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu::after{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -19px;
|
||||
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;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.strongItem {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.descriptionItem {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #2B7EB5;
|
||||
text-decoration: underline;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'coral-ui';
|
||||
import styles from './KarmaTooltip.css';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const initialState = { menuVisible: false };
|
||||
|
||||
class KarmaTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
thresholds: PropTypes.shape({
|
||||
reliable: PropTypes.number.isRequired,
|
||||
unreliable: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
state = initialState;
|
||||
|
||||
toogleMenu = () => {
|
||||
this.setState({ menuVisible: !this.state.menuVisible });
|
||||
};
|
||||
|
||||
hideMenu = () => {
|
||||
this.setState({ menuVisible: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { thresholds: { unreliable } } = this.props;
|
||||
const { menuVisible } = this.state;
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.hideMenu}>
|
||||
<div className={cn(styles.karmaTooltip, 'talk-admin-karma-tooltip')}>
|
||||
<span
|
||||
onClick={this.toogleMenu}
|
||||
className={cn(styles.icon, 'talk-admin-karma-tooltip-icon')}
|
||||
>
|
||||
<Icon name="info" />
|
||||
</span>
|
||||
|
||||
{menuVisible && (
|
||||
<div className={cn(styles.menu, 'talk-admin-karma-tooltip-menu')}>
|
||||
<strong>{t('user_detail.user_karma_score')}</strong>
|
||||
<ul>
|
||||
{/* NOTE: we may display this data in the future, keeping around for that eventuality */}
|
||||
{/* <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')}
|
||||
target="_blank"
|
||||
>
|
||||
{t('user_detail.learn_more')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default KarmaTooltip;
|
||||
@@ -14,7 +14,8 @@ const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className
|
||||
className,
|
||||
'talk-admin-reject-button'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
|
||||
@@ -4,6 +4,7 @@ import styles from './SignIn.css';
|
||||
import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
import External from './External';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
@@ -33,48 +34,55 @@ class SignIn extends React.Component {
|
||||
render() {
|
||||
const { email, password, errorMessage, requireRecaptcha } = this.props;
|
||||
return (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
<div className="talk-admin-login-sign-in">
|
||||
<External slot="authExternalAdminSignIn" />
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(
|
||||
styles.signInButton,
|
||||
'talk-admin-login-sign-in-button'
|
||||
)}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
{/* TODO: translate */}
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,44 +35,49 @@
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.karmaStat {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.statItem,
|
||||
.statReportResult {
|
||||
.statItem, .statReportResult, .statKarmaResult {
|
||||
padding: 3px 5px;
|
||||
background-color: #D8D8D8;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.4px;
|
||||
min-width: 60px;
|
||||
min-width: 25px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.statResult {
|
||||
font-size: 1.5em;
|
||||
padding: 5px 0;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statReportResult {
|
||||
.statReportResult, .statKarmaResult {
|
||||
color: white;
|
||||
margin: 5px 0;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statReportResult.reliable {
|
||||
background-color: #749C48;
|
||||
.statReportResult.reliable, .statKarmaResult.good {
|
||||
background-color: #03AB61;
|
||||
}
|
||||
|
||||
.statReportResult.neutral {
|
||||
.statReportResult.neutral, .statKarmaResult.neutral {
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
.statReportResult.unreliable {
|
||||
.statReportResult.unreliable, .statKarmaResult.bad {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
@@ -143,3 +148,7 @@
|
||||
border-color: #E45241;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.userDetailItem {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
@@ -6,11 +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,
|
||||
} 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 {
|
||||
@@ -25,6 +21,7 @@ import {
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import KarmaTooltip from './KarmaTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
@@ -79,7 +76,13 @@ class UserDetail extends React.Component {
|
||||
renderLoaded() {
|
||||
const {
|
||||
root,
|
||||
root: { me, user, totalComments, rejectedComments },
|
||||
root: {
|
||||
me,
|
||||
user,
|
||||
totalComments,
|
||||
rejectedComments,
|
||||
settings: { karmaThresholds },
|
||||
},
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
toggleSelect,
|
||||
@@ -177,7 +180,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')}:
|
||||
@@ -185,11 +188,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
|
||||
@@ -216,18 +232,21 @@ 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 className={cn(styles.stat, styles.karmaStat)}>
|
||||
<div>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.karma')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statKarmaResult,
|
||||
styles[getKarma(user.reliable.commenter)]
|
||||
)}
|
||||
>
|
||||
{user.reliable.commenterKarma}
|
||||
</span>
|
||||
</div>
|
||||
<KarmaTooltip thresholds={karmaThresholds.comment} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
import CommentLabels from '../containers/CommentLabels';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from './CommentDeletedTombstone';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -43,6 +44,19 @@ class UserDetailComment extends React.Component {
|
||||
body: comment.body,
|
||||
};
|
||||
|
||||
if (!comment.body) {
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {
|
||||
[styles.rootSelected]: selected,
|
||||
})}
|
||||
>
|
||||
<CommentDeletedTombstone />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -152,7 +166,7 @@ UserDetailComment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
body: PropTypes.string,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
|
||||
@@ -12,7 +12,7 @@ import { compose } from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
banUser = async () => {
|
||||
banUser = async ({ message }) => {
|
||||
const {
|
||||
userId,
|
||||
commentId,
|
||||
@@ -21,7 +21,7 @@ class BanUserDialogContainer extends Component {
|
||||
setCommentStatus,
|
||||
hideBanUserDialog,
|
||||
} = this.props;
|
||||
await banUser({ id: userId, message: '' });
|
||||
await banUser({ id: userId, message });
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
|
||||
@@ -2,21 +2,20 @@ import { gql } from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/Header';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
// TODO: eventually we will readd modqueue counts
|
||||
// import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
export default withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Header($nullID: ID) {
|
||||
...${getDefinitionName(ModerationIndicator.fragments.root)}
|
||||
query TalkAdmin_Header {
|
||||
...${getDefinitionName(CommunityIndicator.fragments.root)}
|
||||
}
|
||||
${ModerationIndicator.fragments.root}
|
||||
${CommunityIndicator.fragments.root}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
variables: { nullID: null },
|
||||
// variables: { nullID: null },
|
||||
},
|
||||
}
|
||||
)(Header);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSignIn } from 'coral-framework/hocs';
|
||||
import { withSignIn, withPopupAuthHandler } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import SignIn from '../components/SignIn';
|
||||
|
||||
@@ -55,4 +55,4 @@ SignInContainer.propTypes = {
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn)(SignInContainer);
|
||||
export default compose(withSignIn, withPopupAuthHandler)(SignInContainer);
|
||||
|
||||
@@ -179,12 +179,14 @@ export const withUserDetailQuery = withQuery(
|
||||
id
|
||||
username
|
||||
created_at
|
||||
email
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
reliable {
|
||||
flagger
|
||||
commenter
|
||||
commenterKarma
|
||||
}
|
||||
state {
|
||||
status {
|
||||
@@ -225,6 +227,14 @@ export const withUserDetailQuery = withQuery(
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
settings {
|
||||
karmaThresholds {
|
||||
comment {
|
||||
reliable
|
||||
unreliable
|
||||
}
|
||||
}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
|
||||
@@ -24,6 +24,25 @@ const userRoleFragment = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
if (karma <= unreliable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserRole: ({ variables: { id, role } }) => ({
|
||||
@@ -156,7 +175,9 @@ export default {
|
||||
}
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
|
||||
nodes: {
|
||||
$apply: nodes => nodes.filter(node => node.id !== id),
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
@@ -185,7 +206,9 @@ export default {
|
||||
const updated = update(prev, {
|
||||
...decrement,
|
||||
flaggedUsers: {
|
||||
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
|
||||
nodes: {
|
||||
$apply: nodes => nodes.filter(node => node.id !== id),
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
@@ -295,12 +318,38 @@ export default {
|
||||
updateQueries: {
|
||||
CoralAdmin_UserDetail: prev => {
|
||||
const increment = {
|
||||
user: {
|
||||
reliable: {
|
||||
commenter: {
|
||||
$set: calculateReliability(
|
||||
prev.user.reliable.commenterKarma - 1,
|
||||
prev.settings.karmaThresholds.comment
|
||||
),
|
||||
},
|
||||
commenterKarma: {
|
||||
$apply: count => count - 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
rejectedComments: {
|
||||
$apply: count => (count < prev.totalComments ? count + 1 : count),
|
||||
},
|
||||
};
|
||||
|
||||
const decrement = {
|
||||
user: {
|
||||
reliable: {
|
||||
commenter: {
|
||||
$set: calculateReliability(
|
||||
prev.user.reliable.commenterKarma + 1,
|
||||
prev.settings.karmaThresholds.comment
|
||||
),
|
||||
},
|
||||
commenterKarma: {
|
||||
$apply: count => count + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
rejectedComments: {
|
||||
$apply: count => (count > 0 ? count - 1 : 0),
|
||||
},
|
||||
|
||||
@@ -133,6 +133,7 @@ th.header:nth-child(2), th.header:nth-child(3) {
|
||||
|
||||
.roleDropdown {
|
||||
width: 150px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.roleOption {
|
||||
|
||||
@@ -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">
|
||||
@@ -200,7 +202,7 @@ class People extends React.Component {
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
className={cn(
|
||||
toggleClassName={cn(
|
||||
'talk-admin-community-people-dd-role',
|
||||
styles.roleDropdown
|
||||
)}
|
||||
|
||||
@@ -82,6 +82,20 @@ class StreamSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateDisableCommenting = () => {
|
||||
const updater = {
|
||||
disableCommenting: {
|
||||
$set: !this.props.settings.disableCommenting,
|
||||
},
|
||||
};
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateDisableCommentingMessage = value => {
|
||||
const updater = { disableCommentingMessage: { $set: value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateAutoClose = () => {
|
||||
const updater = {
|
||||
autoCloseStream: { $set: !this.props.settings.autoCloseStream },
|
||||
@@ -192,6 +206,25 @@ class StreamSettings extends React.Component {
|
||||
|
||||
{t('configure.edit_comment_timeframe_text_post')}
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.disableCommenting}
|
||||
onCheckbox={this.updateDisableCommenting}
|
||||
title={t('configure.disable_commenting_title')}
|
||||
>
|
||||
<p>{t('configure.disable_commenting_desc')}</p>
|
||||
<div
|
||||
className={cn(
|
||||
styles.configSettingDisableCommenting,
|
||||
settings.disableCommenting ? null : styles.hidden
|
||||
)}
|
||||
>
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={this.updateDisableCommentingMessage}
|
||||
value={settings.disableCommentingMessage}
|
||||
/>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.autoCloseStream}
|
||||
onCheckbox={this.updateAutoClose}
|
||||
|
||||
@@ -39,6 +39,8 @@ export default compose(
|
||||
autoCloseStream
|
||||
closedTimeout
|
||||
closedMessage
|
||||
disableCommenting
|
||||
disableCommentingMessage
|
||||
${getSlotFragmentSpreads(slots, 'settings')}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.root {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
@@ -13,7 +12,6 @@
|
||||
margin-top: 13px;
|
||||
min-height: 0;
|
||||
outline: 0;
|
||||
|
||||
/*
|
||||
Fix rendering issues in Safari by promoting this
|
||||
into its own layer.
|
||||
@@ -21,7 +19,6 @@
|
||||
https://www.pivotaltracker.com/story/show/151142211
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -39,7 +36,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.author {
|
||||
@@ -74,7 +70,7 @@
|
||||
max-width: 500px;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.created {
|
||||
@@ -85,13 +81,16 @@
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.moderateArticle {
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
max-width: 500px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #063b9a;
|
||||
@@ -99,17 +98,15 @@
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
margin-left: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 0px;
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,12 +131,10 @@
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
@@ -168,7 +163,6 @@
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 14px;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -177,7 +171,6 @@
|
||||
@media (--big-viewport) {
|
||||
.root {
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
@@ -186,7 +179,6 @@
|
||||
|
||||
.commentContent {
|
||||
display: flex;
|
||||
|
||||
blockquote {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
@@ -13,6 +13,7 @@ import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from '../../../components/CommentDeletedTombstone';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -75,6 +76,27 @@ class Comment extends React.Component {
|
||||
asset: comment.asset,
|
||||
};
|
||||
|
||||
if (!comment.body) {
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
className,
|
||||
'mdl-card',
|
||||
selectionStateCSS,
|
||||
styles.root,
|
||||
{ [styles.selected]: selected, [styles.dangling]: dangling },
|
||||
'talk-admin-moderate-comment',
|
||||
styles.deleted
|
||||
)}
|
||||
id={`comment_${comment.id}`}
|
||||
ref={this.handleRef}
|
||||
>
|
||||
<CommentDeletedTombstone />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -200,7 +222,7 @@ Comment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
body: PropTypes.string,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
|
||||
@@ -63,10 +63,6 @@ class Moderation extends Component {
|
||||
this.props.toggleStorySearch(true);
|
||||
};
|
||||
|
||||
getActiveTabCount = (props = this.props) => {
|
||||
return props.root[`${props.activeTab}Count`];
|
||||
};
|
||||
|
||||
moderate = accept => {
|
||||
const {
|
||||
acceptComment,
|
||||
@@ -139,12 +135,14 @@ class Moderation extends Component {
|
||||
|
||||
const comments = root[activeTab];
|
||||
|
||||
const activeTabCount = this.getActiveTabCount();
|
||||
const menuItems = Object.keys(queueConfig).map(queue => ({
|
||||
key: queue,
|
||||
name: queueConfig[queue].name,
|
||||
icon: queueConfig[queue].icon,
|
||||
count: root[`${queue}Count`],
|
||||
indicator:
|
||||
['premod', 'reported'].includes(queue) && root[queue].nodes.length > 0,
|
||||
// TODO: Eventually we'll reintroduce counting
|
||||
// count: root[`${props.queue}Count`]
|
||||
}));
|
||||
|
||||
const slotPassthrough = {
|
||||
@@ -189,7 +187,6 @@ class Moderation extends Component {
|
||||
loadMore={this.loadMore}
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
import Indicator from '../../../components/Indicator';
|
||||
import styles from './ModerationMenu.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
import { Link } from 'react-router';
|
||||
@@ -24,6 +24,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
|
||||
>
|
||||
{items.map(queue => (
|
||||
<Link
|
||||
id={`talk-admin-moderate-tab-${queue.key}`}
|
||||
key={queue.key}
|
||||
to={getModPath(queue.key, asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {
|
||||
@@ -32,7 +33,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name}{' '}
|
||||
<CountBadge count={queue.count} />
|
||||
{queue.indicator && <Indicator />}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'react-virtualized';
|
||||
import throttle from 'lodash/throttle';
|
||||
import key from 'keymaster';
|
||||
import cn from 'classnames';
|
||||
|
||||
const hasComment = (nodes, id) => nodes.some(node => node.id === id);
|
||||
|
||||
@@ -204,7 +205,7 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
const { commentCount, selectedCommentId } = this.props;
|
||||
const { selectedCommentId, hasNextPage } = this.props;
|
||||
|
||||
const switchedToMultiMode = prev.singleView && !this.props.singleView;
|
||||
const switchedMode = prev.singleView !== this.props.singleView;
|
||||
@@ -212,7 +213,6 @@ class ModerationQueue extends React.Component {
|
||||
prev.selectedCommentId !== selectedCommentId && selectedCommentId;
|
||||
const moderatedLastComment =
|
||||
prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0;
|
||||
const hasMoreComment = commentCount > 0;
|
||||
|
||||
if (switchedToMultiMode) {
|
||||
// Reflow virtual list.
|
||||
@@ -223,7 +223,7 @@ class ModerationQueue extends React.Component {
|
||||
this.scrollToSelectedComment();
|
||||
}
|
||||
|
||||
if (moderatedLastComment && hasMoreComment) {
|
||||
if (moderatedLastComment && hasNextPage) {
|
||||
this.props.loadMore();
|
||||
}
|
||||
}
|
||||
@@ -240,10 +240,7 @@ class ModerationQueue extends React.Component {
|
||||
const index = view.findIndex(
|
||||
({ id }) => id === this.props.selectedCommentId
|
||||
);
|
||||
if (
|
||||
index === view.length - 1 &&
|
||||
this.getCommentCountWithoutDagling() !== this.props.commentCount
|
||||
) {
|
||||
if (index === view.length - 1 && this.props.hasNextPage) {
|
||||
await this.props.loadMore();
|
||||
this.selectDown();
|
||||
return;
|
||||
@@ -384,6 +381,11 @@ class ModerationQueue extends React.Component {
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
const rootClassName = cn(
|
||||
styles.root,
|
||||
`talk-admin-moderate-queue-${this.props.activeTab}`
|
||||
);
|
||||
|
||||
if (comments.length === 0) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -405,7 +407,7 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
const comment = comments[index];
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={rootClassName}>
|
||||
<Comment
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
@@ -427,7 +429,7 @@ class ModerationQueue extends React.Component {
|
||||
const view = this.state.view;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={rootClassName}>
|
||||
<ViewMore
|
||||
viewMore={() => this.viewNewComments()}
|
||||
count={comments.length - view.length}
|
||||
@@ -467,7 +469,6 @@ ModerationQueue.propTypes = {
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
singleView: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
|
||||
@@ -314,11 +314,11 @@ class ModerationContainer extends Component {
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
|
||||
if (premodEnabled && root.newCount === 0) {
|
||||
if (premodEnabled && root.new.nodes.length === 0) {
|
||||
delete currentQueueConfig.new;
|
||||
}
|
||||
|
||||
if (!premodEnabled && root.premodCount === 0) {
|
||||
if (!premodEnabled && root.premod.nodes.length === 0) {
|
||||
delete currentQueueConfig.premod;
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags, excludeDeleted: true}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
@@ -432,6 +432,7 @@ const withModQueueQuery = withQuery(
|
||||
${Object.keys(queueConfig).map(
|
||||
queue => `
|
||||
${queue}: comments(query: {
|
||||
excludeDeleted: true,
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
@@ -455,9 +456,14 @@ const withModQueueQuery = withQuery(
|
||||
}
|
||||
`
|
||||
)}
|
||||
${Object.keys(queueConfig).map(
|
||||
${
|
||||
''
|
||||
/*
|
||||
TODO: eventually we'll reintroduce counting..
|
||||
Object.keys(queueConfig).map(
|
||||
queue => `
|
||||
${queue}Count: commentCount(query: {
|
||||
excludeDeleted: true,
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
@@ -476,7 +482,8 @@ const withModQueueQuery = withQuery(
|
||||
asset_id: $asset_id,
|
||||
})
|
||||
`
|
||||
)}
|
||||
)*/
|
||||
}
|
||||
asset(id: $asset_id) @skip(if: $allAssets) {
|
||||
id
|
||||
title
|
||||
|
||||
@@ -92,9 +92,6 @@
|
||||
|
||||
.statusDropdown {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.statusDropdownOption {
|
||||
min-width: 100px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,20 +22,12 @@ class Stories extends Component {
|
||||
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
|
||||
return (
|
||||
<Dropdown
|
||||
className={styles.statusDropdown}
|
||||
toggleClassName={styles.statusDropdown}
|
||||
value={closed}
|
||||
onChange={value => this.props.onStatusChange(value, id)}
|
||||
>
|
||||
<Option
|
||||
value={false}
|
||||
label={t('streams.open')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option
|
||||
value={true}
|
||||
label={t('streams.closed')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option value={false} label={t('streams.open')} />
|
||||
<Option value={true} label={t('streams.closed')} />
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,32 +3,36 @@ import { getStaticConfiguration } from 'coral-framework/services/staticConfigura
|
||||
import { createPostMessage } from 'coral-framework/services/postMessage';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const staticConfig = getStaticConfiguration();
|
||||
const { STATIC_ORIGIN: origin } = staticConfig;
|
||||
const postMessage = createPostMessage(origin);
|
||||
const staticConfig = getStaticConfiguration();
|
||||
const { STATIC_ORIGIN: origin } = staticConfig;
|
||||
const postMessage = createPostMessage(origin);
|
||||
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
const auth = document.getElementById('auth');
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = auth.innerText;
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
const auth = document.getElementById('auth');
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = auth.innerText;
|
||||
|
||||
// Auth state is contained within the node.
|
||||
const { err, data } = JSON.parse(doc.body.textContent);
|
||||
if (err) {
|
||||
// TODO: send back the error message.
|
||||
console.error(err);
|
||||
// Auth state is contained within the node.
|
||||
const { err, data } = JSON.parse(doc.body.textContent);
|
||||
if (err) {
|
||||
const errDiv = document.createElement('div');
|
||||
if (err.message) {
|
||||
errDiv.innerText = `${err.name}: ${err.message}`;
|
||||
} else {
|
||||
// The data will contain a user and a token.
|
||||
const { user, token } = data;
|
||||
|
||||
// Send the state back.
|
||||
postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token });
|
||||
errDiv.innerText = JSON.stringify(err);
|
||||
}
|
||||
} finally {
|
||||
// Always close the window.
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 50);
|
||||
document.body.appendChild(errDiv);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// The data will contain a user and a token.
|
||||
const { user, token } = data;
|
||||
|
||||
// Send the state back.
|
||||
postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token });
|
||||
|
||||
// Close the window when all went well.
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,14 @@ import reducers from './reducers';
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
|
||||
// Resolves touch handling issues encountered on IOS Safari under certain
|
||||
// circumstances. It may be related to issues reported here:
|
||||
//
|
||||
// https://stackoverflow.com/questions/12363742/touchstart-event-is-not-firing-inside-iframe-ios-6
|
||||
//
|
||||
// Further details: https://www.pivotaltracker.com/story/show/157794038
|
||||
document.body.addEventListener('touchstart', () => {});
|
||||
|
||||
async function main() {
|
||||
const context = await createContext({
|
||||
reducers,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.commentSummary {
|
||||
|
||||
@@ -11,16 +11,6 @@ import { getTotalReactionsCount } from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
goToStory = () => {
|
||||
this.props.navigate(this.props.comment.asset.url);
|
||||
};
|
||||
|
||||
goToConversation = () => {
|
||||
this.props.navigate(
|
||||
`${this.props.comment.asset.url}?commentId=${this.props.comment.id}`
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { comment, root } = this.props;
|
||||
const reactionCount = getTotalReactionsCount(comment.action_summaries);
|
||||
@@ -76,8 +66,8 @@ class Comment extends React.Component {
|
||||
<div className="my-comment-asset">
|
||||
<a
|
||||
className={cn(styles.assetURL, 'my-comment-anchor')}
|
||||
href="#"
|
||||
onClick={this.goToStory}
|
||||
href={this.props.comment.asset.url}
|
||||
target="_parent"
|
||||
>
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
@@ -87,7 +77,13 @@ class Comment extends React.Component {
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
<li>
|
||||
<a onClick={this.goToConversation} className={styles.viewLink}>
|
||||
<a
|
||||
className={styles.viewLink}
|
||||
href={`${this.props.comment.asset.url}?commentId=${
|
||||
this.props.comment.id
|
||||
}`}
|
||||
target="_parent"
|
||||
>
|
||||
<Icon name="open_in_new" className={styles.iconView} />
|
||||
{t('view_conversation')}
|
||||
</a>
|
||||
|
||||
@@ -214,7 +214,7 @@ AllCommentsPane.propTypes = {
|
||||
asset: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
postFlag: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
postDontAgree: PropTypes.func.isRequired,
|
||||
loadNewReplies: PropTypes.func,
|
||||
deleteAction: PropTypes.func,
|
||||
showSignInDialog: PropTypes.func,
|
||||
|
||||
@@ -184,7 +184,7 @@ export default class Comment extends React.Component {
|
||||
maxCharCount: PropTypes.number,
|
||||
root: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
postDontAgree: PropTypes.func.isRequired,
|
||||
animateEnter: PropTypes.bool,
|
||||
commentClassNames: PropTypes.array,
|
||||
comment: PropTypes.object.isRequired,
|
||||
@@ -410,6 +410,7 @@ export default class Comment extends React.Component {
|
||||
charCountEnable,
|
||||
showSignInDialog,
|
||||
liveUpdates,
|
||||
postDontAgree,
|
||||
emit,
|
||||
} = this.props;
|
||||
return (
|
||||
@@ -440,6 +441,7 @@ export default class Comment extends React.Component {
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
emit={emit}
|
||||
postDontAgree={postDontAgree}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -743,10 +745,21 @@ export default class Comment extends React.Component {
|
||||
|
||||
const id = `c_${comment.id}`;
|
||||
|
||||
// props that are passed down the slots.
|
||||
const slotPassthrough = {
|
||||
action: 'deleted',
|
||||
comment,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={rootClassName} id={id}>
|
||||
{isCommentDeleted(comment) ? (
|
||||
<CommentTombstone action="deleted" />
|
||||
<Slot
|
||||
fill="commentTombstone"
|
||||
defaultComponent={CommentTombstone}
|
||||
size={1}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{this.renderComment()}
|
||||
|
||||
@@ -39,6 +39,7 @@ class CommentTombstone extends React.Component {
|
||||
|
||||
CommentTombstone.propTypes = {
|
||||
action: PropTypes.string,
|
||||
comment: PropTypes.object,
|
||||
onUndo: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import StreamError from './StreamError';
|
||||
import Comment from '../containers/Comment';
|
||||
import BannedAccount from '../../../components/BannedAccount';
|
||||
import ChangeUsername from '../containers/ChangeUsername';
|
||||
import Markdown from 'coral-framework/components/Markdown';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import InfoBox from './InfoBox';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
@@ -181,7 +182,9 @@ class Stream extends React.Component {
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
disableReply={asset.isClosed}
|
||||
disableReply={
|
||||
asset.isClosed || asset.settings.disableCommenting
|
||||
}
|
||||
postComment={postComment}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
@@ -215,7 +218,7 @@ class Stream extends React.Component {
|
||||
currentUser,
|
||||
} = this.props;
|
||||
const { keepCommentBox } = this.state;
|
||||
const open = !asset.isClosed;
|
||||
const open = !(asset.isClosed || asset.settings.disableCommenting);
|
||||
|
||||
const banned = get(currentUser, 'status.banned.status');
|
||||
const suspensionUntil = get(currentUser, 'status.suspension.until');
|
||||
@@ -293,7 +296,13 @@ class Stream extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
<div>
|
||||
{asset.isClosed ? (
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
) : (
|
||||
<Markdown content={asset.settings.disableCommentingMessage} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Slot fill="stream" passthrough={slotPassthrough} />
|
||||
|
||||
@@ -24,6 +24,7 @@ const slots = [
|
||||
'commentAuthorName',
|
||||
'commentAuthorTags',
|
||||
'commentTimestamp',
|
||||
'commentTombstone',
|
||||
'commentContent',
|
||||
];
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ StreamContainer.propTypes = {
|
||||
commentClassNames: PropTypes.array,
|
||||
setActiveStreamTab: PropTypes.func,
|
||||
postFlag: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
postDontAgree: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func,
|
||||
showSignInDialog: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
@@ -434,6 +434,8 @@ const fragments = {
|
||||
questionBoxIcon
|
||||
closedTimeout
|
||||
closedMessage
|
||||
disableCommenting
|
||||
disableCommentingMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
|
||||
@@ -37,7 +37,8 @@ export default class Popup extends Component {
|
||||
this.onBlur();
|
||||
};
|
||||
|
||||
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
|
||||
// Use `onunload` instead of `onbeforeunload` which is not supported in iOS
|
||||
// Safari.
|
||||
this.ref.onunload = () => {
|
||||
this.onUnload();
|
||||
|
||||
@@ -46,10 +47,15 @@ export default class Popup extends Component {
|
||||
}
|
||||
|
||||
this.resetCallbackInterval = setInterval(() => {
|
||||
if (this.ref && this.ref.onload === null) {
|
||||
clearInterval(this.resetCallbackInterval);
|
||||
this.resetCallbackInterval = null;
|
||||
this.setCallbacks();
|
||||
try {
|
||||
if (this.ref && this.ref.onload === null) {
|
||||
clearInterval(this.resetCallbackInterval);
|
||||
this.resetCallbackInterval = null;
|
||||
this.setCallbacks();
|
||||
}
|
||||
} catch (err) {
|
||||
// We could be getting a security exception here if the login page
|
||||
// gets redirected to another domain to authenticate.
|
||||
}
|
||||
}, 50);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
const PropTypes = require('prop-types');
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApolloProvider } from 'react-apollo';
|
||||
|
||||
class TalkProvider extends React.Component {
|
||||
|
||||
@@ -116,7 +116,7 @@ export const withRemoveTag = withMutation(
|
||||
asset_id: assetId,
|
||||
item_type: itemType,
|
||||
},
|
||||
o3timisticResponse: {
|
||||
optimisticResponse: {
|
||||
removeTag: {
|
||||
__typename: 'ModifyTagResponse',
|
||||
errors: null,
|
||||
|
||||
+12
-2
@@ -1,9 +1,9 @@
|
||||
/* global __webpack_public_path__ */ // eslint-disable-line no-unused-vars
|
||||
/* global __webpack_public_path__, __webpack_nonce__ */ // eslint-disable-line no-unused-vars
|
||||
|
||||
import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration';
|
||||
|
||||
// Load the static url from the static configuration.
|
||||
const { STATIC_URL } = getStaticConfiguration();
|
||||
const { STATIC_URL, SCRIPT_NONCE } = getStaticConfiguration();
|
||||
|
||||
// Update the static url for the imported public path so dynamically imported
|
||||
// chunks will use the correct path as defined by the process.env.STATIC_URL
|
||||
@@ -14,3 +14,13 @@ const { STATIC_URL } = getStaticConfiguration();
|
||||
// https://webpack.js.org/configuration/output/#output-publicpath
|
||||
//
|
||||
__webpack_public_path__ = STATIC_URL + 'static/';
|
||||
|
||||
// All dynamically included scripts that support nonce's will add this to their
|
||||
// script tags.
|
||||
//
|
||||
// The __webpack_nonce__ can be referenced: https://webpack.js.org/guides/csp/
|
||||
//
|
||||
// Pending issues:
|
||||
// - https://github.com/webpack-contrib/style-loader/pull/319
|
||||
//
|
||||
__webpack_nonce__ = SCRIPT_NONCE;
|
||||
@@ -59,6 +59,7 @@ const withSetUsername = hoistStatics(WrappedComponent => {
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
this.setState(changeSet);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,22 @@ 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 = {
|
||||
// the list of languages that are whitelisted. If false, all languages that are
|
||||
// supported by Talk will be enabled.
|
||||
const whitelistedLanguages =
|
||||
process.env.TALK_WHITELISTED_LANGUAGES &&
|
||||
process.env.TALK_WHITELISTED_LANGUAGES.split(',').map(l => l.trim());
|
||||
|
||||
// The default language. If the whitelisted languages is specified and the
|
||||
// default language is not in that list, then the first language in the
|
||||
// whitelisted list will be used as the default.
|
||||
export const defaultLocale = whitelistedLanguages
|
||||
? !whitelistedLanguages.includes(process.env.TALK_DEFAULT_LANG)
|
||||
? whitelistedLanguages[0]
|
||||
: process.env.TALK_DEFAULT_LANG
|
||||
: process.env.TALK_DEFAULT_LANG;
|
||||
|
||||
export const translations = {
|
||||
...ar,
|
||||
...en,
|
||||
...da,
|
||||
@@ -49,84 +67,66 @@ 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,
|
||||
whitelistedLanguages || 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 +140,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;
|
||||
|
||||
@@ -56,10 +56,10 @@ export function createPostMessage(origin, scope = 'client') {
|
||||
// Send the message.
|
||||
target.postMessage(msg, origin);
|
||||
},
|
||||
subscribe: (handler, target = window) => {
|
||||
subscribe(handler, target = window) {
|
||||
// If this handler is already attached to the target, detach it.
|
||||
if (has(listeners, [target, handler])) {
|
||||
this.unsubscribeFromMessages(handler, target);
|
||||
this.unsubscribe(handler, target);
|
||||
}
|
||||
|
||||
// Wrap the listener with a origin check.
|
||||
@@ -71,7 +71,7 @@ export function createPostMessage(origin, scope = 'client') {
|
||||
// Attach the listener to the target.
|
||||
target.addEventListener('message', listener);
|
||||
},
|
||||
unsubscribe: (handler, target = window) => {
|
||||
unsubscribe(handler, target = window) {
|
||||
if (!has(listeners, [target, handler])) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
border: none;
|
||||
touch-action: manipulation;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -273,3 +273,23 @@ export function translateError(error) {
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* handlePopupAuth will optionally open a popup with the requested uri if the
|
||||
* window is not already a popup.
|
||||
*
|
||||
* @param {String} uri the url to open the window? to
|
||||
* @param {String} title the title of the new window? to open
|
||||
* @param {String} features the features to use when opening a window?
|
||||
*/
|
||||
export function handlePopupAuth(
|
||||
uri,
|
||||
title = 'Login', // TODO: translate
|
||||
features = 'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
|
||||
) {
|
||||
if (window.opener) {
|
||||
window.location = uri;
|
||||
} else {
|
||||
window.open(uri, title, features);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,3 +49,18 @@ export const canUsernameBeUpdated = status => {
|
||||
moment(created_at).isAfter(oldestEditTime)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* getKarma
|
||||
* retrieves karma value as string
|
||||
*/
|
||||
|
||||
export const getKarma = reliability => {
|
||||
if (reliability === null) {
|
||||
return 'neutral';
|
||||
} else if (reliability) {
|
||||
return 'good';
|
||||
} else {
|
||||
return 'bad';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,17 +7,26 @@ import cn from 'classnames';
|
||||
* BareButton is a button whose styling is stripped off to a minimum.
|
||||
* Can pass anchor=true to use `a` instead of `button`
|
||||
*/
|
||||
const BareButton = ({ anchor, className, ...props }) => {
|
||||
let Element = 'button';
|
||||
if (anchor) {
|
||||
Element = 'a';
|
||||
export default class BareButton extends React.Component {
|
||||
ref = null;
|
||||
|
||||
handleRef = ref => (this.ref = ref);
|
||||
focus = () => this.ref.focus();
|
||||
|
||||
render() {
|
||||
const { anchor, className, ...props } = this.props;
|
||||
const Element = anchor ? 'a' : 'button';
|
||||
return (
|
||||
<Element
|
||||
{...props}
|
||||
className={cn(styles.bare, className)}
|
||||
ref={this.handleRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Element {...props} className={cn(styles.bare, className)} />;
|
||||
};
|
||||
}
|
||||
|
||||
BareButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
anchor: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BareButton;
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 34px;
|
||||
background: #2c2c2c;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
line-height: 20px;
|
||||
|
||||
font-size: 0.98em;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
color: #e5e5e5;
|
||||
background: #888;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
padding: 8px 45px 8px 15px;
|
||||
outline: none;
|
||||
color: white;
|
||||
background: #2c2c2c;
|
||||
border-radius: 3px;
|
||||
height: 34px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
line-height: 20px;
|
||||
font-size: 0.98em;
|
||||
|
||||
&:focus {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #e5e5e5;
|
||||
background: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.toggleOpen {
|
||||
|
||||
@@ -4,6 +4,7 @@ import styles from './Dropdown.css';
|
||||
import Icon from './Icon';
|
||||
import cn from 'classnames';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import { BareButton } from 'coral-ui';
|
||||
|
||||
class Dropdown extends React.Component {
|
||||
toggleRef = null;
|
||||
@@ -88,16 +89,6 @@ class Dropdown extends React.Component {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
const code = e.which;
|
||||
|
||||
// 13 = Return, 32 = Space
|
||||
if (code === 13 || code === 32) {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
hideMenu = () => {
|
||||
this.setState({
|
||||
isOpen: false,
|
||||
@@ -155,23 +146,18 @@ class Dropdown extends React.Component {
|
||||
styles.dropdown,
|
||||
className,
|
||||
containerClassName,
|
||||
'dd dd-container',
|
||||
{
|
||||
[styles.disabled]: disabled,
|
||||
}
|
||||
'dd dd-container'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
<BareButton
|
||||
className={cn(styles.toggle, toggleClassName, {
|
||||
[cn(this.state.isOpen, toggleOpenClassName)]: this.state.isOpen,
|
||||
})}
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
role="button"
|
||||
aria-pressed={this.state.isOpen}
|
||||
aria-haspopup="true"
|
||||
tabIndex={disabled ? '-1' : '0'}
|
||||
ref={this.handleToggleRef}
|
||||
disabled={disabled}
|
||||
>
|
||||
{this.props.icon && (
|
||||
<Icon
|
||||
@@ -194,7 +180,7 @@ class Dropdown extends React.Component {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</BareButton>
|
||||
{this.state.isOpen && (
|
||||
<div>
|
||||
<div tabIndex="0" onFocus={this.trapFocusBegin} />
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
.option {
|
||||
min-width: 100px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
|
||||
&:focus, &:hover {
|
||||
background-color: #ccc;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Option.css';
|
||||
import cn from 'classnames';
|
||||
import { BareButton } from 'coral-ui';
|
||||
|
||||
class Option extends React.Component {
|
||||
ref = null;
|
||||
|
||||
handleRef = ref => {
|
||||
this.ref = ref;
|
||||
this.ref = findDOMNode(ref);
|
||||
};
|
||||
|
||||
focus = () => {
|
||||
@@ -19,16 +21,17 @@ class Option extends React.Component {
|
||||
const { className, label = '', onClick, onKeyDown } = this.props;
|
||||
const id = this.props.id ? this.props.id : this.props.value;
|
||||
return (
|
||||
<li
|
||||
className={cn(styles.option, className, 'dd-option')}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
role="option"
|
||||
tabIndex="0"
|
||||
ref={this.handleRef}
|
||||
id={id}
|
||||
>
|
||||
{label}
|
||||
<li>
|
||||
<BareButton
|
||||
className={cn(styles.option, className, 'dd-option')}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
role="option"
|
||||
ref={this.handleRef}
|
||||
id={id}
|
||||
>
|
||||
{label}
|
||||
</BareButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user