mirror of
https://github.com/wassname/talk.git
synced 2026-08-12 12:30:39 +08:00
Merged branch master into admin-login-upgrade
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
|
||||
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
|
||||
## Contributing to Talk
|
||||
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import React from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import BanUserButton from './BanUserButton';
|
||||
import {FabButton} from 'coral-ui';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
if (type === 'BAN') {
|
||||
return <BanUserButton user={user} onClick={() => props.showBanUserDialog(props.user, props.id)} />;
|
||||
}
|
||||
|
||||
const ActionButton = ({type = '', ...props}) => {
|
||||
return (
|
||||
<FabButton
|
||||
<Button
|
||||
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||
cStyle={type.toLowerCase()}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||
/>
|
||||
>{menuActionsMap[type].text}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
-webkit-transform: scale(.8);
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
|
||||
@@ -8,7 +8,7 @@ const lang = new I18n(translations);
|
||||
|
||||
const BanUserButton = ({user, ...props}) => (
|
||||
<div className={styles.ban}>
|
||||
<Button cStyle='darkGrey'
|
||||
<Button cStyle='ban'
|
||||
className={`ban ${styles.banButton}`}
|
||||
disabled={user.status === 'BANNED' ? 'disabled' : ''}
|
||||
onClick={props.onClick}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
background: #E5E5E5;
|
||||
height: 100%;
|
||||
width: 128px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.base {
|
||||
|
||||
@@ -80,6 +80,7 @@ class ModerationContainer extends Component {
|
||||
<ModerationQueue
|
||||
currentAsset={asset}
|
||||
comments={comments}
|
||||
activeTab={activeTab}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
|
||||
@@ -19,6 +19,7 @@ const ModerationQueue = ({comments, ...props}) => {
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
commentType={props.activeTab}
|
||||
suspectWords={props.suspectWords}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
|
||||
@@ -6,8 +6,10 @@ import {Link} from 'react-router';
|
||||
|
||||
import styles from './styles.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import ActionButton from '../../../components/ActionButton';
|
||||
import FlagBox from './FlagBox';
|
||||
import CommentType from './CommentType';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import BanUserButton from 'coral-admin/src/components/BanUserButton';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
@@ -21,46 +23,48 @@ const Comment = ({actions = [], ...props}) => {
|
||||
return (
|
||||
<li tabIndex={props.index}
|
||||
className={`mdl-card mdl-shadow--2dp ${styles.Comment} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<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>
|
||||
{props.comment.action_summaries ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
{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})}
|
||||
showBanUserDialog={() => props.showBanUserDialog(props.comment.user, props.comment.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
: null}
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
{!props.currentAsset && (
|
||||
<div className={styles.moderateArticle}>
|
||||
Article: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate Article</Link>
|
||||
{!props.currentAsset && (
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate →</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>
|
||||
)}
|
||||
<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>
|
||||
{actionSummaries && <FlagBox actionSummaries={actionSummaries} />}
|
||||
</li>
|
||||
@@ -72,7 +76,6 @@ Comment.propTypes = {
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
comment: PropTypes.shape({
|
||||
body: PropTypes.string.isRequired,
|
||||
action_summaries: PropTypes.array,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.commentType {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 11px;
|
||||
color: white;
|
||||
background: grey;
|
||||
padding: 2px 13px;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
line-height: 29px;
|
||||
padding-left: 26px;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.premod {
|
||||
background: #063B9A;
|
||||
}
|
||||
|
||||
&.flagged {
|
||||
background: #d03235;
|
||||
}
|
||||
|
||||
&.no-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentType.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const CommentType = props => {
|
||||
const typeData = getTypeData(props.type);
|
||||
|
||||
return (
|
||||
<span className={`${styles.commentType} ${styles[typeData.className]}`}>
|
||||
<Icon name={typeData.icon}/>{typeData.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeData = type => {
|
||||
switch (type) {
|
||||
case 'premod':
|
||||
return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'};
|
||||
case 'flagged':
|
||||
return {icon: 'flag', text: 'Flagged', className: 'flagged'};
|
||||
default:
|
||||
return {icon: 'flag', className: 'no-type'};
|
||||
}
|
||||
};
|
||||
|
||||
CommentType.propTypes = {
|
||||
type: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default CommentType;
|
||||
@@ -0,0 +1,55 @@
|
||||
.flagBox {
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 20px 16px;
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
.moreDetail {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
margin-top: 8px;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
i {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
}
|
||||
ul {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,47 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './styles.css';
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
|
||||
const FlagBox = props => (
|
||||
<div className={styles.flagBox}>
|
||||
<h3>Flags:</h3>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
class FlagBox extends Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return (
|
||||
<div className={styles.flagBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name='flag'/><h3>Flags ({props.actionSummaries.length}):</h3>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
{/* <a onClick={this.toggleDetail} className={styles.moreDetail}>More detail</a>*/}
|
||||
</div>
|
||||
{this.state.showDetail && (<div className={styles.detail}>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FlagBox.propTypes = {
|
||||
actionSummaries: PropTypes.array.isRequired
|
||||
|
||||
@@ -162,16 +162,21 @@ span {
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
margin-top: 0;
|
||||
padding: 4px 0 0;
|
||||
min-height: 220px;
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
@@ -194,7 +199,7 @@ span {
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
padding: 100px 12px 65px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -213,6 +218,8 @@ span {
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -228,7 +235,7 @@ span {
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
@@ -310,20 +317,26 @@ span {
|
||||
}
|
||||
|
||||
.Comment {
|
||||
|
||||
.moderateArticle {
|
||||
font-size: 12px;
|
||||
font-size: 15px;
|
||||
margin-top: 14px;
|
||||
font-weight: 500;
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #679af3;
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 0px;
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -331,16 +344,6 @@ span {
|
||||
}
|
||||
}
|
||||
|
||||
.flagBox {
|
||||
max-width: 480px;
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.selectField {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
|
||||
+8
-7
@@ -1,13 +1,14 @@
|
||||
export const actionsMap = {
|
||||
PREMOD: ['REJECT', 'APPROVE', 'BAN'],
|
||||
FLAGGED: ['REJECT', 'APPROVE', 'BAN'],
|
||||
REJECTED: ['APPROVE']
|
||||
PREMOD: ['APPROVE', 'REJECT'],
|
||||
FLAGGED: ['APPROVE', 'REJECT'],
|
||||
REJECTED: ['APPROVE', 'REJECTED']
|
||||
};
|
||||
|
||||
export const menuActionsMap = {
|
||||
'REJECT': {status: 'REJECTED', icon: 'close', key: 'r'},
|
||||
'APPROVE': {status: 'ACCEPTED', icon: 'done', key: 't'},
|
||||
'FLAGGED': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||
'BAN': {status: 'BANNED', icon: 'not interested'},
|
||||
'REJECT': {status: 'REJECTED', text: 'Reject', icon: 'close', key: 'r'},
|
||||
'REJECTED': {status: 'REJECTED', text: 'Rejected', icon: 'close'},
|
||||
'APPROVE': {status: 'ACCEPTED', text: 'Approve', icon: 'done', key: 't'},
|
||||
'FLAGGED': {status: 'FLAGGED', text: 'Flag', icon: 'flag', filter: 'Untouched'},
|
||||
'BAN': {status: 'BANNED', text: 'Ban User', icon: 'not interested'},
|
||||
'': {icon: 'done'}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
addTypename: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
|
||||
import pym from 'pym.js';
|
||||
|
||||
// This function should return value of window.Coral
|
||||
const Coral = {};
|
||||
const Talk = Coral.Talk = {};
|
||||
|
||||
// build the URL to load in the pym iframe
|
||||
function buildStreamIframeUrl(talkBaseUrl, asset, comment) {
|
||||
let iframeArray = [
|
||||
talkBaseUrl,
|
||||
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
|
||||
'embed/stream?asset_url=',
|
||||
encodeURIComponent(asset)
|
||||
];
|
||||
|
||||
if (comment) {
|
||||
iframeArray.push('&comment_id=');
|
||||
iframeArray.push(encodeURIComponent(comment));
|
||||
}
|
||||
return iframeArray.join('');
|
||||
}
|
||||
|
||||
// Set up postMessage listeners/handlers on the pymParent
|
||||
// e.g. to resize the iframe, and navigate the host page
|
||||
function configurePymParent(pymParent, assetUrl) {
|
||||
let notificationOffset = 200;
|
||||
let ready = false;
|
||||
|
||||
// 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`;
|
||||
});
|
||||
|
||||
// Helps child show notifications at the right scrollTop
|
||||
pymParent.onMessage('getPosition', function() {
|
||||
let position = viewport().height + document.body.scrollTop;
|
||||
|
||||
if (position > notificationOffset) {
|
||||
position = position - notificationOffset;
|
||||
}
|
||||
|
||||
pymParent.sendMessage('position', position);
|
||||
});
|
||||
|
||||
// Tell child when parent's DOMContentLoaded
|
||||
pymParent.onMessage('childReady', function () {
|
||||
const interval = setInterval(function () {
|
||||
if (ready) {
|
||||
window.clearInterval(interval);
|
||||
|
||||
// @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place?
|
||||
pymParent.sendMessage('DOMContentLoaded', assetUrl);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// When end-user clicks link in iframe, open it in parent context
|
||||
pymParent.onMessage('navigate', function (url) {
|
||||
window.open(url, '_blank').focus();
|
||||
});
|
||||
|
||||
// wait till images and other iframes are loaded before scrolling the page.
|
||||
// or do we want to be more aggressive and scroll when we hit DOM ready?
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
ready = true;
|
||||
});
|
||||
|
||||
// get dimensions of viewport
|
||||
const viewport = () => {
|
||||
let e = window, a = 'inner';
|
||||
if ( !( 'innerWidth' in window ) ){
|
||||
a = 'client';
|
||||
e = document.documentElement || document.body;
|
||||
}
|
||||
return {
|
||||
width : e[`${a}Width`],
|
||||
height : e[`${a}Height`]
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Talk stream
|
||||
* @param {HTMLElement} el - Element to render the stream in
|
||||
* @param {Object} opts - Configuration options for talk
|
||||
* @param {String} opts.talk - Talk base URL
|
||||
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
|
||||
* @param {String} [opts.asset] - parent Asset ID or URL. Comments in the
|
||||
* stream will replies to this asset
|
||||
*/
|
||||
Talk.render = function (el, opts) {
|
||||
if ( ! el) {
|
||||
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
|
||||
}
|
||||
if (typeof el !== 'object') {
|
||||
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`);
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
// @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
|
||||
if (! opts.talk) {
|
||||
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
|
||||
}
|
||||
|
||||
// ensure el has an id, as pym can't directly accept the HTMLElement
|
||||
if (!el.id) {
|
||||
el.id = `_${Math.random()}`;
|
||||
}
|
||||
|
||||
let asset = opts.asset || window.location.href.split('#')[0];
|
||||
let comment = window.location.hash.slice(1);
|
||||
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset, comment), {
|
||||
title: opts.title,
|
||||
asset_url: asset,
|
||||
id: `${el.id}_iframe`,
|
||||
name: `${el.id}_iframe`
|
||||
});
|
||||
|
||||
configurePymParent(pymParent, asset);
|
||||
};
|
||||
|
||||
export default Coral;
|
||||
@@ -110,6 +110,82 @@
|
||||
background: #00a291;
|
||||
}
|
||||
|
||||
.type--approve {
|
||||
display: block;
|
||||
color: #519954;
|
||||
border: solid 2px rgba(81, 153, 84, 0.75);
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
}
|
||||
}
|
||||
|
||||
.type--reject, .type--rejected {
|
||||
display: block;
|
||||
color: #D03235;
|
||||
border: solid 1px #D03235;
|
||||
background: white;
|
||||
padding: 10px 11px;
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.type--rejected {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.type--ban {
|
||||
display: block;
|
||||
color: #616161;
|
||||
border: solid 2px 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;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #616161;
|
||||
}
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
+3
-2
@@ -6,8 +6,8 @@
|
||||
"scripts": {
|
||||
"start": "./bin/cli serve --jobs",
|
||||
"dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"",
|
||||
"build": "NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"build-watch": "NODE_ENV=development webpack --config webpack.config.js --watch",
|
||||
"build": "NODE_ENV=production webpack --progress -p --config webpack.config.js --bail",
|
||||
"build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"lint": "eslint bin/* .",
|
||||
"lint-fix": "eslint bin/* . --fix",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
@@ -130,6 +130,7 @@
|
||||
"jsdom": "^9.8.3",
|
||||
"json-loader": "^0.5.4",
|
||||
"keymaster": "^1.6.2",
|
||||
"license-webpack-plugin": "^0.4.2",
|
||||
"material-design-lite": "^1.2.1",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-junit-reporter": "^1.12.1",
|
||||
|
||||
+13
-9
@@ -5,16 +5,20 @@ const router = express.Router();
|
||||
router.use('/api/v1', require('./api'));
|
||||
router.use('/admin', require('./admin'));
|
||||
router.use('/embed', require('./embed'));
|
||||
router.get('/embed.js', (req, res) => res.sendFile(path.join(__dirname, '../client/coral-embed/index.js')));
|
||||
router.use('/assets', require('./assets'));
|
||||
router.get('/embed.js', (req, res) => res.sendFile(path.join(__dirname, '../dist/embed.js')));
|
||||
router.get('/embed.js.map', (req, res) => res.sendFile(path.join(__dirname, '../dist/embed.js.map')));
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
return res.render('article', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
body: '',
|
||||
basePath: '/client/embed/stream'
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
router.use('/assets', require('./assets'));
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
return res.render('article', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
body: '',
|
||||
basePath: '/client/embed/stream'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+22
-4
@@ -2,6 +2,7 @@ const path = require('path');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const precss = require('precss');
|
||||
const Copy = require('copy-webpack-plugin');
|
||||
const LicenseWebpackPlugin = require('license-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// Edit the build targets and embeds below.
|
||||
@@ -17,7 +18,12 @@ const buildEmbeds = [
|
||||
|
||||
module.exports = {
|
||||
devtool: 'cheap-source-map',
|
||||
entry: buildTargets.reduce((entry, target) => {
|
||||
entry: Object.assign({}, {
|
||||
'embed': [
|
||||
'babel-polyfill',
|
||||
path.join(__dirname, 'client/coral-embed/src/index')
|
||||
]
|
||||
}, buildTargets.reduce((entry, target) => {
|
||||
|
||||
// Add the entry for the bundle.
|
||||
entry[`${target}/bundle`] = [
|
||||
@@ -26,7 +32,7 @@ module.exports = {
|
||||
];
|
||||
|
||||
return entry;
|
||||
}, buildEmbeds.reduce((entry, embed) => {
|
||||
}, {}), buildEmbeds.reduce((entry, embed) => {
|
||||
|
||||
// Add the entry for the bundle.
|
||||
entry[`embed/${embed}/bundle`] = [
|
||||
@@ -38,14 +44,22 @@ module.exports = {
|
||||
}, {})),
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
publicPath: '/client/',
|
||||
filename: '[name].js',
|
||||
|
||||
// NOTE: this causes all exports to override the global.Coral, so no more
|
||||
// than one bundle.js can be included on a page.
|
||||
library: 'Coral'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
test: /\.js$/
|
||||
test: /\.js$/,
|
||||
query: {
|
||||
cacheDirectory: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'json-loader',
|
||||
@@ -80,6 +94,10 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new LicenseWebpackPlugin({
|
||||
pattern: /^(MIT|ISC|BSD.*)$/,
|
||||
addUrl: true
|
||||
}),
|
||||
new Copy([
|
||||
...buildEmbeds.map(embed => ({
|
||||
from: path.join(__dirname, 'client', `coral-embed-${embed}`, 'style'),
|
||||
|
||||
@@ -4385,6 +4385,12 @@ libqp@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8"
|
||||
|
||||
license-webpack-plugin@^0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-0.4.2.tgz#188df5418a72ab61cad648b9ffd9355be270242b"
|
||||
dependencies:
|
||||
object-assign "^4.1.0"
|
||||
|
||||
linkify-it@^1.2.0:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a"
|
||||
|
||||
Reference in New Issue
Block a user