mirror of
https://github.com/wassname/talk.git
synced 2026-08-12 12:30:39 +08:00
Merge branch 'master' into story/150636425
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user