Merge branch 'mod-queue-load-more' of github.com:coralproject/talk into mod-queue-load-more

This commit is contained in:
David Jay
2017-03-09 13:36:59 -05:00
77 changed files with 584 additions and 333 deletions
+1
View File
@@ -34,6 +34,7 @@ app.use(helmet({
}));
app.use(bodyParser.json());
app.use('/client', express.static(path.join(__dirname, 'dist')));
app.use('/public', express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
@@ -8,7 +8,14 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
const onBanClick = (userId, commentId, handleBanUser, rejectComment, handleClose) => (e) => {
e.preventDefault();
handleBanUser({userId})
.then(handleClose)
.then(() => rejectComment({commentId}));
};
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId}) => (
<Dialog
className={styles.dialog}
id="banuserDialog"
@@ -29,7 +36,7 @@ const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
{lang.t('bandialog.cancel')}
</Button>
<Button cStyle="black" className={styles.ban} onClick={() => handleBanUser({userId: user.id})} raised>
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, handleBanUser, rejectComment, handleClose)} raised>
{lang.t('bandialog.yes_ban_user')}
</Button>
</div>
@@ -24,10 +24,30 @@
margin-bottom: 20px;
align-items: flex-start;
min-height: 100px;
max-width: 600px;
h3 {
margin: 0;
}
.actions {
display: inline-block;
width: 100%;
.copiedText {
display: inline-block;
color: #00796b;
padding: 12px;
font-size: 14px;
float: right;
}
.copyButton {
display: inline-block;
width: 200px;
float: right;
}
}
}
.settingsError {
@@ -41,7 +61,9 @@
.settingsHeader {
margin-top: 3px;
margin-bottom: 10px;
margin-bottom: 7px;
font-size: 18px;
font-weight: 500;
}
.disabledSettingText {
@@ -58,11 +80,6 @@
overflow: visible;
}
.configSettingInfoBox p {
font-size: 12px;
bottom: 0;
}
.configSettingEmbed {
border: 1px solid #ccc;
border-radius: 4px;
@@ -81,6 +98,8 @@
border-color: #ccc;
border-style: solid;
border-width: 0px 0px 1px 0px;
font-size: 14px;
text-align: center;
}
.charCountTexfieldEnabled {
@@ -96,29 +115,18 @@
color: white;
}
.copiedText {
color: #00796b;
float: right;
padding: 12px;
font-size: 14px;
}
.copyButton {
float: right;
width: 200px;
}
.embedInput {
border-radius: 3px;
border: 1px solid #ccc;
width: 100%;
display: block;
width: 90%;
vertical-align: middle;
margin-bottom: 10px;
color: #555;
padding: 14px;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
min-height: 175px;
font-size: 14px;
letter-spacing: 0.03em;
resize: none;
}
#bannedWordlist, #suspectWordlist {
@@ -170,3 +178,24 @@
padding-left: 30px;
}
}
.Configure {
p {
line-height: 1.2;
max-width: 550px;
}
.wrapper {
width: 550px;
}
.descriptionBox {
margin-top: 15px;
max-width: 550px;
input {
height: 150px;
}
}
}
@@ -10,15 +10,19 @@ const lang = new I18n(translations);
const Domainlist = ({domains, onChangeDomainlist}) => {
return (
<Card id={styles.domainlist} className={styles.configSetting}>
<h3>{lang.t('configure.domain-list-title')}</h3>
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
<div className={styles.wrapper}>
<div className={styles.settingsHeader}>{lang.t('configure.domain-list-title')}</div>
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
<div className={styles.wrapper}>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
</div>
</div>
</Card>
);
};
@@ -45,13 +45,19 @@ class EmbedLink extends Component {
`.trim();
return (
<Card shadow="2" className={styles.configSetting}>
<h3>Embed Comment Stream</h3>
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
<div className={styles.wrapper}>
<div className={styles.settingsHeader}>Embed Comment Stream</div>
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<div className={styles.actions}>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>
{this.state.copied && 'Copied!'}
</div>
</div>
</div>
</Card>
);
}
@@ -23,7 +23,7 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
const off = styles.disabledSetting;
return (
<div>
<div className={styles.Configure}>
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
<div className={styles.action}>
<Checkbox
@@ -3,8 +3,8 @@ import {SelectField, Option} from 'react-mdl-selectfield';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import styles from './Configure.css';
import {Textfield, Checkbox} from 'react-mdl';
import {Card, Icon} from 'coral-ui';
import {Checkbox, Textfield} from 'react-mdl';
import {Card, Icon, TextArea} from 'coral-ui';
const TIMESTAMPS = {
weeks: 60 * 60 * 24 * 7,
@@ -32,6 +32,11 @@ const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
updateSettings({infoBoxEnable});
};
const updatePremodLinksEnable = (updateSettings, premodLinks) => () => {
const premodLinksEnable = !premodLinks;
updateSettings({premodLinksEnable});
};
const updateInfoBoxContent = (updateSettings) => (event) => {
const infoBoxContent = event.target.value;
updateSettings({infoBoxContent});
@@ -64,7 +69,7 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
const off = styles.disabledSetting;
return (
<div>
<div className={styles.Configure}>
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
@@ -79,7 +84,9 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
value={settings.charCount}
disabled={settings.charCountEnable ? '' : 'disabled'}
/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
@@ -92,41 +99,56 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
</p>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
<Card className={`${styles.configSetting} ${settings.premodLinksEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updatePremodLinksEnable(updateSettings, settings.premodLinksEnable)}
checked={settings.premodLinksEnable} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.enable-premod-links')}</div>
<p>
{lang.t('configure.enable-premod-links-text')}
</p>
</div>
</Card>
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</div>
<div className={styles.content}>
{lang.t('configure.include-comment-stream')}
<p>
<div className={styles.settingsHeader}>
{lang.t('configure.include-comment-stream')}
</div>
<p className={settings.infoBoxEnable ? '' : styles.disabledSettingText}>
{lang.t('configure.include-comment-stream-desc')}
</p>
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<div className={styles.content}>
<Textfield
<div>
<TextArea
className={styles.descriptionBox}
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
/>
</div>
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<div className={styles.content}>
{lang.t('configure.closed-comments-desc')}
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
<div className={styles.settingsHeader}>{lang.t('configure.closed-stream-settings')}</div>
<div className={styles.wrapper}>
<p>{lang.t('configure.closed-comments-desc')}</p>
<div>
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
<TextArea className={styles.descriptionBox}
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
/>
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
<div className={styles.content}>
{lang.t('configure.close-after')}
<br />
@@ -14,19 +14,20 @@ const updateCustomCssUrl = (updateSettings) => (event) => {
const TechSettings = ({settings, onChangeDomainlist, updateSettings}) => {
return (
<div>
<div className={styles.Configure}>
<Domainlist
domains={settings.domains.whitelist}
onChangeDomainlist={onChangeDomainlist} />
<EmbedLink />
<Card className={styles.configSetting}>
<h3>{lang.t('configure.custom-css-url')}</h3>
<p>{lang.t('configure.custom-css-url-desc')}</p>
<br />
<input
className={styles.customCSSInput}
value={settings.customCssUrl}
onChange={updateCustomCssUrl(updateSettings)} />
<div className={styles.wrapper}>
<div className={styles.settingsHeader}>{lang.t('configure.custom-css-url')}</div>
<p>{lang.t('configure.custom-css-url-desc')}</p>
<input
className={styles.customCSSInput}
value={settings.customCssUrl}
onChange={updateCustomCssUrl(updateSettings)} />
</div>
</Card>
</div>
);
@@ -8,25 +8,29 @@ import {Card} from 'coral-ui';
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
<div>
<Card id={styles.bannedWordlist} className={styles.configSetting}>
<h3>{lang.t('configure.banned-words-title')}</h3>
<div className={styles.settingsHeader}>{lang.t('configure.banned-words-title')}</div>
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
<TagsInput
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('banned', tags)}
/>
<div className={styles.wrapper}>
<TagsInput
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('banned', tags)}
/>
</div>
</Card>
<Card id={styles.suspectWordlist} className={styles.configSetting}>
<h3>{lang.t('configure.suspect-words-title')}</h3>
<div className={styles.settingsHeader}>{lang.t('configure.suspect-words-title')}</div>
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
<TagsInput
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('suspect', tags)} />
<div className={styles.wrapper}>
<TagsInput
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('suspect', tags)} />
</div>
</Card>
</div>
);
@@ -16,7 +16,7 @@ const FlagWidget = (props) => {
<thead className={styles.widgetHead}>
<tr>
<th>{lang.t('streams.article')}</th>
<th>{lang.t('dashboard.flags')}</th>
<th colSpan='2'>{lang.t('dashboard.flags')}</th>
</tr>
</thead>
<tbody>
@@ -27,20 +27,21 @@ const FlagWidget = (props) => {
return (
<tr className={styles.rowLinkify} key={asset.id}>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</Link>
</td>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
</td>
<td>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
</td>
</tr>
);
})
: <tr className={styles.rowLinkify}><td colSpan="2">{lang.t('dashboard.no_flags')}</td></tr>
: <tr className={styles.rowLinkify}><td colSpan="3">{lang.t('dashboard.no_flags')}</td></tr>
}
{ /* rows in a table with a fixed height will expand and ignore height.
this extra row will expand to fill the extra space. */
@@ -17,7 +17,7 @@ const LikeWidget = (props) => {
<thead className={styles.widgetHead}>
<tr>
<th>{lang.t('streams.article')}</th>
<th>{lang.t('modqueue.likes')}</th>
<th colSpan='2'>{lang.t('modqueue.likes')}</th>
</tr>
</thead>
<tbody>
@@ -28,20 +28,21 @@ const LikeWidget = (props) => {
return (
<tr className={styles.rowLinkify} key={asset.id}>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</Link>
</td>
<td>
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
</td>
<td>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
</td>
</tr>
);
})
: <tr className={styles.rowLinkify}><td colSpan="2">{lang.t('dashboard.no_likes')}</td></tr>
: <tr className={styles.rowLinkify}><td colSpan="3">{lang.t('dashboard.no_likes')}</td></tr>
}
{ /* rows in a table with a fixed height will expand and ignore height.
this extra row will expand to fill the extra space. */
@@ -55,11 +55,26 @@
padding: 10px;
}
.widgetTable tbody td:last-child {
padding-top: 0;
}
.linkToAsset {
display: block;
text-decoration: none;
}
.linkToModerate {
background-color: #e0e0e0;
padding: 10px 14px;
text-decoration: none;
color: black;
}
.linkToModerate:hover {
background-color: #ccc;
}
.lede {
font-size: 0.9em;
color: #aaa;
@@ -27,23 +27,21 @@ class ModerationContainer extends Component {
componentWillMount() {
const {toggleModal, singleView} = this.props;
const {selectedIndex} = this.state;
this.props.fetchSettings();
key('s', () => singleView());
key('shift+/', () => toggleModal(true));
key('esc', () => toggleModal(false));
key('j', () => this.setState({selectedIndex: selectedIndex + 1}));
key('k', () => this.setState({selectedIndex: selectedIndex > 0 ? selectedIndex + 1 : selectedIndex}));
key('r', () => this.moderate(false));
key('t', () => this.moderate(true));
key('j', this.select(true));
key('k', this.select(false));
key('r', this.moderate(false));
key('t', this.moderate(true));
}
moderate = (accept) => {
const {data, route, acceptComment, rejectComment} = this.props;
moderate = (accept) => () => {
const {acceptComment, rejectComment} = this.props;
const {selectedIndex} = this.state;
const activeTab = route.path === ':id' ? 'premod' : route.path;
const comments = data[activeTab];
const comments = this.getComments();
const commentId = {commentId: comments[selectedIndex].id};
if (accept) {
@@ -51,7 +49,30 @@ class ModerationContainer extends Component {
} else {
rejectComment(commentId);
}
}
getComments = () => {
const {data, route} = this.props;
const activeTab = route.path === ':id' ? 'premod' : route.path;
return data[activeTab];
}
select = (next) => () => {
if (next) {
this.setState(prevState =>
({
...prevState,
selectedIndex: prevState.selectedIndex < this.getComments().length - 1
? prevState.selectedIndex + 1 : prevState.selectedIndex
}));
} else {
this.setState(prevState =>
({
...prevState,
selectedIndex: prevState.selectedIndex > 0 ?
prevState.selectedIndex - 1 : prevState.selectedIndex
}));
}
}
selectSort = (sort) => {
@@ -142,8 +163,10 @@ class ModerationContainer extends Component {
<BanUserDialog
open={moderation.banDialog}
user={moderation.user}
commentId={moderation.commentId}
handleClose={props.hideBanUserDialog}
handleBanUser={props.banUser}
rejectComment={props.rejectComment}
/>
<ModerationKeysModal
open={moderation.modalOpen}
@@ -10,9 +10,9 @@ const ModerationHeader = props => (
props.asset ?
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
<Link className="mdl-tabs__tab" to="/admin/moderate">All Streams</Link>
<a className="mdl-tabs__tab">
{props.asset.title}
<a href={props.asset.url} className={styles.settingsButton}><Icon name="settings"/></a>
<a className="mdl-tabs__tab" href={props.asset.url}>
<span>{props.asset.title}</span>
<Icon className={styles.settingsButton} name="open_in_new"/>
</a>
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
</div>
@@ -84,11 +84,10 @@ span {
margin-bottom: -1px;
.settingsButton {
i {
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
}
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
font-size: 16px;
}
.moderateAsset {
@@ -114,7 +113,15 @@ span {
}
&:nth-child(2) {
text-align: center;
span {
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 344px;
display: inline-block;
vertical-align: top;
}
}
&:last-child {
@@ -22,7 +22,26 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'ACCEPTED'
},
refetchQueries: ['ModQueue']
updateQueries: {
ModQueue: (oldData) => {
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const rejected = oldData.rejected.filter(c => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
return {
...oldData,
premodCount,
flaggedCount,
rejectedCount,
premod,
flagged,
rejected,
};
}
}
});
},
rejectComment: ({commentId}) => {
@@ -31,7 +50,27 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'REJECTED'
},
refetchQueries: ['ModQueue']
updateQueries: {
ModQueue: (oldData) => {
const comment = oldData.premod.concat(oldData.flagged).filter(c => c.id === commentId)[0];
const rejected = [comment].concat(oldData.rejected);
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = oldData.rejectedCount + 1;
return {
...oldData,
premodCount,
flaggedCount,
rejectedCount,
premod,
flagged,
rejected
};
}
}
});
}
})
+10 -4
View File
@@ -53,6 +53,7 @@
"copy": "Copy to Clipboard"
},
"configure": {
"closed-stream-settings": "Closed Stream Message",
"stream-settings": "Stream Settings",
"moderation-settings": "Moderation Settings",
"tech-settings": "Tech Settings",
@@ -63,9 +64,11 @@
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
"require-email-verification": "Require Email Verification",
"require-email-verification-text": "New Users must verify their email before commenting",
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream": "Include Comment Stream Description for Readers",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
"include-text": "Include your text here.",
"enable-premod-links": "Pre-Moderate Comments Containing Links",
"enable-premod-links-text": "Moderators must approve any comment containing a link before its published.",
"comment-settings": "Settings",
"embed-comment-stream": "Embed Stream",
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
@@ -79,7 +82,7 @@
"configure": "Configure",
"community": "Community",
"streams": "Streams",
"closed-comments-desc": "Write a message for closed threads",
"closed-comments-desc": "Write a message to be displayed when when your comment stream is closed and no longer accepting comments.",
"closed-comments-label": "Write a message...",
"hours": "Hours",
"days": "Days",
@@ -87,7 +90,7 @@
"close-after": "Close comments after",
"comment-count-header": "Limit Comment Length",
"comment-count-text-pre": "Comments will be limited to ",
"comment-count-text-post": " characters.",
"comment-count-text-post": " characters",
"comment-count-error": "Please enter a valid number.",
"domain-list-title": "Permitted Domains",
"domain-list-text": "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)."
@@ -178,6 +181,7 @@
"username_flags": ""
},
"configure": {
"closed-stream-settings": "Mensaje cuando los comentarios están cerrados en el artículo",
"stream-settings": "Configuración de Comentarios",
"moderation-settings": "Configuración de Moderación",
"tech-settings": "Configuración Technical",
@@ -193,6 +197,8 @@
"include-text": "Incluir tu texto aqui.",
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
"enable-premod-links": "Pre-Moderar Commentarios que contienen Links",
"enable-premod-links-text": "Los y las Moderadoras deben probar cualquier comentario que contengan links antes de su publicación.",
"wordlist": "Palabras Suspendidas y Suspechosas",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
@@ -204,7 +210,7 @@
"configure": "Configurar",
"community": "Comunidad",
"streams": "Streams",
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
"closed-comments-desc": "Escribe un mensaje que será mostrado cuando los comentarios estén cerrados y no se acepten más comentarios.",
"closed-comments-label": "Escribe un mensaje...",
"never": "Nunca",
"hours": "Horas",
@@ -34,6 +34,18 @@ export default ({handleChange, handleApply, changed, updateQuestionBoxContent, .
description: lang.t('configureCommentStream.enablePremodDescription')
}} />
</li>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premodLinks"
onChange={handleChange}
defaultChecked={props.premodLinks}
info={{
title: lang.t('configureCommentStream.enablePremodLinks'),
description: lang.t('configureCommentStream.enablePremodLinksDescription')
}} />
</li>
<li>
<Checkbox
className={styles.checkbox}
@@ -31,13 +31,14 @@ class ConfigureStreamContainer extends Component {
const questionBoxEnable = elements.qboxenable.checked;
const questionBoxContent = elements.qboxcontent.value;
// const premodLinks = elements.premodLinks.checked;
const premodLinksEnable = elements.premodLinks.checked;
const {changed} = this.state;
const newConfig = {
moderation: premod ? 'PRE' : 'POST',
questionBoxEnable,
questionBoxContent
questionBoxContent,
premodLinksEnable
};
if (changed) {
@@ -77,10 +78,9 @@ class ConfigureStreamContainer extends Component {
}
render () {
const status = this.props.asset.closedAt === null ? 'open' : 'closed';
const premod = this.props.asset.settings.moderation === 'PRE';
const questionBoxEnable = this.props.asset.settings.questionBoxEnable;
const questionBoxContent = this.props.asset.settings.questionBoxContent;
const {settings, closedAt} = this.props.asset;
const status = closedAt === null ? 'open' : 'closed';
const premod = settings.moderation === 'PRE';
return (
<div>
@@ -88,11 +88,11 @@ class ConfigureStreamContainer extends Component {
handleChange={this.handleChange}
handleApply={this.handleApply}
changed={this.state.changed}
premodLinks={false}
premodLinks={settings.premodLinks}
premod={premod}
updateQuestionBoxContent={this.updateQuestionBoxContent}
questionBoxEnable={questionBoxEnable}
questionBoxContent={questionBoxContent}
questionBoxEnable={settings.questionBoxEnable}
questionBoxContent={settings.questionBoxContent}
/>
<hr />
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
+6 -1
View File
@@ -3,5 +3,10 @@
}
.Comment {
}
.pendingComment {
filter: blur(2px);
pointer-events: none;
}
+1
View File
@@ -116,6 +116,7 @@ class Comment extends React.Component {
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async () => {
+3 -20
View File
@@ -16,7 +16,7 @@ import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {updateCountCache} from 'coral-framework/actions/asset';
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
import InfoBox from 'coral-plugin-infobox/InfoBox';
@@ -176,7 +176,7 @@ class Embed extends Component {
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
addNotification={addNotification}
addNotification={this.props.addNotification}
depth={0}
postItem={this.props.postItem}
asset={asset}
@@ -220,11 +220,6 @@ class Embed extends Component {
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
</div>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={{text: null}}
/>
<LoadMore
assetId={asset.id}
comments={asset.comments}
@@ -246,11 +241,6 @@ class Embed extends Component {
/>
</RestrictedContent>
</TabContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
</div>
);
@@ -258,7 +248,6 @@ class Embed extends Component {
}
const mapStateToProps = state => ({
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS(),
asset: state.asset.toJS()
@@ -267,13 +256,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
requestConfirmEmail: () => dispatch(requestConfirmEmail()),
loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)),
addNotification: (type, text) => {
pym.sendMessage('getPosition');
pym.onMessage('position', position => {
dispatch(addNotification(type, text, position));
});
},
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
editName: (username) => dispatch(editName(username)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
+54 -15
View File
@@ -1,10 +1,16 @@
* {
font-weight: inherit;
font-family: inherit;
font-style: inherit;
font-size: 100%;
}
html, body {
width:auto;
height:auto;
}
body {
font-family: 'Open Sans', sans-serif;
font-family: 'Lato', sans-serif;
width: 100%;
font-size: 14px;
@@ -80,6 +86,16 @@ hr {
padding: 10px;
margin-bottom: 10px;
display: block;
box-sizing: border-box;
border-radius: 2px;
}
.commentStream .material-icons {
vertical-align: middle;
width: 1em;
font-size: 1em;
overflow: hidden;
}
/* Question Box Styles */
@@ -95,15 +111,41 @@ hr {
font-weight: bold;
font-size: 14px;
display: block;
overflow: hidden;
height: 50px;
}
.coral-plugin-questionbox-icon {
.coral-plugin-questionbox-icon.bubble{
position: absolute;
top: 11px;
left: 15px;
color: #949393;
font-size: 20px;
z-index: 0;
}
.coral-plugin-questionbox-icon.person{
z-index: 2;
top: 20px;
left: 20px;
position: absolute;
font-size: 24px;
color: white;
}
.coral-plugin-questionbox-box {
position: relative;
border: 0;
background: black;
color: white;
padding: 20px;
margin-left: 0px !important;
margin-right: 10px;
display: inline-block;
width: 15px;
height: 100%;
padding: 3px 20px;
vertical-align: middle;
}
.hidden {
@@ -127,6 +169,8 @@ hr {
flex: 1;
padding: 5px;
min-height: 100px;
margin-top: 10px;
font-size: 14px;
}
.coral-plugin-commentbox-button-container {
@@ -220,7 +264,7 @@ hr {
.comment__action-container .material-icons {
font-size: 12px;
margin-left: 3px;
margin-left: 3px;
}
button.comment__action-button,
@@ -237,13 +281,6 @@ button.comment__action-button[disabled],
white-space: nowrap;
}
.commentStream .material-icons {
vertical-align: middle;
width: 1em;
font-size: 1em;
overflow: hidden;
}
.likedButton {
color: rgb(0,134,227);
}
@@ -323,14 +360,14 @@ button.comment__action-button[disabled],
}
.coral-plugin-flags-popup-counter {
float: left;
margin-top: 21px;
color: #999;
float: left;
margin-top: 21px;
color: #999;
}
.coral-plugin-flags-popup-button {
float: right;
margin-top: 10px;
float: right;
margin-top: 10px;
}
.coral-plugin-flags-reason-text {
@@ -386,6 +423,8 @@ button.coral-load-more {
color: #FFF;
background-color: #2376D8;
cursor: pointer;
padding: 10px;
border-radius: 2px;
}
button.coral-load-more:hover {
+50
View File
@@ -1,5 +1,26 @@
import pym from 'pym.js';
const snackbarStyles = {
position: 'fixed',
cursor: 'default',
userSelect: 'none',
backgroundColor: '#323232',
zIndex: 3,
willChange: 'transform, opacity',
transition: 'transform .35s cubic-bezier(.55,0,.1,1), opacity .35s',
pointerEvents: 'none',
padding: '12px 18px',
color: '#fff',
borderRadius: '3px 3px 0 0',
textAlign: 'center',
maxWidth: '300px',
left: '50%',
opacity: 0,
transform: 'translate(-50%, 20px)',
bottom: 0,
boxSizing: 'border-box'
};
// This function should return value of window.Coral
const Coral = {};
const Talk = Coral.Talk = {};
@@ -32,6 +53,14 @@ function configurePymParent(pymParent, asset_url) {
let notificationOffset = 200;
let ready = false;
let cachedHeight;
const snackbar = document.createElement('div');
snackbar.id = 'coral-notif';
for (let key in snackbarStyles) {
snackbar.style[key] = snackbarStyles[key];
}
window.document.body.appendChild(snackbar);
// Resize parent iframe height when child height changes
pymParent.onMessage('height', function(height) {
@@ -41,6 +70,27 @@ function configurePymParent(pymParent, asset_url) {
}
});
pymParent.onMessage('coral-clear-notification', function () {
snackbar.style.opacity = 0;
});
pymParent.onMessage('coral-alert', function (message) {
const [type, text] = message.split('|');
snackbar.style.transform = 'translate(-50%, 20px)';
snackbar.style.opacity = 0;
snackbar.className = `coral-notif-${type}`;
snackbar.textContent = text;
setTimeout(() => {
snackbar.style.transform = 'translate(-50%, 0)';
snackbar.style.opacity = 1;
}, 0);
setTimeout(() => {
snackbar.style.opacity = 0;
}, 5000);
});
// Helps child show notifications at the right scrollTop
pymParent.onMessage('getPosition', function() {
let position = viewport().height + document.body.scrollTop;
+4 -12
View File
@@ -1,17 +1,9 @@
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
import {pym} from 'coral-framework';
export const addNotification = (notifType, text, position) => {
return {
type: ADD_NOTIFICATION,
notifType,
text,
position
};
export const addNotification = (notifType, text) => {
pym.sendMessage('coral-alert', `${notifType}|${text}`);
};
export const clearNotification = () => {
return {
type: CLEAR_NOTIFICATION
};
pym.sendMessage('coral-clear-notification');
};
@@ -35,14 +35,14 @@ export const postComment = graphql(POST_COMMENT, {
action_summaries: [],
tags: [],
status: null,
id: `${Date.now()}_temp_id`
id: 'pending'
}
}
},
updateQueries: {
AssetQuery: (oldData, {mutationResult:{data:{createComment:{comment}}}}) => {
if (oldData.asset.settings.moderation === 'PRE') {
if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return oldData;
}
-2
View File
@@ -4,7 +4,6 @@ import I18n from './modules/i18n/i18n';
import * as authActions from './actions/auth';
import * as assetActions from './actions/asset';
import * as notificationActions from './actions/notification';
import Notification from './modules/notification/Notification';
export {
pym,
@@ -12,6 +11,5 @@ export {
store,
authActions,
assetActions,
Notification,
notificationActions
};
@@ -1,22 +0,0 @@
import React from 'react';
import {SnackBar} from 'coral-ui';
const Notification = (props) => {
if (props.notification.text) {
setTimeout(() => {
props.clearNotification();
}, props.notifLength);
}
return (
<div>
{
props.notification.text &&
<SnackBar id='coral-notif' className={`coral-notif-${props.notification.type}`} position={props.notification.position}>
{props.notification.text}
</SnackBar>
}
</div>
);
};
export default Notification;
-2
View File
@@ -1,11 +1,9 @@
import auth from './auth';
import user from './user';
import asset from './asset';
import notification from './notification';
export default {
auth,
user,
asset,
notification
};
@@ -1,24 +0,0 @@
import * as actions from '../actions/notification';
import {Map} from 'immutable';
const initialState = Map({
text: '',
type: '',
position: 400
});
export default (state = initialState, action) => {
switch (action.type) {
case actions.ADD_NOTIFICATION:
return state
.merge({
type: action.notifType,
text: action.text,
position: action.position
});
case actions.CLEAR_NOTIFICATION:
return initialState;
default:
return state;
}
};
@@ -3,7 +3,7 @@
"post": "Post",
"cancel": "Cancel",
"reply": "Reply",
"comment": "Comment",
"comment": "Post a Comment",
"name": "Name",
"comment-post-notif": "Your comment has been posted.",
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
@@ -14,7 +14,7 @@
"post": "Publicar",
"cancel": "Cancelar",
"reply": "Respuesta",
"comment": "Comentario",
"comment": "Escribe un Comentario",
"name": "Nombre",
"comment-post-notif": "Tu comentario ha sido publicado.",
"comment-post-notif-premod": "Gracias por comentar. Nuestro equipo de moderación va a revisarlo muy pronto.",
+15 -3
View File
@@ -32,18 +32,30 @@ class FlagButton extends Component {
if (flagged) {
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
deleteAction(localPost || flag.current_user.id);
} else if (this.state.showMenu){
this.closeMenu();
} else {
this.setState({showMenu: !this.state.showMenu});
this.setState({showMenu: true});
}
}
closeMenu = () => {
this.setState({
showMenu: false,
itemType: '',
reason: '',
message: '',
step: 0
});
}
onPopupContinue = () => {
const {postFlag, postDontAgree, id, author_id} = this.props;
const {itemType, reason, step, posted, message} = this.state;
// Proceed to the next step or close the menu if we've reached the end
if (step + 1 >= this.props.getPopupMenu.length) {
this.setState({showMenu: false});
this.closeMenu();
} else {
this.setState({step: step + 1});
}
@@ -114,7 +126,7 @@ class FlagButton extends Component {
}
handleClickOutside () {
this.setState({showMenu: false});
this.closeMenu();
}
render () {
@@ -2,9 +2,11 @@ import React from 'react';
const packagename = 'coral-plugin-questionbox';
const QuestionBox = ({enable, content}) =>
<div
className={`${packagename}-info ${enable ? null : 'hidden'}` }>
<i className={`${packagename}-icon material-icons`}>chat_bubble person</i>
<div className={`${packagename}-info ${enable ? null : 'hidden'}` }>
<div className={`${packagename}-box`}>
<i className={`${packagename}-icon material-icons bubble`}>chat_bubble</i>
<i className={`${packagename}-icon material-icons person`}>person</i>
</div>
{content}
</div>;
+22 -14
View File
@@ -1,21 +1,29 @@
import React, {PropTypes} from 'react';
import React, {Component, PropTypes} from 'react';
import CommentBox from '../coral-plugin-commentbox/CommentBox';
const name = 'coral-plugin-replies';
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler, setActiveReplyBox}) => (
<div className={`${name}-textarea`} style={styles && styles.container}>
<CommentBox
commentPostedHandler={commentPostedHandler}
parentId={parentId}
cancelButtonClicked={setActiveReplyBox}
addNotification={addNotification}
authorId={authorId}
assetId={assetId}
postItem={postItem}
isReply={true} />
</div>
);
class ReplyBox extends Component {
componentDidMount() {
document.getElementById('replyText').focus();
}
render() {
const {styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler, setActiveReplyBox} = this.props;
return <div className={`${name}-textarea`} style={styles && styles.container}>
<CommentBox
commentPostedHandler={commentPostedHandler}
parentId={parentId}
cancelButtonClicked={setActiveReplyBox}
addNotification={addNotification}
authorId={authorId}
assetId={assetId}
postItem={postItem}
isReply={true} />
</div>;
}
}
ReplyBox.propTypes = {
setActiveReplyBox: PropTypes.func.isRequired,
+1 -1
View File
@@ -9,7 +9,7 @@
min-width: 64px;
padding: 0 8px;
display: inline-block;
font-family: 'Roboto','Helvetica','Arial',sans-serif;
font-family: inherit;
font-size: 14px;
overflow: hidden;
will-change: box-shadow,transform;
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import {Icon as IconMDL} from 'react-mdl';
const Icon = ({className, name}) => (
const Icon = ({className = '', name}) => (
<IconMDL className={className} name={name} />
);
+15
View File
@@ -0,0 +1,15 @@
.textArea {
textarea {
width: 100%;
display: block;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
min-height: 175px;
font-size: 14px;
resize: none;
}
}
+14
View File
@@ -0,0 +1,14 @@
import React, {PropTypes} from 'react';
import styles from './TextArea.css';
const TextArea = ({className, value = '', ...props}) => (
<div className={`${styles.textArea} ${className ? className : ''}`}>
<textarea value={value} {...props}/>
</div>
);
TextArea.propTypes = {
onChange: PropTypes.func,
};
export default TextArea;
+1
View File
@@ -22,3 +22,4 @@ export {default as WizardNav} from './components/WizardNav';
export {default as Select} from './components/Select';
export {default as Option} from './components/Option';
export {default as SnackBar} from './components/SnackBar';
export {default as TextArea} from './components/TextArea';
+11 -5
View File
@@ -3,6 +3,7 @@ const errors = require('../../errors');
const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const CommentsService = require('../../services/comments');
const linkify = require('linkify-it')();
const Wordlist = require('../../services/wordlist');
@@ -54,13 +55,16 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id =
* @param {String} body body of a comment
* @return {Object} resolves to the wordlist results
*/
const filterNewComment = (context, {body}) => {
const filterNewComment = (context, {body, asset_id}) => {
// Create a new instance of the Wordlist.
const wl = new Wordlist();
// Load the wordlist and filter the comment content.
return wl.load().then(() => wl.scan('body', body));
return Promise.all([
wl.load().then(() => wl.scan('body', body)),
AssetsService.rectifySettings(AssetsService.findById(asset_id))
]);
};
/**
@@ -72,7 +76,7 @@ const filterNewComment = (context, {body}) => {
* @param {Object} [wordlist={}] the results of the wordlist scan
* @return {Promise} resolves to the comment's status
*/
const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}, settings) => {
// Decide the status based on whether or not the current asset/settings
// has pre-mod enabled or not. If the comment was rejected based on the
@@ -82,6 +86,8 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
if (wordlist.banned) {
status = Promise.resolve('REJECTED');
} else if (settings.premodLinksEnable && linkify.test(body)) {
status = Promise.resolve('PREMOD');
} else {
status = AssetsService
.rectifySettings(AssetsService.findById(asset_id).then((asset) => {
@@ -131,13 +137,13 @@ const createPublicComment = (context, commentInput) => {
// We then take the wordlist and the comment into consideration when
// considering what status to assign the new comment, and resolve the new
// status to set the comment to.
.then((wordlist) => resolveNewCommentStatus(context, commentInput, wordlist)
.then(([wordlist, settings]) => resolveNewCommentStatus(context, commentInput, wordlist, settings)
// Then we actually create the comment with the new status.
.then((status) => createComment(context, commentInput, status))
.then((comment) => {
// If the comment was flagged as being suspect, we need to add a
// If the comment has a suspect word or a link, we need to add a
// flag to it to indicate that it needs to be looked at.
// Otherwise just return the new comment.
+1
View File
@@ -370,6 +370,7 @@ type Settings {
infoBoxEnable: Boolean
infoBoxContent: String
premodLinksEnable: Boolean
questionBoxEnable: Boolean
questionBoxContent: String
closeTimeout: Int
+4
View File
@@ -40,6 +40,10 @@ const SettingSchema = new Schema({
type: String,
default: ''
},
premodLinksEnable: {
type: Boolean,
default: false
},
organizationName: {
type: String
},
+1
View File
@@ -70,6 +70,7 @@
"inquirer": "^3.0.1",
"jsonwebtoken": "^7.1.9",
"kue": "^0.11.5",
"linkify-it": "^2.0.3",
"lodash": "^4.16.6",
"metascraper": "^1.0.6",
"minimist": "^1.2.0",
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+4
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

+41
View File
@@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/img\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/img\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/img\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/img\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/img\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/img\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
+1 -1
View File
@@ -12,7 +12,7 @@ router.get('/password-reset', (req, res) => {
// TODO: store the redirect uri in the token or something fancy.
// admins and regular users should probably be redirected to different places.
res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL});
res.render('admin/password-reset', {redirectUri: process.env.TALK_ROOT_URL});
});
router.get('*', (req, res) => {
@@ -1,35 +0,0 @@
import {Map} from 'immutable';
import {expect} from 'chai';
import notificationReducer from '../../../../client/coral-framework/reducers/notification';
import * as actions from '../../../../client/coral-framework/actions/notification';
describe ('notificationsReducer', () => {
describe('ADD_NOTIFICATION', () => {
it('should add a notification', () => {
const action = {
type: actions.ADD_NOTIFICATION,
text: 'Test notification',
notifType: 'test'
};
const store = new Map({});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal(action.text);
expect(result.get('type')).to.equal(action.notifType);
});
});
describe('CLEAR_NOTIFICATION', () => {
it('should clear a notification', () => {
const action = {
type: actions.CLEAR_NOTIFICATION
};
const store = new Map({
text: 'Test notification',
type: 'test'
});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal('');
expect(result.get('type')).to.equal('');
});
});
});
@@ -1,35 +0,0 @@
import {Map} from 'immutable';
import {expect} from 'chai';
import notificationReducer from '../../../../client/coral-framework/reducers/notification';
import * as actions from '../../../../client/coral-framework/actions/notification';
describe ('notificationsReducer', () => {
describe('ADD_NOTIFICATION', () => {
it('should add a notification', () => {
const action = {
type: actions.ADD_NOTIFICATION,
text: 'Test notification',
notifType: 'test'
};
const store = new Map({});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal(action.text);
expect(result.get('type')).to.equal(action.notifType);
});
});
describe('CLEAR_NOTIFICATION', () => {
it('should clear a notification', () => {
const action = {
type: actions.CLEAR_NOTIFICATION
};
const store = new Map({
text: 'Test notification',
type: 'test'
});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal('');
expect(result.get('type')).to.equal('');
});
});
});
+14
View File
@@ -5,6 +5,20 @@
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<meta property="csrf" content="<%= csrfToken %>">
<title>Talk - Coral Admin</title>
<link rel="apple-touch-icon" sizes="57x57" href="/public/img/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/public/img/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/public/img/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/public/img/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/public/img/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/public/img/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/public/img/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/public/img/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/public/img/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="/public/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/public/img/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/public/img/favicon-16x16.png">
<link rel="manifest" href="/public/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">