Merge branch 'master' into story/150636425

This commit is contained in:
Chi Vinh Le
2017-09-15 19:11:31 +07:00
46 changed files with 574 additions and 472 deletions
@@ -1,3 +1,7 @@
.root {
position: relative;
}
.bodyLeave {
position: absolute;
width: 100%;
@@ -7,6 +7,7 @@ export default ({children, body}) => {
return (
<CSSTransitionGroup
component={'div'}
className={styles.root}
transitionName={{
enter: styles.bodyEnter,
enterActive: styles.bodyEnterActive,
@@ -1,25 +1,78 @@
import React from 'react';
import Highlighter from 'react-highlight-words';
import Linkify from 'react-linkify';
const linkify = new Linkify();
import {matchLinks} from '../utils';
import memoize from 'lodash/memoize';
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map((phrase) =>
phrase.split(/\s+/)
.map((word) => escapeRegExp(word))
.join('[\\s"?!.]+')
).join('|');
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
function getPhrasesRegexp(suspectWords, bannedWords) {
return generateRegExp([...suspectWords, ...bannedWords]);
}
// Memoized version as arguments rarely change.
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
// an array of React Elements.
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map((token, i) =>
i % 3 === 2
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
: token
);
}
// markLinks looks for links inside `body` and highlights them by returning
// an array of React Elements.
function markLinks(body) {
const matches = matchLinks(body);
const content = [];
let index = 0;
if (matches) {
matches
.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
export default ({suspectWords, bannedWords, body, ...rest}) => {
const links = linkify.getMatches(body);
const linkText = links ? links.map((link) => link.raw) : [];
// First highlight links.
const content = markLinks(body)
.map((element, index) => {
const searchWords = [
...suspectWords,
...bannedWords,
...linkText
];
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<Highlighter
{...rest}
autoEscape={true}
searchWords={searchWords}
textToHighlight={body}
/>
<div {...rest}>
{content}
</div>
);
};
@@ -1,9 +1,8 @@
import React from 'react';
import Linkify from 'react-linkify';
const linkify = new Linkify();
import {matchLinks} from '../utils';
export default ({text, children}) => {
const hasLinks = !!linkify.getMatches(text);
const hasLinks = !!matchLinks(text);
if (!hasLinks) {
return null;
@@ -127,7 +127,7 @@ class Comment extends React.Component {
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.itemBody}>
<p className={styles.body}>
<div className={styles.body}>
<CommentBodyHighlighter
suspectWords={suspectWords}
bannedWords={bannedWords}
@@ -141,7 +141,7 @@ class Comment extends React.Component {
>
<Icon name="open_in_new" /> {t('comment.view_context')}
</a>
</p>
</div>
<Slot
fill="adminCommentContent"
data={data}
@@ -0,0 +1,8 @@
import LinkifyIt from 'linkify-it';
import tlds from 'tlds';
export function createLinkify() {
const linkify = new LinkifyIt();
linkify.tlds(tlds);
return linkify;
}
+10
View File
@@ -1,4 +1,14 @@
import LinkifyIt from 'linkify-it';
import tlds from 'tlds';
const linkify = new LinkifyIt();
linkify.tlds(tlds);
export function matchLinks(text) {
return linkify.match(text);
}
export const isPremod = (mod) => mod === 'PRE';
export const getModPath = (type = 'all', assetId) =>
assetId ? `/admin/moderate/${type}/${assetId}` : `/admin/moderate/${type}`;
@@ -26,9 +26,9 @@ class StreamTabPanelContainer extends React.Component {
// it does not result in a change of slot children.
const changes = getShallowChanges(this.props, next);
if (changes.length === 1 && changes[0] === 'reduxState') {
const prevUuid = this.getSlotComponents(this.props.tabSlot, this.props).map((cmp) => cmp.talkUuid);
const nextUuid = this.getSlotComponents(next.tabSlot, next).map((cmp) => cmp.talkUuid);
return !isEqual(prevUuid, nextUuid);
const prevKeys = this.getSlotElements(this.props.tabSlot, this.props).map((el) => el.key);
const nextKeys = this.getSlotElements(next.tabSlot, next).map((el) => el.key);
return !isEqual(prevKeys, nextKeys);
}
// Prevent Slot from rerendering when no props has shallowly changed.
@@ -37,42 +37,33 @@ class StreamTabPanelContainer extends React.Component {
fallbackAllTab(props = this.props) {
if (props.activeTab !== props.fallbackTab) {
const slotPlugins = this.getSlotComponents(props.tabSlot, props).map((c) => c.talkPluginName);
const slotPlugins = this.getSlotElements(props.tabSlot, props).map((el) => el.type.talkPluginName);
if (slotPlugins.indexOf(props.activeTab) === -1) {
props.setActiveTab(props.fallbackTab);
}
}
}
getSlotComponents(slot, props = this.props) {
getSlotElements(slot, props = this.props) {
const {plugins} = this.context;
return plugins.getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData);
return plugins.getSlotElements(slot, props.reduxState, props.slotProps, props.queryData);
}
getPluginTabElements(props = this.props) {
const {plugins} = this.context;
return this.getSlotComponents(props.tabSlot).map((PluginComponent) => {
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
return this.getSlotElements(props.tabSlot).map((el) => {
return (
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...pluginProps}
active={this.props.activeTab === PluginComponent.talkPluginName}
/>
<Tab tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
{React.cloneElement(el, {active: this.props.activeTab === el.type.talkPluginName})}
</Tab>
);
});
}
getPluginTabPaneElements(props = this.props) {
const {plugins} = this.context;
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => {
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
return this.getSlotElements(props.tabPaneSlot).map((el) => {
return (
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...pluginProps}
/>
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
{el}
</TabPane>
);
});
+3 -3
View File
@@ -20,9 +20,9 @@ class Slot extends React.Component {
// it does not result in a change of slot children.
const changes = getShallowChanges(this.props, next);
if (changes.length === 1 && changes[0] === 'reduxState') {
const prevChildrenUuid = this.getChildren(this.props).map((child) => child.type.talkUuid);
const nextChildrenUuid = this.getChildren(next).map((child) => child.type.talkUuid);
return !isEqual(prevChildrenUuid, nextChildrenUuid);
const prevChildrenKeys = this.getChildren(this.props).map((child) => child.key);
const nextChildrenKeys = this.getChildren(next).map((child) => child.key);
return !isEqual(prevChildrenKeys, nextChildrenKeys);
}
// Prevent Slot from rerendering when no props has shallowly changed.
+37 -31
View File
@@ -8,7 +8,6 @@ import flatten from 'lodash/flatten';
import mapValues from 'lodash/mapValues';
import {getDisplayName} from 'coral-framework/helpers/hoc';
import camelize from '../helpers/camelize';
import uuid from 'uuid/v4';
// This is returned for pluginConfig when it is empty.
const emptyConfig = {};
@@ -63,9 +62,6 @@ function addMetaDataToSlotComponents(plugins) {
// Attach plugin name to the component
component.talkPluginName = plugin.name;
// Attach uuid to the component
component.talkUuid = uuid();
});
});
});
@@ -77,30 +73,8 @@ class PluginsService {
addMetaDataToSlotComponents(plugins);
}
getSlotComponents(slot, reduxState, props = {}, queryData = {}) {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
return flatten(this.plugins
// Filter out components that have slots and have been disabled in `plugin_config`
.filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components))
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot])
)
.filter((component) => {
if(!component.isExcluded) {
return true;
}
let resolvedProps = this.getSlotComponentProps(component, reduxState, props, queryData);
if (component.mapStateToProps) {
resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)};
}
return !component.isExcluded(resolvedProps);
});
}
isSlotEmpty(slot, reduxState, props = {}, queryData = {}) {
return this.getSlotComponents(slot, reduxState, props, queryData).length === 0;
return this.getSlotElements(slot, reduxState, props, queryData).length === 0;
}
/**
@@ -124,10 +98,42 @@ class PluginsService {
* Returns React Elements for given slot.
*/
getSlotElements(slot, reduxState, props = {}, queryData = {}) {
return this.getSlotComponents(slot, reduxState, props, queryData)
.map((component, i) => {
return React.createElement(component, {key: i, ...this.getSlotComponentProps(component, reduxState, props, queryData)});
});
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
const isDisabled = (component) => {
if (
pluginConfig &&
pluginConfig[component.talkPluginName] &&
pluginConfig[component.talkPluginName].disable_components
) {
return true;
}
// Check if component is excluded.
if(component.isExcluded) {
let resolvedProps = this.getSlotComponentProps(component, reduxState, props, queryData);
if (component.mapStateToProps) {
resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)};
}
return component.isExcluded(resolvedProps);
}
return false;
};
return flatten(this.plugins
.filter((o) => o.module.slots && o.module.slots[slot])
.map((o) => o.module.slots[slot])
)
.map((component, i) => ({
component,
disabled: isDisabled(component),
key: i,
}))
.filter((o) => !o.disabled)
.map(({component, key}) =>
React.createElement(component, {key, ...this.getSlotComponentProps(component, reduxState, props, queryData)})
);
}
getSlotFragments(slot, part) {