From bc6d13e768c3a6d69c91ac31d8b4b45444aed942 Mon Sep 17 00:00:00 2001 From: Bastian Date: Wed, 30 Jan 2019 11:22:05 +0100 Subject: [PATCH 01/13] Extend slash command '/topic' to display the room topic If no is provided, the command will display a modal dialog containing the sanitized and linkified room topic. This is just adding some juice to make reading long room topics more convenient. --- src/SlashCommands.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 24328d6372..5b5a53b83f 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -27,7 +27,12 @@ import SettingsStore, {SettingLevel} from './settings/SettingsStore'; import {MATRIXTO_URL_PATTERN} from "./linkify-matrix"; import * as querystring from "querystring"; import MultiInviter from './utils/MultiInviter'; +import * as linkify from 'linkifyjs'; +import linkifyString from 'linkifyjs/string'; +import linkifyMatrix from './linkify-matrix'; +import sanitizeHtml from 'sanitize-html'; +linkifyMatrix(linkify); class Command { constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) { @@ -125,13 +130,27 @@ 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.getContent().topic; + const topicHtml = linkifyString(sanitizeHtml(topic)); + + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Slash Commands', 'Topic', QuestionDialog, { + title: room.name, + description:
, + hasCancelButton: false, + }); + return success(); }, }), From 5f90321d0f1a1dcbe47e46982eb26ffefaa83555 Mon Sep 17 00:00:00 2001 From: Bastian Date: Wed, 30 Jan 2019 11:57:50 +0100 Subject: [PATCH 02/13] Update i18n --- src/i18n/strings/en_EN.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c6701156cb..69c9a77cf3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -114,8 +114,6 @@ "Failed to invite": "Failed to invite", "Failed to invite users to the room:": "Failed to invite users to the room:", "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:", - "Waiting for %(userId)s to accept...": "Waiting for %(userId)s to accept...", - "Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...", "You need to be logged in.": "You need to be logged in.", "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", "Unable to create widget.": "Unable to create widget.", @@ -134,7 +132,7 @@ "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", "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", "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", "Leave room": "Leave room", From f245fa6a5273db61e2cd95510ad755051f254cb5 Mon Sep 17 00:00:00 2001 From: Bastian Date: Thu, 31 Jan 2019 17:57:57 +0100 Subject: [PATCH 03/13] Add InfoDialog Signed-off-by: Bastian --- src/components/views/dialogs/InfoDialog.js | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/components/views/dialogs/InfoDialog.js diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js new file mode 100644 index 0000000000..f917a907bd --- /dev/null +++ b/src/components/views/dialogs/InfoDialog.js @@ -0,0 +1,80 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd. + +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, + extraButtons: PropTypes.node, + button: PropTypes.string, + danger: PropTypes.bool, + focus: PropTypes.bool, + onFinished: PropTypes.func.isRequired, + }, + + getDefaultProps: function() { + return { + title: "", + description: "", + extraButtons: null, + focus: true, + danger: false, + }; + }, + + onOk: function() { + this.props.onFinished(true); + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + let primaryButtonClass = ""; + if (this.props.danger) { + primaryButtonClass = "danger"; + } + return ( + +
+ { this.props.description } +
+ + { this.props.extraButtons } + +
+ ); + }, +}); From 8273662500888a5a58778ac46939d1e3988777e2 Mon Sep 17 00:00:00 2001 From: Bastian Date: Thu, 31 Jan 2019 18:00:37 +0100 Subject: [PATCH 04/13] Replace QuestionDialog with InfoDialog Display a default message if no room topic is present Signed-off-by: Bastian --- src/SlashCommands.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index d70d970796..f31bf32eac 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -154,13 +154,12 @@ export const CommandMap = { const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); const topic = topicEvents.getContent().topic; - const topicHtml = linkifyString(sanitizeHtml(topic)); + const topicHtml = topic ? linkifyString(sanitizeHtml(topic)) : _t('This room has no topic.'); - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); - Modal.createTrackedDialog('Slash Commands', 'Topic', QuestionDialog, { + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { title: room.name, description:
, - hasCancelButton: false, }); return success(); }, From 3e2bcd1d3c1e80c22c6ad90d60be8c88252d687b Mon Sep 17 00:00:00 2001 From: Bastian Date: Thu, 31 Jan 2019 18:01:53 +0100 Subject: [PATCH 05/13] Update i18n Signed-off-by: Bastian --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bb382304f9..4da2195834 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -134,6 +134,7 @@ "Changes your display nickname": "Changes your display nickname", "Changes colour scheme of current room": "Changes colour scheme of current room", "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", From 9cd13a8893bf6f3dbb6c2d7b3457598207cd1d1f Mon Sep 17 00:00:00 2001 From: Bastian Date: Thu, 31 Jan 2019 22:26:07 +0100 Subject: [PATCH 06/13] Add HtmlUtils.linkifyString() Add HtmlUtils.linkifyElement() Add HtmlUtils.linkifyAndSanitize() Refactor module imports Signed-off-by: Bastian --- src/HtmlUtils.js | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index e72c0bfe4b..ff99b2386b 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,35 @@ 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 + * @param {object} [sanitizeHtmlOptions] Optional settings for sanitize-html + * @returns {string} + */ +export function linkifyAndSanitizeHtml(dirtyHtml, sanitizeHtmlOptions = undefined) { + return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlOptions); +} From 23971b3d0d5fb3627d68588f9776e32aaadedb59 Mon Sep 17 00:00:00 2001 From: Bastian Date: Thu, 31 Jan 2019 22:35:58 +0100 Subject: [PATCH 07/13] Refactor to use HtmlUtils for linkifying and sanitizing Signed-off-by: Bastian --- src/SlashCommands.js | 9 ++------- src/components/structures/RoomDirectory.js | 9 ++------- src/components/views/messages/TextualBody.js | 7 +------ src/components/views/rooms/LinkPreviewWidget.js | 12 ++++-------- src/components/views/rooms/RoomDetailRow.js | 8 ++------ src/components/views/rooms/RoomHeader.js | 8 ++------ 6 files changed, 13 insertions(+), 40 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index f31bf32eac..be1f4ebda8 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -27,12 +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 * as linkify from 'linkifyjs'; -import linkifyString from 'linkifyjs/string'; -import linkifyMatrix from './linkify-matrix'; -import sanitizeHtml from 'sanitize-html'; - -linkifyMatrix(linkify); +import { linkifyAndSanitizeHtml } from './HtmlUtils'; class Command { constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) { @@ -154,7 +149,7 @@ export const CommandMap = { const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); const topic = topicEvents.getContent().topic; - const topicHtml = topic ? linkifyString(sanitizeHtml(topic)) : _t('This room has no topic.'); + const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.'); const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index c18dd4d48a..08d3403bc3 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( Date: Wed, 6 Feb 2019 19:27:53 +0100 Subject: [PATCH 08/13] Update copyright Signed-off-by: Bastian --- src/components/views/dialogs/InfoDialog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index f917a907bd..9e195c3291 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd. +Copyright 2019 Bastian Masanek, Noxware IT Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 7428e97910837167d2823270a22ff1a808b917bf Mon Sep 17 00:00:00 2001 From: Bastian Date: Wed, 6 Feb 2019 19:35:43 +0100 Subject: [PATCH 09/13] Clean up InfoButton Signed-off-by: Bastian --- src/components/views/dialogs/InfoDialog.js | 25 ++++------------------ 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index 9e195c3291..620aaf615a 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -26,38 +26,24 @@ export default React.createClass({ propTypes: { title: PropTypes.string, description: PropTypes.node, - extraButtons: PropTypes.node, button: PropTypes.string, - danger: PropTypes.bool, - focus: PropTypes.bool, - onFinished: PropTypes.func.isRequired, + onFinished: PropTypes.func, }, getDefaultProps: function() { return { title: "", description: "", - extraButtons: null, - focus: true, - danger: false, }; }, - onOk: function() { - this.props.onFinished(true); - }, - - onCancel: function() { - this.props.onFinished(false); + onFinished: function() { + this.props.onFinished(); }, render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - let primaryButtonClass = ""; - if (this.props.danger) { - primaryButtonClass = "danger"; - } return ( - { this.props.extraButtons } ); From d77f10e085c9f4353275ea9baaab004e7e010fe1 Mon Sep 17 00:00:00 2001 From: Bastian Date: Wed, 6 Feb 2019 19:36:57 +0100 Subject: [PATCH 10/13] Reformat Signed-off-by: Bastian --- src/components/views/dialogs/InfoDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index 620aaf615a..1a59aaf97c 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -32,8 +32,8 @@ export default React.createClass({ getDefaultProps: function() { return { - title: "", - description: "", + title: '', + description: '', }; }, @@ -50,7 +50,7 @@ export default React.createClass({ contentId='mx_Dialog_content' hasCancel={false} > -
+
{ this.props.description }
Date: Wed, 6 Feb 2019 19:43:55 +0100 Subject: [PATCH 11/13] Replace QuestionDialog with InfoDialog Signed-off-by: Bastian --- src/SlashCommands.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index be1f4ebda8..6a4127f4b5 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -404,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, }); }), ); @@ -436,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, }); }), ); @@ -559,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:

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

