diff --git a/bin/cli-assets b/bin/cli-assets index 78229d91b..210b9f22e 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -13,6 +13,7 @@ const CommentModel = require('../models/comment'); const AssetsService = require('../services/assets'); const mongoose = require('../services/mongoose'); const scraper = require('../services/scraper'); +const Context = require('../graph/context'); const inquirer = require('inquirer'); const { URL } = require('url'); @@ -52,22 +53,27 @@ async function refreshAssets(ageString) { const ageMs = parseDuration(ageString); const age = new Date(now - ageMs); - let assets = await AssetModel.find({ - $or: [ - { - scraped: { - $lte: age, + let assets = await AssetModel.find( + { + $or: [ + { + scraped: { + $lte: age, + }, }, - }, - { - scraped: null, - }, - ], - }); + { + scraped: null, + }, + ], + }, + { id: 1 } + ); + + // Create a graph context. + const ctx = Context.forSystem(); // Queue all the assets for scraping. - await Promise.all(assets.map(scraper.create)); - + await Promise.all(assets.map(({ id }) => scraper.create(ctx, id))); console.log('Assets were queued to be scraped'); util.shutdown(); } catch (e) { diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9d68d592b..28255aeb5 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -2,10 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Router, Route, IndexRedirect, IndexRoute } from 'react-router'; -import Configure from 'routes/Configure'; import Install from 'routes/Install'; import Stories from 'routes/Stories'; import Community from 'routes/Community'; + +import Configure from 'routes/Configure'; +import StreamSettings from './routes/Configure/containers/StreamSettings'; +import ModerationSettings from './routes/Configure/containers/ModerationSettings'; +import TechSettings from './routes/Configure/containers/TechSettings'; + import { ModerationLayout, Moderation } from 'routes/Moderation'; import Layout from 'containers/Layout'; @@ -15,7 +20,14 @@ const routes = ( - + + + + + + + + {/* Community Routes */} diff --git a/client/coral-admin/src/actions/configure.js b/client/coral-admin/src/actions/configure.js index acc30be1b..47c7fd25f 100644 --- a/client/coral-admin/src/actions/configure.js +++ b/client/coral-admin/src/actions/configure.js @@ -8,6 +8,10 @@ export const clearPending = () => { return { type: actions.CLEAR_PENDING }; }; -export const setActiveSection = section => { - return { type: actions.SET_ACTIVE_SECTION, section }; +export const showSaveDialog = () => { + return { type: actions.SHOW_SAVE_DIALOG }; +}; + +export const hideSaveDialog = () => { + return { type: actions.HIDE_SAVE_DIALOG }; }; diff --git a/client/coral-admin/src/constants/configure.js b/client/coral-admin/src/constants/configure.js index 05673b5aa..9ab22580d 100644 --- a/client/coral-admin/src/constants/configure.js +++ b/client/coral-admin/src/constants/configure.js @@ -2,4 +2,6 @@ const prefix = 'TALK_ADMIN_CONFIGURE'; export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`; export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`; -export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`; + +export const SHOW_SAVE_DIALOG = `${prefix}_SHOW_SAVE_DIALOG`; +export const HIDE_SAVE_DIALOG = `${prefix}_HIDE_SAVE_DIALOG`; diff --git a/client/coral-admin/src/reducers/configure.js b/client/coral-admin/src/reducers/configure.js index 9809b0fa9..c87463423 100644 --- a/client/coral-admin/src/reducers/configure.js +++ b/client/coral-admin/src/reducers/configure.js @@ -6,11 +6,23 @@ const initialState = { canSave: false, pending: {}, errors: {}, - activeSection: 'stream', + saveDialog: false, }; export default function configure(state = initialState, action) { switch (action.type) { + case actions.SHOW_SAVE_DIALOG: { + return { + ...state, + saveDialog: true, + }; + } + case actions.HIDE_SAVE_DIALOG: { + return { + ...state, + saveDialog: false, + }; + } case actions.UPDATE_PENDING: { let next = state; if (action.updater) { @@ -40,11 +52,8 @@ export default function configure(state = initialState, action) { pending: {}, canSave: false, }; - case actions.SET_ACTIVE_SECTION: - return { - ...state, - activeSection: action.section, - }; + default: + return state; } return state; } diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index efd428378..4d88f8420 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -1,50 +1,37 @@ -import React, { Component } from 'react'; - -import { Button, List, Item } from 'coral-ui'; -import styles from './Configure.css'; -import StreamSettings from '../containers/StreamSettings'; -import ModerationSettings from '../containers/ModerationSettings'; -import TechSettings from '../containers/TechSettings'; -import t from 'coral-framework/services/i18n'; -import { can } from 'coral-framework/services/perms'; +import React from 'react'; import PropTypes from 'prop-types'; +import t from 'coral-framework/services/i18n'; +import { Button, List, Item } from 'coral-ui'; +import { can } from 'coral-framework/services/perms'; +import styles from './Configure.css'; +import SaveChangesDialog from './SaveChangesDialog'; -export default class Configure extends Component { - getSectionComponent(section) { - switch (section) { - case 'stream': - return StreamSettings; - case 'moderation': - return ModerationSettings; - case 'tech': - return TechSettings; - } - throw new Error(`Unknown section ${section}`); - } - +class Configure extends React.Component { render() { - const { - currentUser, - canSave, - savePending, - setActiveSection, - activeSection, - } = this.props; - const SectionComponent = this.getSectionComponent(activeSection); + const { canSave, currentUser, root, savePending, settings } = this.props; if (!can(currentUser, 'UPDATE_CONFIG')) { - return ( -

- You must be an administrator to access config settings. Please find - the nearest Admin and ask them to level you up! -

- ); + return

{t('configure.access_message')}

; } + const passProps = { + root, + settings, + }; + return (
+
- + {t('configure.stream_settings')} @@ -74,10 +61,7 @@ export default class Configure extends Component {
- + {React.cloneElement(this.props.children, passProps)}
); @@ -86,10 +70,17 @@ export default class Configure extends Component { Configure.propTypes = { savePending: PropTypes.func.isRequired, + saveChanges: PropTypes.func.isRequired, + discardChanges: PropTypes.func.isRequired, currentUser: PropTypes.object.isRequired, root: PropTypes.object.isRequired, settings: PropTypes.object.isRequired, canSave: PropTypes.bool.isRequired, - setActiveSection: PropTypes.func.isRequired, + handleSectionChange: PropTypes.func.isRequired, activeSection: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + saveDialog: PropTypes.bool, + hideSaveDialog: PropTypes.func.isRequired, }; + +export default Configure; diff --git a/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.css b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.css new file mode 100644 index 000000000..f66a92e44 --- /dev/null +++ b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.css @@ -0,0 +1,40 @@ +.buttonActions { + padding-top: 15px; + text-align: right; +} + +.dialog { + padding: 25px; + min-width: 400px; +} + +.close { + font-size: 20px; + line-height: 14px; + top: 10px; + right: 10px; + position: absolute; + display: block; + font-weight: bold; + color: #363636; + cursor: pointer; +} + +.title { + font-size: 18px; + font-weight: 800; + margin-bottom: 20px; +} + +.cancel { + color: #363636; + margin-right: 15px; + display: inline-block; + &:hover { + cursor: pointer; + } +} + +.button { + margin-left: 5px; +} \ No newline at end of file diff --git a/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.js b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.js new file mode 100644 index 000000000..fca87d2cd --- /dev/null +++ b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import { Button, Dialog } from 'coral-ui'; +import styles from './SaveChangesDialog.css'; +import t from 'coral-framework/services/i18n'; + +const SaveChangesDialog = ({ + saveDialog, + hideSaveDialog, + saveChanges, + discardChanges, +}) => ( + + + × + +
+ {t('configure.save_changes_dialog.unsaved_changes')} +
+ {t('configure.save_changes_dialog.copy')} +
+ + Cancel + + + +
+
+); + +SaveChangesDialog.propTypes = { + saveDialog: PropTypes.bool.isRequired, + hideSaveDialog: PropTypes.func.isRequired, + saveChanges: PropTypes.func.isRequired, + discardChanges: PropTypes.func.isRequired, +}; + +export default SaveChangesDialog; diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index ce6ea0627..9f980d31b 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { compose, gql } from 'react-apollo'; @@ -10,15 +10,70 @@ import { getDefinitionName } from 'coral-framework/utils'; import StreamSettings from './StreamSettings'; import TechSettings from './TechSettings'; import ModerationSettings from './ModerationSettings'; -import { clearPending, setActiveSection } from '../../../actions/configure'; +import { + clearPending, + showSaveDialog, + hideSaveDialog, +} from '../../../actions/configure'; import Configure from '../components/Configure'; +import { withRouter } from 'react-router'; + +class ConfigureContainer extends React.Component { + state = { nextRoute: '' }; -class ConfigureContainer extends Component { savePending = async () => { await this.props.updateSettings(this.props.pending); this.props.clearPending(); }; + saveChanges = async () => { + await this.savePending(); + this.props.hideSaveDialog(); + this.gotoNextRoute(); + }; + + discardChanges = async () => { + await this.props.clearPending(); + this.props.hideSaveDialog(); + this.gotoNextRoute(); + }; + + gotoNextRoute = () => { + const { nextRoute } = this.state; + if (nextRoute) { + this.props.router.push(nextRoute); + this.setState({ nextRoute: '' }); + } + }; + + handleSectionChange = async section => { + const nextRoute = `/admin/configure/${section}`; + + if (this.shouldShowSaveDialog()) { + await this.setState({ nextRoute }); + this.props.showSaveDialog(); + } else { + // Just go to the section + this.props.router.push(nextRoute); + } + }; + + shouldShowSaveDialog = () => { + return !!Object.keys(this.props.pending).length; + }; + + routeLeave = ({ pathname }) => { + if (this.shouldShowSaveDialog()) { + this.setState({ nextRoute: pathname }); + this.props.showSaveDialog(); + return false; + } + }; + + componentDidMount() { + this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave); + } + render() { if (this.props.data.error) { return
{this.props.data.error.message}
; @@ -30,14 +85,20 @@ class ConfigureContainer extends Component { return ( + > + {this.props.children} + ); } } @@ -74,18 +135,21 @@ const mapStateToProps = state => ({ pending: state.configure.pending, canSave: state.configure.canSave, activeSection: state.configure.activeSection, + saveDialog: state.configure.saveDialog, }); const mapDispatchToProps = dispatch => bindActionCreators( { clearPending, - setActiveSection, + showSaveDialog, + hideSaveDialog, }, dispatch ); export default compose( + withRouter, connect(mapStateToProps, mapDispatchToProps), withUpdateSettings, withConfigureQuery, @@ -93,14 +157,20 @@ export default compose( )(ConfigureContainer); ConfigureContainer.propTypes = { + activeSection: PropTypes.string, updateSettings: PropTypes.func.isRequired, clearPending: PropTypes.func.isRequired, - setActiveSection: PropTypes.func.isRequired, + showSaveDialog: PropTypes.func.isRequired, + hideSaveDialog: PropTypes.func.isRequired, + saveDialog: PropTypes.bool.isRequired, currentUser: PropTypes.object.isRequired, data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, canSave: PropTypes.bool.isRequired, pending: PropTypes.object.isRequired, mergedSettings: PropTypes.object.isRequired, - activeSection: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + router: PropTypes.object, + route: PropTypes.object, + routes: PropTypes.array, }; diff --git a/client/coral-embed-stream/src/tabs/profile/components/Comment.js b/client/coral-embed-stream/src/tabs/profile/components/Comment.js index be13c2c50..171172dab 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/profile/components/Comment.js @@ -34,6 +34,7 @@ class Comment extends React.Component { defaultComponent={CommentContent} className={cn(styles.commentBody, 'my-comment-body')} passthrough={slotPassthrough} + size={1} />
(dispatch, _, { localStorage }) => { localStorage.setItem('token', token); } + // Dispatch the set auth token action. For some browsers and situations, we + // may not be able to persist the auth token any other way. Keep it in redux! + dispatch({ type: actions.SET_AUTH_TOKEN, token }); + dispatch(checkLogin()); }; @@ -87,6 +91,7 @@ export const handleSuccessfulLogin = (user, token) => ( dispatch({ type: actions.HANDLE_SUCCESSFUL_LOGIN, user, + token, }); }; diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index 6318e6447..1707a83b6 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -7,7 +7,7 @@ class IfSlotIsNotEmpty extends React.Component { isSlotEmpty(props = this.props) { const { slotElements } = props; return slotElements.length === 0 - ? false + ? true : slotElements.every(elements => elements.length === 0); } @@ -19,6 +19,7 @@ class IfSlotIsNotEmpty extends React.Component { IfSlotIsNotEmpty.propTypes = { slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + slotElements: PropTypes.array.isRequired, children: PropTypes.node.isRequired, passthrough: PropTypes.object.isRequired, }; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 25e2f6a78..b340924c3 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -1,5 +1,7 @@ const prefix = `TALK_FRAMEWORK`; +export const SET_AUTH_TOKEN = `${prefix}_SET_AUTH_TOKEN`; + export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`; export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`; export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 3af606d0b..89478e30d 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -5,6 +5,7 @@ const initialState = { checkedInitialLogin: false, initialLoginError: null, user: null, + token: null, }; const purge = user => { @@ -14,12 +15,18 @@ const purge = user => { export default function auth(state = initialState, action) { switch (action.type) { + case actions.SET_AUTH_TOKEN: + return { + ...state, + token: action.token || null, + }; case actions.CHECK_LOGIN_FAILURE: return { ...state, initialLoginError: action.error, checkedInitialLogin: true, user: null, + token: null, }; case actions.CHECK_LOGIN_SUCCESS: return { @@ -31,11 +38,13 @@ export default function auth(state = initialState, action) { return { ...state, user: action.user ? purge(action.user) : null, + token: action.token || null, }; case actions.LOGOUT: return { ...state, user: null, + token: null, }; case actions.UPDATE_STATUS: { return { diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index e0bfbb950..9bfec2acd 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -47,6 +47,12 @@ const getAuthToken = (store, storage) => { } else if (!bowser.safari && !bowser.ios && storage) { // Use local storage auth tokens where there's a stable api. return storage.getItem('token'); + } else if (state.auth && state.auth.token) { + // Use the redux token state if the remaining methods fall out. If the embed + // is called with `embed.login(token)`, and the browser is not capable of + // storing the token in localStorage, then we would have persisted it to the + // redux state. + return state.auth.token; } return null; @@ -123,7 +129,7 @@ export async function createContext({ // Try to get the token from localStorage. If it isn't here, it may // be passed as a cookie. - // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT + // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERENT // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. return getAuthToken(store, localStorage); }; diff --git a/docs/source/01-01-talk-quickstart.md b/docs/source/01-01-talk-quickstart.md index c18aea638..7e2adb2fe 100644 --- a/docs/source/01-01-talk-quickstart.md +++ b/docs/source/01-01-talk-quickstart.md @@ -167,14 +167,10 @@ TALK_REDIS_URL=redis://127.0.0.1:6379 TALK_ROOT_URL=http://127.0.0.1:3000 TALK_PORT=3000 TALK_JWT_SECRET=password -TALK_FACEBOOK_APP_ID=A-Facebook-App-ID -TALK_FACEBOOK_APP_SECRET=A-Facebook-App-Secret ``` This is only the bare minimum needed to run the demo, for more configuration -variables, check out the [Configuration](/talk/configuration/) section. Facebook login above -will definitely not work unless you change those values as well. - +variables, check out the [Configuration](/talk/configuration/) section. You can now start the application by running: diff --git a/docs/source/01-03-installation-from-source.md b/docs/source/01-03-installation-from-source.md index a460751f7..9f4b96d93 100644 --- a/docs/source/01-03-installation-from-source.md +++ b/docs/source/01-03-installation-from-source.md @@ -57,14 +57,11 @@ TALK_REDIS_URL=redis://127.0.0.1:6379 TALK_ROOT_URL=http://127.0.0.1:3000 TALK_PORT=3000 TALK_JWT_SECRET=password -TALK_FACEBOOK_APP_ID=A-Facebook-App-ID -TALK_FACEBOOK_APP_SECRET=A-Facebook-App-Secret ``` This is the bare minimum needed to start Talk, for more configuration variables, check out the [Configuration](/talk/configuration/) -section. Facebook login above will definitely not work unless you change those -values as well. +section. You can now start the application by running: diff --git a/docs/source/03-02-product-guide-commenter-features.md b/docs/source/03-02-product-guide-commenter-features.md index 390fdce50..3d60eeda9 100644 --- a/docs/source/03-02-product-guide-commenter-features.md +++ b/docs/source/03-02-product-guide-commenter-features.md @@ -24,7 +24,7 @@ All levels of comments and replies are able to be linked to via permalink. Perma ```text https://?commentId= ``` -{:.no-copy} + ### Threading @@ -44,7 +44,7 @@ talk-stream-comment-level-${depth} talk-stream-highlighted-comment talk-stream-pending-comment ``` -{:.no-copy} + ### Automatic Updates diff --git a/locales/en.yml b/locales/en.yml index 08d64ddc8..6b2330f27 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -154,12 +154,19 @@ en: sign_out: "Sign Out" stories: Stories stream_settings: "Stream Settings" + access_message: "You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!" suspect_word_title: "Suspect words list" suspect_word_text: "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list." tech_settings: "Tech Settings" title: "Configure Comment Stream" weeks: Weeks wordlist: "Banned Words" + save_changes_dialog: + unsaved_changes: "Unsaved changes" + copy: "You have made one or more changes without saving. Would you like to save or discard your changes?" + save_settings: "Save Settings" + discard: "Discard" + cancel: "Cancel" continue: "Continue" createdisplay: check_the_form: "Invalid Form. Please check the fields" diff --git a/locales/es.yml b/locales/es.yml index ba6cb8ad3..4e7a2c51b 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -153,12 +153,19 @@ es: sign_out: "Desconectar" stories: Artículos stream_settings: "Configuración de Comentarios" + access_message: "Usted debe ser un administrador para acceder a esta página. Encuentre a otro admin y actualice los permisos de su cuenta!" suspect_word_title: "Lista de palabras sospechosas" suspect_word_text: "Comentarios que contengan estas palabras o frases, considerando mayusculas y minúsculas, serán automáticamente destacadas en los comentarios publicados. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pegar una lista de palabras separadas por coma." tech_settings: "Configuración Técnica" title: "Configurar los comentarios" weeks: Semanas wordlist: "Palabras Suspendidas" + save_changes_dialog: + unsaved_changes: "Cambios no guardados" + copy: Has hecho uno o más cambios sin guardar. Deseas guardar o descartar tus cambios?" + save_settings: "Guardar configuración" + discard: "Descartar" + cancel: "Cancelar" continue: "Continuar" createdisplay: check_the_form: "Formulario Inválido. Por favor verifica los campos" diff --git a/plugins/talk-plugin-featured-comments/client/components/Comment.js b/plugins/talk-plugin-featured-comments/client/components/Comment.js index 589e6e2a6..25a4740d8 100644 --- a/plugins/talk-plugin-featured-comments/client/components/Comment.js +++ b/plugins/talk-plugin-featured-comments/client/components/Comment.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import cn from 'classnames'; import styles from './Comment.css'; import { t } from 'plugin-api/beta/client/services'; @@ -28,6 +29,7 @@ class Comment extends React.Component { fill="commentContent" defaultComponent={CommentContent} passthrough={slotPassthrough} + size={1} />
@@ -87,4 +89,11 @@ class Comment extends React.Component { } } +Comment.propTypes = { + viewComment: PropTypes.func, + comment: PropTypes.object, + asset: PropTypes.object, + root: PropTypes.object, +}; + export default Comment; diff --git a/plugins/talk-plugin-rich-text/client/components/AdminCommentContent.js b/plugins/talk-plugin-rich-text/client/components/AdminCommentContent.js index 1b7e53ebf..f49d89055 100644 --- a/plugins/talk-plugin-rich-text/client/components/AdminCommentContent.js +++ b/plugins/talk-plugin-rich-text/client/components/AdminCommentContent.js @@ -1,12 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Linkify from 'react-linkify'; 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 ( + const content = ( ); + + if (!!comment.richTextBody) { + return content; + } + + return {content}; } } diff --git a/plugins/talk-plugin-rich-text/client/components/CommentContent.js b/plugins/talk-plugin-rich-text/client/components/CommentContent.js index 2cf481830..64aa96c82 100644 --- a/plugins/talk-plugin-rich-text/client/components/CommentContent.js +++ b/plugins/talk-plugin-rich-text/client/components/CommentContent.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { PLUGIN_NAME } from '../constants'; import cn from 'classnames'; import styles from './CommentContent.css'; +import Linkify from 'react-linkify'; class CommentContent extends React.Component { render() { @@ -14,7 +15,9 @@ class CommentContent extends React.Component { dangerouslySetInnerHTML={{ __html: comment.richTextBody }} /> ) : ( -
{comment.body}
+ +
{comment.body}
+
); } }