Merge branch 'master' into keyboard-shortcuts

This commit is contained in:
Riley Davis
2017-03-07 11:26:58 -07:00
committed by GitHub
88 changed files with 1427 additions and 597 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../.babelrc",
"plugins": [
"transform-async-to-generator",
]
}
@@ -5,7 +5,7 @@
i {
vertical-align: middle;
margin-right: 10px;
margin-right: 5px;
font-size: 14px;
}
}
@@ -14,28 +14,35 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
<div>
<Navigation className={styles.nav}>
<IndexLink
id='dashboardNav'
className={styles.navLink}
to="/admin/dashboard"
activeClassName={styles.active}>
{lang.t('configure.dashboard')}
</IndexLink>
<Link
id='moderateNav'
className={styles.navLink}
to="/admin/moderate"
activeClassName={styles.active}>
{lang.t('configure.moderate')}
</Link>
<Link className={styles.navLink}
<Link
id='streamsNav'
className={styles.navLink}
to="/admin/streams"
activeClassName={styles.active}>
{lang.t('configure.streams')}
</Link>
<Link className={styles.navLink}
<Link
id='communityNav'
className={styles.navLink}
to="/admin/community"
activeClassName={styles.active}>
{lang.t('configure.community')}
</Link>
<Link
id='configureNav'
className={styles.navLink}
to="/admin/configure"
activeClassName={styles.active}>
@@ -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,
@@ -64,7 +64,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 +79,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 +94,43 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
</p>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
<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>
);
@@ -21,49 +21,51 @@ const Comment = ({actions = [], ...props}) => {
const links = linkify.getMatches(props.comment.body);
const actionSummaries = props.comment.action_summaries;
return (
<li tabIndex={props.index}
className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>{props.comment.user.name}</span>
<div className={styles.author}>
<span>
{props.comment.user.name}
</span>
<span className={styles.created}>
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
<BanUserButton user={props.comment.user} onClick={() => props.showBanUserDialog(props.comment.user, props.comment.id)} />
<CommentType type={props.commentType} />
</div>
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={props.comment.user}
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
/>
)}
</div>
{props.comment.user.status === 'banned' ?
<span className={styles.banned}>
<Icon name='error_outline'/>
<Icon name='error_outline'/>
{lang.t('comment.banned_user')}
</span>
</span>
: null}
</div>
</div>
{!props.currentAsset && (
<div className={styles.moderateArticle}>
Story: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate &rarr;</Link>
</div>
)}
<div className={styles.moderateArticle}>
Story: {props.comment.asset.title}
{!props.currentAsset && (
<Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate &rarr;</Link>
)}
</div>
<div className={styles.itemBody}>
<p className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter searchWords={props.suspectWords} textToHighlight={props.comment.body}/>
</Linkify>
</p>
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={props.comment.user}
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
/>
)}
</div>
</div>
</div>
</div>
{actionSummaries && <FlagBox actionSummaries={actionSummaries} />}
@@ -4,12 +4,12 @@
top: 11px;
color: white;
background: grey;
padding: 2px 13px;
font-size: 14px;
height: 32px;
box-sizing: border-box;
line-height: 29px;
padding-left: 26px;
padding: 2px 8px 2px 26px;
border-radius: 2px;
font-size: 12px;
i {
font-size: 14px;
@@ -147,7 +147,6 @@ span {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
@@ -169,13 +168,12 @@ span {
margin: 0 auto;
position: relative;
transition: all 200ms;
margin-top: 0;
padding: 4px 0 0;
padding: 10px 0 0;
min-height: 220px;
.container {
padding: 0 14px;
min-height: 220px;
min-height: 180px;
}
&:hover {
@@ -200,11 +198,8 @@ span {
}
.sideActions {
position: absolute;
right: 0;
height: 100%;
top: 0;
padding: 100px 12px 65px;
box-sizing: border-box;
}
@@ -214,6 +209,7 @@ span {
justify-content: space-between;
.author {
font-weight: 600;
min-width: 230px;
display: flex;
align-items: center;
@@ -223,8 +219,9 @@ span {
.itemBody {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 16px;
font-size: 14px;
line-height: 1.5;
font-weight: 300;
}
.avatar {
@@ -241,6 +238,7 @@ span {
color: #666;
font-size: 13px;
margin-left: 15px;
line-height: 1px;
}
.actionButton {
@@ -251,10 +249,10 @@ span {
.body {
margin-top: 0px;
flex: 1;
font-size: 0.88em;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
}
.flagged {
@@ -324,9 +322,12 @@ span {
.Comment {
.moderateArticle {
font-size: 15px;
margin-top: 14px;
font-size: 14px;
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
max-width: 500px;
a {
display: inline-block;
color: #063b9a;
+6 -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,7 +64,7 @@
"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.",
"comment-settings": "Settings",
@@ -79,7 +80,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 +88,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 +179,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",
@@ -204,7 +206,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",
@@ -1,3 +1,7 @@
.Reply {
position: relative;
}
.Comment {
}
+84 -27
View File
@@ -17,6 +17,7 @@ import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
import styles from './Comment.css';
@@ -25,6 +26,11 @@ const getActionSummary = (type, comment) => comment.action_summaries
.filter((a) => a.__typename === type)[0];
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
// hold actions links (e.g. Like, Reply) along the comment footer
const ActionButton = ({children}) => {
return <span className="comment__action-button comment__action-button--nowrap">{ children }</span>;
};
class Comment extends React.Component {
constructor(props) {
@@ -74,7 +80,13 @@ class Comment extends React.Component {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
}).isRequired
}).isRequired,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
}
render () {
@@ -94,7 +106,9 @@ class Comment extends React.Component {
loadMore,
setActiveReplyBox,
activeReplyBox,
deleteAction
deleteAction,
addCommentTag,
removeCommentTag,
} = this.props;
const like = getActionSummary('LikeActionSummary', comment);
@@ -103,6 +117,27 @@ class Comment extends React.Component {
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async () => {
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
try {
return await fn();
} catch (error) {
addNotification('error', errorToMessage(error));
throw error;
}
};
const addBestTag = notifyOnError(() => addCommentTag({
id: comment.id,
tag: BEST_TAG,
}), () => 'Failed to tag comment as best');
const removeBestTag = notifyOnError(() => removeCommentTag({
id: comment.id,
tag: BEST_TAG,
}), () => 'Failed to remove best comment tag');
return (
<div
className={commentClass}
@@ -112,35 +147,55 @@ class Comment extends React.Component {
<AuthorName
author={comment.user}/>
{ isStaff(comment.tags)
? <TagLabel isStaff={true}/>
? <TagLabel>Staff</TagLabel>
: null }
{ commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<Content body={comment.body} />
<div className="commentActionsLeft">
<LikeButton
like={like}
id={comment.id}
postLike={postLike}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false} />
<div className="commentActionsLeft comment__action-container">
<ActionButton>
<LikeButton
like={like}
id={comment.id}
postLike={postLike}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</ActionButton>
<ActionButton>
<ReplyButton
onClick={() => setActiveReplyBox(comment.id)}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false} />
</ActionButton>
<ActionButton>
<IfUserCanModifyBest user={currentUser}>
<BestButton
isBest={commentIsBest(comment)}
addBest={addBestTag}
removeBest={removeBestTag} />
</IfUserCanModifyBest>
</ActionButton>
</div>
<div className="commentActionsRight">
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
<FlagComment
flag={flag && flag.current_user ? flag : dontagree}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
<div className="commentActionsRight comment__action-container">
<ActionButton>
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
</ActionButton>
<ActionButton>
<FlagComment
flag={flag && flag.current_user ? flag : dontagree}
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</ActionButton>
</div>
{
activeReplyBox === comment.id
@@ -172,6 +227,8 @@ class Comment extends React.Component {
postLike={postLike}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
+30 -18
View File
@@ -13,7 +13,7 @@ const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, postDontAgree, deleteAction} from 'coral-framework/graphql/mutations';
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';
@@ -55,7 +55,13 @@ class Embed extends Component {
data: React.PropTypes.shape({
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired
}).isRequired,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
}
componentDidMount () {
@@ -194,22 +200,26 @@ class Embed extends Component {
assetId={asset.id}
updateCountCache={this.props.updateCountCache}
/>
<Stream
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
getCounts={this.props.getCounts}
updateCountCache={this.props.updateCountCache}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
<div className="embed__stream">
<Stream
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
getCounts={this.props.getCounts}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
updateCountCache={this.props.updateCountCache}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
</div>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
@@ -278,6 +288,8 @@ export default compose(
postFlag,
postLike,
postDontAgree,
addCommentTag,
removeCommentTag,
deleteAction,
queryStream
)(Embed);
+13 -3
View File
@@ -12,7 +12,13 @@ class Stream extends React.Component {
currentUser: PropTypes.shape({
username: PropTypes.string,
id: PropTypes.string
})
}),
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
}
constructor(props) {
@@ -52,11 +58,13 @@ class Stream extends React.Component {
postDontAgree,
loadMore,
deleteAction,
showSignInDialog
showSignInDialog,
addCommentTag,
removeCommentTag
} = this.props;
return (
<div>
<div id='stream'>
{
comments.map(comment =>
<Comment
@@ -70,6 +78,8 @@ class Stream extends React.Component {
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
+26 -4
View File
@@ -80,6 +80,7 @@ hr {
padding: 10px;
margin-bottom: 10px;
display: block;
box-sizing: border-box;
}
/* Question Box Styles */
@@ -172,7 +173,7 @@ hr {
.coral-plugin-author-name-text {
display: inline-block;
margin: 10px 8px 10px 0;
margin: 10px 5px 10px 0;
font-weight: bold;
}
@@ -191,7 +192,7 @@ hr {
background-color: #4C1066;
color: white;
display: inline-block;
margin: 10px 10px;
margin: 0px 5px;
padding: 5px 5px;
border-radius: 2px;
}
@@ -218,10 +219,30 @@ hr {
width: 50%;
}
.material-icons {
font-size: 12px !important;
.comment__action-container .material-icons {
font-size: 12px;
margin-left: 3px;
}
button.comment__action-button,
.comment__action-button button {
cursor: pointer;
}
button.comment__action-button[disabled],
.comment__action-button[disabled] button {
cursor: inherit;
}
.comment__action-button--nowrap {
white-space: nowrap;
}
.commentStream .material-icons {
vertical-align: middle;
width: 1em;
font-size: 1em;
overflow: hidden;
}
.likedButton {
@@ -236,6 +257,7 @@ hr {
color: #696969;
display: inline-block;
font-size: .75rem;
margin-left: 5px;
}
.coral-plugin-permalinks-container {
+5 -4
View File
@@ -31,13 +31,14 @@ function buildStreamIframeUrl(talkBaseUrl, asset_url, comment, asset_id) {
function configurePymParent(pymParent, asset_url) {
let notificationOffset = 200;
let ready = false;
let cachedHeight;
// Resize parent iframe height when child height changes
pymParent.onMessage('height', function(height) {
// TODO: In local testing, this is firing nonstop. Maybe there's a bug on the inside?
// Or it's by design of pym... but that's very wasteful of CPU and DOM reflows (jank)
pymParent.el.querySelector('iframe').height = `${height }px`;
if (height !== cachedHeight) {
pymParent.el.firstChild.style.height = `${height}px`;
cachedHeight = height;
}
});
// Helps child show notifications at the right scrollTop
@@ -0,0 +1,13 @@
mutation AddCommentTag ($id: ID!, $tag: String!) {
addCommentTag(id:$id, tag:$tag) {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
}
@@ -4,6 +4,8 @@ import POST_FLAG from './postFlag.graphql';
import POST_LIKE from './postLike.graphql';
import POST_DONT_AGREE from './postDontAgree.graphql';
import DELETE_ACTION from './deleteAction.graphql';
import ADD_COMMENT_TAG from './addCommentTag.graphql';
import REMOVE_COMMENT_TAG from './removeCommentTag.graphql';
import commentView from '../fragments/commentView.graphql';
@@ -122,3 +124,27 @@ export const deleteAction = graphql(DELETE_ACTION, {
});
}}),
});
export const addCommentTag = graphql(ADD_COMMENT_TAG, {
props: ({mutate}) => ({
addCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, {
props: ({mutate}) => ({
removeCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
@@ -0,0 +1,13 @@
mutation RemoveCommentTag ($id: ID!, $tag: String!) {
removeCommentTag(id:$id, tag:$tag) {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
}
+106
View File
@@ -0,0 +1,106 @@
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import {Icon} from 'coral-ui';
import classnames from 'classnames';
// tag string for best comments
export const BEST_TAG = 'BEST';
export const commentIsBest = ({tags} = {}) => {
const isBest = Array.isArray(tags) && tags.some(t => t.name === BEST_TAG);
return isBest;
};
const name = 'coral-plugin-best';
const lang = new I18n(translations);
// It would be best if the backend/api held this business logic
const canModifyBestTag = ({roles = []} = {}) => roles && ['ADMIN', 'MODERATOR'].some(role => roles.includes(role));
// Put this on a comment to show that it is best
export const BestIndicator = ({children = <Icon name='star'/>}) => (
<span aria-label={lang.t('commentIsBest')}>
{ children }
</span>
);
/**
* Component that only renders children if the provided user prop can modify best tags
*/
export const IfUserCanModifyBest = ({user, children}) => {
if ( ! ( user && canModifyBestTag(user))) {return null;}
return children;
};
/**
* Button that lets a moderator tag a comment as "Best".
* Used to recognize really good comments.
*/
export class BestButton extends Component {
static propTypes = {
// whether the comment is already tagged as best
isBest: PropTypes.bool.isRequired,
// set that this comment is best
addBest: PropTypes.func.isRequired,
// remove the best status
removeBest: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.onClickAddBest = this.onClickAddBest.bind(this);
this.onClickRemoveBest = this.onClickRemoveBest.bind(this);
}
state = {
isSaving: false
}
async onClickAddBest(e) {
e.preventDefault();
const {addBest} = this.props;
if ( ! addBest) {
console.warn('BestButton#onClickAddBest called even though there is no addBest prop. doing nothing');
return;
}
this.setState({isSaving: true});
try {
await addBest();
} finally {
this.setState({isSaving: false});
}
}
async onClickRemoveBest(e) {
e.preventDefault();
const {removeBest} = this.props;
if ( ! removeBest) {
console.warn('BestButton#onClickAddBest called even though there is no removeBest prop. doing nothing');
return;
}
this.setState({isSaving: true});
try {
await removeBest();
} finally {
this.setState({isSaving: false});
}
}
render() {
const {isBest, addBest, removeBest} = this.props;
const {isSaving} = this.state;
const disabled = isSaving || ! (isBest ? removeBest : addBest);
return (
<button onClick={isBest ? this.onClickRemoveBest : this.onClickAddBest}
disabled={disabled}
className={classnames(`${name}-button`, `e2e__${isBest ? 'unset' : 'set'}-best-comment`)}
aria-label={lang.t(isBest ? 'unsetBest' : 'setBest')}>
<Icon name={ isBest ? 'star' : 'star_border' } />
</button>
);
}
}
@@ -0,0 +1,12 @@
{
"en": {
"setBest": "Tag as Best",
"unsetBest": "Untag as Best",
"commentIsBest": "This comment is one of the best"
},
"es": {
"like": "Establecer como mejor",
"liked": "Desarmado como mejor",
"commentIsBest": "Este comentario es uno de los mejores"
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ class LikeButton extends Component {
};
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
<button onClick={onLikeClick} className={`${name}-button ${liked ? 'likedButton' : ''}`}>
<span className={`${name}-button-text`}>{lang.t(liked ? 'liked' : 'like')}</span>
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
+2 -1
View File
@@ -1,13 +1,14 @@
import React, {PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import classnames from 'classnames';
const name = 'coral-plugin-replies';
const ReplyButton = ({banned, onClick}) => {
return (
<button
className={`${name}-reply-button`}
className={classnames(`${name}-reply-button`)}
onClick={onClick}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
+2 -2
View File
@@ -1,7 +1,7 @@
import React from 'react';
const TagLabel = ({isStaff}) => <div className='coral-plugin-tag-label'>
{isStaff ? 'Staff' : ''}
const TagLabel = ({children}) => <div className='coral-plugin-tag-label'>
{children}
</div>;
export default TagLabel;
+5 -5
View File
@@ -166,18 +166,18 @@
.type--ban {
display: block;
color: #616161;
border: solid 2px rgba(97, 97, 97, 0.77);
border: solid 1px rgba(97, 97, 97, 0.77);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 128px;
padding: 0 8px;
height: 34px;
font-size: 14px;
width: auto;
&:hover {
box-shadow: none;
+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';