mirror of
https://github.com/wassname/talk.git
synced 2026-07-11 00:52:26 +08:00
Implement AdminCommentContent for text and html
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { matchLinks } from '../utils';
|
||||
import matchLinks from 'coral-framework/utils/matchLinks';
|
||||
|
||||
export default ({ text, children }) => {
|
||||
const hasLinks = !!matchLinks(text);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Link } from 'react-router';
|
||||
import { Icon } from 'coral-ui';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import AdminCommentContent from 'coral-framework/components/AdminCommentContent';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
@@ -93,7 +93,7 @@ class UserDetailComment extends React.Component {
|
||||
'talk-admin-user-detail-comment'
|
||||
)}
|
||||
size={1}
|
||||
defaultComponent={CommentFormatter}
|
||||
defaultComponent={AdminCommentContent}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
<a
|
||||
|
||||
@@ -8,7 +8,7 @@ import styles from './Comment.css';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import AdminCommentContent from 'coral-framework/components/AdminCommentContent';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
@@ -140,7 +140,7 @@ class Comment extends React.Component {
|
||||
fill="adminCommentContent"
|
||||
className={cn(styles.commentContent, 'talk-admin-comment')}
|
||||
size={1}
|
||||
defaultComponent={CommentFormatter}
|
||||
defaultComponent={AdminCommentContent}
|
||||
passthrough={{ ...slotPassthrough, ...formatterSettings }}
|
||||
/>
|
||||
<div className={styles.commentContentFooter}>
|
||||
|
||||
+36
-47
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { matchLinks } from '../utils';
|
||||
import { matchLinks } from './index';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
@@ -35,6 +34,17 @@ function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
function nl2br(body, keyPrefix) {
|
||||
const tokens = body.split('\n').reduce((tokens, t, i) => {
|
||||
if (i !== 0) {
|
||||
tokens.push(<br key={`${keyPrefix}_${i}`} />);
|
||||
}
|
||||
tokens.push(t);
|
||||
return tokens;
|
||||
}, []);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
@@ -48,14 +58,20 @@ function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body) {
|
||||
function markLinks(body, keyPrefix) {
|
||||
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>);
|
||||
content.push(
|
||||
<mark key={`${keyPrefix}_${i}`}>
|
||||
<a href={match.url} target="_blank">
|
||||
{match.text}
|
||||
</a>
|
||||
</mark>
|
||||
);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
@@ -63,47 +79,20 @@ function markLinks(body) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const CommentFormatter = ({
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
className = 'comment',
|
||||
...rest
|
||||
}) => {
|
||||
// Breaking the body by line break
|
||||
const textbreaks = body.split('\n');
|
||||
export function renderText(body, suspectWords, bannedWords) {
|
||||
return nl2br(body).map((element, index) => {
|
||||
// Skip br tags.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
return markLinks(element, index).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`${className}-text`} {...rest}>
|
||||
{textbreaks.map((line, i) => {
|
||||
const content = markLinks(line).map((element, index) => {
|
||||
// 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 (
|
||||
<span key={i} className={`${className}-line`}>
|
||||
{content}
|
||||
{i !== textbreaks.length - 1 && (
|
||||
<br className={`${className}-linebreak`} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentFormatter.propTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array,
|
||||
suspectWords: PropTypes.array,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentFormatter;
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
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) =>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.content {
|
||||
a {
|
||||
color: #063b9a;
|
||||
text-decoration: underline;
|
||||
font-weight: 300;
|
||||
background-color: ##f4ff81;
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: ##f4ff81;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import matchLinks from '../utils/matchLinks';
|
||||
import memoize from 'lodash/memoize';
|
||||
import cn from 'classnames';
|
||||
import styles from './AdminCommentContent.css';
|
||||
|
||||
function escapeHTML(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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('|');
|
||||
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
} catch (_err) {
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
function nl2br(body, keyPrefix) {
|
||||
const tokens = body.split('\n').reduce((tokens, t, i) => {
|
||||
if (i !== 0) {
|
||||
tokens.push(<br key={`${keyPrefix}_${i}`} />);
|
||||
}
|
||||
tokens.push(t);
|
||||
return tokens;
|
||||
}, []);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// 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, keyPrefix) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(
|
||||
<a key={`${keyPrefix}_${i}`} href={match.url} target="_blank">
|
||||
{match.text}
|
||||
</a>
|
||||
);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
// markPhrasesHTML looks for `supsectWords` and `bannedWords` inside `text` and highlights them by returning
|
||||
// a HTML string.
|
||||
function markPhrasesHTML(text, suspectWords, bannedWords) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = text.split(regexp);
|
||||
if (tokens.length === 1) {
|
||||
return text;
|
||||
}
|
||||
return tokens
|
||||
.map(
|
||||
(token, i) =>
|
||||
i % 3 === 2 ? `<mark>${escapeHTML(token)}</mark>` : escapeHTML(token)
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// markHTMLNode manipulates the node by looking for #text nodes and adding markers
|
||||
// for `supsectWords` and `bannedWords`.
|
||||
function markHTMLNode(parentNode, suspectWords, bannedWords) {
|
||||
parentNode.childNodes.forEach(node => {
|
||||
if (node.nodeName === '#text') {
|
||||
const newContent = markPhrasesHTML(
|
||||
node.textContent,
|
||||
suspectWords,
|
||||
bannedWords
|
||||
);
|
||||
if (newContent !== node.textContent) {
|
||||
const newNode = document.createElement('span');
|
||||
newNode.innerHTML = newContent;
|
||||
parentNode.replaceChild(newNode, node);
|
||||
}
|
||||
} else {
|
||||
markHTMLNode(node, suspectWords, bannedWords);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// renderText performs all the marking of a text body and returns an array of React Elements.
|
||||
function renderText(body, suspectWords, bannedWords) {
|
||||
return nl2br(body).map((element, index) => {
|
||||
// Skip br tags.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
return markLinks(element, index).map((element, index) => {
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const commonPropTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array.isRequired,
|
||||
suspectWords: PropTypes.array.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const AdminCommentContentText = ({
|
||||
body,
|
||||
className,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.content)}>
|
||||
{renderText(body, suspectWords, bannedWords)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
AdminCommentContentText.propTypes = commonPropTypes;
|
||||
|
||||
const AdminCommentContentHTML = ({
|
||||
body,
|
||||
className,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
}) => {
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = body;
|
||||
markHTMLNode(node, suspectWords, bannedWords);
|
||||
return (
|
||||
<div
|
||||
className={cn(className, styles.content)}
|
||||
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
AdminCommentContentHTML.propTypes = commonPropTypes;
|
||||
|
||||
const AdminCommentContent = ({
|
||||
className,
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
html,
|
||||
}) => {
|
||||
const Component = html ? AdminCommentContentHTML : AdminCommentContentText;
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
body={body}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AdminCommentContent.propTypes = {
|
||||
...commonPropTypes,
|
||||
html: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AdminCommentContent;
|
||||
@@ -0,0 +1,8 @@
|
||||
import LinkifyIt from 'linkify-it';
|
||||
import tlds from 'tlds';
|
||||
const linkify = new LinkifyIt();
|
||||
linkify.tlds(tlds);
|
||||
|
||||
export default function matchLinks(text) {
|
||||
return linkify.match(text);
|
||||
}
|
||||
@@ -20,6 +20,9 @@ export {
|
||||
export {
|
||||
default as CommentContent,
|
||||
} from 'coral-framework/components/CommentContent';
|
||||
export {
|
||||
default as AdminCommentContent,
|
||||
} from 'coral-framework/components/AdminCommentContent';
|
||||
export {
|
||||
default as ConfigureCard,
|
||||
} from 'coral-framework/components/ConfigureCard';
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.content {
|
||||
composes: content from "./CommentContent.css";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './AdminCommentContent.css';
|
||||
import { AdminCommentContent as Content } from 'plugin-api/beta/client/components';
|
||||
|
||||
class AdminCommentContent extends React.Component {
|
||||
render() {
|
||||
const { comment, suspectWords, bannedWords } = this.props;
|
||||
return (
|
||||
<Content
|
||||
className={styles.content}
|
||||
body={comment.richTextBody ? comment.richTextBody : comment.body}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
html={!!comment.richTextBody}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdminCommentContent.propTypes = {
|
||||
comment: PropTypes.object.isRequired,
|
||||
suspectWords: PropTypes.array.isRequired,
|
||||
bannedWords: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default AdminCommentContent;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { gql } from 'react-apollo';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import AdminCommentContent from '../components/AdminCommentContent';
|
||||
|
||||
export default withFragments({
|
||||
comment: gql`
|
||||
fragment TalkPluginRichText_AdminCommentContent_comment on Comment {
|
||||
body
|
||||
richTextBody
|
||||
}
|
||||
`,
|
||||
})(AdminCommentContent);
|
||||
@@ -1,13 +1,15 @@
|
||||
import Editor from './containers/Editor';
|
||||
import CommentContent from './containers/CommentContent';
|
||||
import AdminCommentContent from './containers/AdminCommentContent';
|
||||
|
||||
import { gql } from 'react-apollo';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
draftArea: [Editor],
|
||||
commentContent: [CommentContent],
|
||||
adminCommentContent: [CommentContent],
|
||||
userDetailCommentContent: [CommentContent],
|
||||
adminCommentContent: [AdminCommentContent],
|
||||
userDetailCommentContent: [AdminCommentContent],
|
||||
},
|
||||
fragments: {
|
||||
CreateCommentResponse: gql`
|
||||
|
||||
Reference in New Issue
Block a user