, - hasCancelButton: false, }); }), ); From 951f0fc816f62272af4b8ad870ce570b3c72c861 Mon Sep 17 00:00:00 2001 From: Bastian Date: Wed, 6 Feb 2019 20:10:44 +0100 Subject: [PATCH 12/13] Fix error if topicEvents is undefined Signed-off-by: Bastian --- src/SlashCommands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 6a4127f4b5..115cf0e018 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -148,7 +148,7 @@ export const CommandMap = { if (!room) return reject('Bad room ID: ' + roomId); const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); - const topic = topicEvents.getContent().topic; + const topic = topicEvents && topicEvents.getContent().topic; const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.'); const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); From 179f9a19436fc41c298641b61ae4a3eeaf31a043 Mon Sep 17 00:00:00 2001 From: Bastian Date: Thu, 7 Feb 2019 14:33:11 +0100 Subject: [PATCH 13/13] Use default options from sanitizeHtmlParams for sanitizeHtml() Signed-off-by: Bastian --- src/HtmlUtils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index ff99b2386b..371804725d 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -539,9 +539,8 @@ export function linkifyElement(element, options = linkifyMatrix.options) { * Linkify the given string and sanitize the HTML afterwards. * * @param {string} dirtyHtml The HTML string to sanitize and linkify - * @param {object} [sanitizeHtmlOptions] Optional settings for sanitize-html * @returns {string} */ -export function linkifyAndSanitizeHtml(dirtyHtml, sanitizeHtmlOptions = undefined) { - return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlOptions); +export function linkifyAndSanitizeHtml(dirtyHtml) { + return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams); }