diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index e72c0bfe4b..371804725d 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -19,16 +19,21 @@ limitations under the License. import ReplyThread from "./components/views/elements/ReplyThread"; -const React = require('react'); -const sanitizeHtml = require('sanitize-html'); -const highlight = require('highlight.js'); -const linkifyMatrix = require('./linkify-matrix'); +import React from 'react'; +import sanitizeHtml from 'sanitize-html'; +import highlight from 'highlight.js'; +import * as linkify from 'linkifyjs'; +import linkifyMatrix from './linkify-matrix'; +import _linkifyElement from 'linkifyjs/element'; +import _linkifyString from 'linkifyjs/string'; import escape from 'lodash/escape'; import emojione from 'emojione'; import classNames from 'classnames'; import MatrixClientPeg from './MatrixClientPeg'; import url from 'url'; +linkifyMatrix(linkify); + emojione.imagePathSVG = 'emojione/svg/'; // Store PNG path for displaying many flags at once (for increased performance over SVG) emojione.imagePathPNG = 'emojione/png/'; @@ -508,3 +513,34 @@ export function emojifyText(text) { __html: unicodeToImage(escape(text)), }; } + +/** + * Linkifies the given string. This is a wrapper around 'linkifyjs/string'. + * + * @param {string} str + * @returns {string} + */ +export function linkifyString(str) { + return _linkifyString(str); +} + +/** + * Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'. + * + * @param {object} element DOM element to linkify + * @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options + * @returns {object} + */ +export function linkifyElement(element, options = linkifyMatrix.options) { + return _linkifyElement(element, options); +} + +/** + * Linkify the given string and sanitize the HTML afterwards. + * + * @param {string} dirtyHtml The HTML string to sanitize and linkify + * @returns {string} + */ +export function linkifyAndSanitizeHtml(dirtyHtml) { + return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams); +} diff --git a/src/SlashCommands.js b/src/SlashCommands.js index b9fc4e4334..115cf0e018 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -27,7 +27,7 @@ import SettingsStore, {SettingLevel} from './settings/SettingsStore'; import {MATRIXTO_URL_PATTERN} from "./linkify-matrix"; import * as querystring from "querystring"; import MultiInviter from './utils/MultiInviter'; - +import { linkifyAndSanitizeHtml } from './HtmlUtils'; class Command { constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) { @@ -137,13 +137,26 @@ export const CommandMap = { topic: new Command({ name: 'topic', - args: '', - description: _td('Sets the room topic'), + args: '[]', + description: _td('Gets or sets the room topic'), runFn: function(roomId, args) { + const cli = MatrixClientPeg.get(); if (args) { - return success(MatrixClientPeg.get().setRoomTopic(roomId, args)); + return success(cli.setRoomTopic(roomId, args)); } - return reject(this.getUsage()); + const room = cli.getRoom(roomId); + if (!room) return reject('Bad room ID: ' + roomId); + + const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); + const topic = topicEvents && topicEvents.getContent().topic; + const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.'); + + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { + title: room.name, + description:
, + }); + return success(); }, }), @@ -391,13 +404,12 @@ export const CommandMap = { ignoredUsers.push(userId); // de-duped internally in the js-sdk return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); - Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, { + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, { title: _t('Ignored user'), description:

{ _t('You are now ignoring %(userId)s', {userId}) }

, - hasCancelButton: false, }); }), ); @@ -423,13 +435,12 @@ export const CommandMap = { if (index !== -1) ignoredUsers.splice(index, 1); return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); - Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, { + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, { title: _t('Unignored user'), description:

{ _t('You are no longer ignoring %(userId)s', {userId}) }

, - hasCancelButton: false, }); }), ); @@ -546,8 +557,8 @@ export const CommandMap = { return cli.setDeviceVerified(userId, deviceId, true); }).then(() => { // Tell the user we verified everything - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); - Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, { + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { title: _t('Verified key'), description:

@@ -558,7 +569,6 @@ export const CommandMap = { }

, - hasCancelButton: false, }); }), ); diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 3fb2cc86ef..5309b02041 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -24,10 +24,7 @@ const Modal = require('../../Modal'); const sdk = require('../../index'); const dis = require('../../dispatcher'); -const linkify = require('linkifyjs'); -const linkifyString = require('linkifyjs/string'); -const linkifyMatrix = require('../../linkify-matrix'); -const sanitizeHtml = require('sanitize-html'); +import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; import Promise from 'bluebird'; import { _t } from '../../languageHandler'; @@ -37,8 +34,6 @@ import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/Dire const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 160; -linkifyMatrix(linkify); - module.exports = React.createClass({ displayName: 'RoomDirectory', @@ -438,7 +433,7 @@ module.exports = React.createClass({ if (topic.length > MAX_TOPIC_LENGTH) { topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; } - topic = linkifyString(sanitizeHtml(topic)); + topic = linkifyAndSanitizeHtml(topic); rows.push( + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +export default React.createClass({ + displayName: 'InfoDialog', + propTypes: { + title: PropTypes.string, + description: PropTypes.node, + button: PropTypes.string, + onFinished: PropTypes.func, + }, + + getDefaultProps: function() { + return { + title: '', + description: '', + }; + }, + + onFinished: function() { + this.props.onFinished(); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return ( + +
+ { this.props.description } +
+ + +
+ ); + }, +}); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index a2a902b3e0..ec8c18cd7e 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -22,9 +22,6 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; -import * as linkify from 'linkifyjs'; -import linkifyElement from 'linkifyjs/element'; -import linkifyMatrix from '../../../linkify-matrix'; import sdk from '../../../index'; import ScalarAuthClient from '../../../ScalarAuthClient'; import Modal from '../../../Modal'; @@ -38,8 +35,6 @@ import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; import ReplyThread from "../elements/ReplyThread"; import {host as matrixtoHost} from '../../../matrix-to'; -linkifyMatrix(linkify); - module.exports = React.createClass({ displayName: 'TextualBody', @@ -98,7 +93,7 @@ module.exports = React.createClass({ // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. this.pillifyLinks(this.refs.content.children); - linkifyElement(this.refs.content, linkifyMatrix.options); + HtmlUtils.linkifyElement(this.refs.content); this.calculateUrlPreview(); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 1483fbffae..78796a8f14 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -16,19 +16,15 @@ limitations under the License. 'use strict'; -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import { linkifyElement } from '../../../HtmlUtils'; const sdk = require('../../../index'); const MatrixClientPeg = require('../../../MatrixClientPeg'); const ImageUtils = require('../../../ImageUtils'); const Modal = require('../../../Modal'); -const linkify = require('linkifyjs'); -const linkifyElement = require('linkifyjs/element'); -const linkifyMatrix = require('../../../linkify-matrix'); -linkifyMatrix(linkify); - module.exports = React.createClass({ displayName: 'LinkPreviewWidget', @@ -62,13 +58,13 @@ module.exports = React.createClass({ componentDidMount: function() { if (this.refs.description) { - linkifyElement(this.refs.description, linkifyMatrix.options); + linkifyElement(this.refs.description); } }, componentDidUpdate: function() { if (this.refs.description) { - linkifyElement(this.refs.description, linkifyMatrix.options); + linkifyElement(this.refs.description); } }, diff --git a/src/components/views/rooms/RoomDetailRow.js b/src/components/views/rooms/RoomDetailRow.js index f7dc9086ec..09d7eb22ed 100644 --- a/src/components/views/rooms/RoomDetailRow.js +++ b/src/components/views/rooms/RoomDetailRow.js @@ -17,15 +17,11 @@ limitations under the License. import sdk from '../../../index'; import React from 'react'; import { _t } from '../../../languageHandler'; -import * as linkify from 'linkifyjs'; -import linkifyElement from 'linkifyjs/element'; -import linkifyMatrix from '../../../linkify-matrix'; +import { linkifyElement } from '../../../HtmlUtils'; import { ContentRepo } from 'matrix-js-sdk'; import MatrixClientPeg from '../../../MatrixClientPeg'; import PropTypes from 'prop-types'; -linkifyMatrix(linkify); - export function getDisplayAliasForRoom(room) { return room.canonicalAlias || (room.aliases ? room.aliases[0] : ""); } @@ -53,7 +49,7 @@ export default React.createClass({ _linkifyTopic: function() { if (this.refs.topic) { - linkifyElement(this.refs.topic, linkifyMatrix.options); + linkifyElement(this.refs.topic); } }, diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 0e04551bbf..ce229620d7 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -25,9 +25,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from "../../../Modal"; import RateLimitedFunc from '../../../ratelimitedfunc'; -import * as linkify from 'linkifyjs'; -import linkifyElement from 'linkifyjs/element'; -import linkifyMatrix from '../../../linkify-matrix'; +import { linkifyElement } from '../../../HtmlUtils'; import AccessibleButton from '../elements/AccessibleButton'; import ManageIntegsButton from '../elements/ManageIntegsButton'; import {CancelButton} from './SimpleRoomHeader'; @@ -35,8 +33,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; -linkifyMatrix(linkify); - module.exports = React.createClass({ displayName: 'RoomHeader', @@ -76,7 +72,7 @@ module.exports = React.createClass({ componentDidUpdate: function() { if (this.refs.topic) { - linkifyElement(this.refs.topic, linkifyMatrix.options); + linkifyElement(this.refs.topic); } }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 14e1a4dae5..b586d2f8f7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -133,7 +133,8 @@ "Upgrades a room to a new version": "Upgrades a room to a new version", "Changes your display nickname": "Changes your display nickname", "Changes colour scheme of current room": "Changes colour scheme of current room", - "Sets the room topic": "Sets the room topic", + "Gets or sets the room topic": "Gets or sets the room topic", + "This room has no topic.": "This room has no topic.", "Sets the room name": "Sets the room name", "Invites user with given id to current room": "Invites user with given id to current room", "Joins room with given alias": "Joins room with given alias",