From 0604c86779cdea98ace30bdd78eb4db6888ffc40 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sat, 19 Sep 2020 15:30:00 +0100 Subject: [PATCH 01/57] added katex package and import --- package.json | 1 + src/HtmlUtils.tsx | 1 + yarn.lock | 7 +++++++ 3 files changed, 9 insertions(+) diff --git a/package.json b/package.json index 156cbb1bc8..7aa3df136b 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "highlight.js": "^10.1.2", "html-entities": "^1.3.1", "is-ip": "^2.0.0", + "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index bd314c2e5f..99acbfcb0c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -26,6 +26,7 @@ import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; +import katex from 'katex'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; diff --git a/yarn.lock b/yarn.lock index efc1f0eae1..34b99708fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5607,6 +5607,13 @@ jsx-ast-utils@^2.4.1: array-includes "^3.1.1" object.assign "^4.1.0" +katex@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9" + integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg== + dependencies: + commander "^2.19.0" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" From becc79d67a29a0886f4a6f800daabebae16d655c Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 12:59:22 +0100 Subject: [PATCH 02/57] send tex math as data-mx-maths attribute --- src/HtmlUtils.tsx | 26 +++++++++++++++++++++++++- src/editor/serialize.ts | 23 ++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 99acbfcb0c..344fb3514c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -27,6 +27,7 @@ import classNames from 'classnames'; import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; import katex from 'katex'; +import { AllHtmlEntities } from 'html-entities'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; @@ -236,7 +237,8 @@ const sanitizeHtmlParams: sanitizeHtml.IOptions = { allowedAttributes: { // custom ones first: font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix - span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix + span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix + div: ['data-mx-maths'], a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix img: ['src', 'width', 'height', 'alt', 'title'], ol: ['start'], @@ -409,6 +411,27 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); + if (true) { // TODO: add katex setting + const mathDelimiters = [ + { left: "
.*?
", display: true }, + { left: ".*?", display: false } + ]; + + mathDelimiters.forEach(function (d) { + var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + + safeBody = safeBody.replace(reg, function(match, p1) { + return katex.renderToString( + AllHtmlEntities.decode(p1), + { + throwOnError: false, + displayMode: d.display, + output: "mathml" + }) + }); + }); + } + } } finally { delete sanitizeParams.textFilter; @@ -450,6 +473,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts 'markdown-body': isHtmlMessage && !emojiBody, }); + return isDisplayedWithHtml ? { @@ -38,7 +39,27 @@ export function mdSerialize(model: EditorModel) { } export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) { - const md = mdSerialize(model); + var md = mdSerialize(model); + + if (true) { // TODO: add katex setting + const mathDelimiters = [ // TODO: make customizable + { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true }, + { left: "\\$\\$", right: "\\$\\$", display: false } + ]; + + mathDelimiters.forEach(function (d) { + var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + md = md.replace(reg, function(match, p1) { + const p1e = AllHtmlEntities.encode(p1); + if (d.display == true) { + return `
${p1e}
`; + } else { + return `${p1e}`; + } + }); + }); + } + const parser = new Markdown(md); if (!parser.isPlainText() || forceHTML) { return parser.toHTML(); From e78734bbf6b2fbf1ebee530921998ff97c56f203 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 14:20:35 +0100 Subject: [PATCH 03/57] Deserialize back to math delimiters for editing --- src/HtmlUtils.tsx | 4 +++- src/editor/deserialize.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 344fb3514c..46bc7b441c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -534,7 +534,6 @@ export function checkBlockNode(node: Node) { case "H6": case "PRE": case "BLOCKQUOTE": - case "DIV": case "P": case "UL": case "OL": @@ -547,6 +546,9 @@ export function checkBlockNode(node: Node) { case "TH": case "TD": return true; + case "DIV": + // don't treat math nodes as block nodes for deserializing + return !(node as HTMLElement).hasAttribute("data-mx-maths"); default: return false; } diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index ec697b193c..edaa330e50 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -130,6 +130,18 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl } break; } + case "DIV": + case "SPAN": { + // math nodes are translated back into delimited latex strings + if (n.hasAttribute("data-mx-maths")) { + const delim = (n.nodeName == "SPAN") ? "$$" : "$$$"; + const tex = n.getAttribute("data-mx-maths"); + return partCreator.plain(delim + tex + delim); + } else if (!checkDescendInto(n)) { + return partCreator.plain(n.textContent); + } + break; + } case "OL": state.listIndex.push((n).start || 1); /* falls through */ From 428a6b94ff5c34533b8684e5ae8b019a4dbec07c Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 15:07:12 +0100 Subject: [PATCH 04/57] math off by default, enable with latex_maths flag --- src/HtmlUtils.tsx | 4 +++- src/editor/serialize.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 46bc7b441c..047a891847 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -28,6 +28,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; +import SdkConfig from './SdkConfig'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; @@ -50,6 +51,7 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g"); // Regex pattern for whitespace characters const WHITESPACE_REGEX = new RegExp("\\s", "g"); + const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; @@ -411,7 +413,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); - if (true) { // TODO: add katex setting + if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ { left: "
.*?
", display: true }, { left: ".*?", display: false } diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 8ec590cba5..72a551a4a3 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -19,6 +19,7 @@ import Markdown from '../Markdown'; import {makeGenericPermalink} from "../utils/permalinks/Permalinks"; import EditorModel from "./model"; import { AllHtmlEntities } from 'html-entities'; +import SdkConfig from '../SdkConfig'; export function mdSerialize(model: EditorModel) { return model.parts.reduce((html, part) => { @@ -41,7 +42,7 @@ export function mdSerialize(model: EditorModel) { export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) { var md = mdSerialize(model); - if (true) { // TODO: add katex setting + if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ // TODO: make customizable { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true }, { left: "\\$\\$", right: "\\$\\$", display: false } From e4448ae1ad87cbd3e47c73a589012494ec7d4189 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 16:52:29 +0100 Subject: [PATCH 05/57] send fallback in pre tags, not code --- src/editor/serialize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 72a551a4a3..c0d9509ffa 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = md = md.replace(reg, function(match, p1) { const p1e = AllHtmlEntities.encode(p1); if (d.display == true) { - return `
${p1e}
`; + return `
${p1e}
`; } else { - return `${p1e}`; + return `
${p1e}
`; } }); }); From 7e6d7053e0a6c55f082153a521de079c7db2d77c Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 17:02:27 +0100 Subject: [PATCH 06/57] Revert "send fallback in pre tags, not code" (code looks better) This reverts commit e4448ae1ad87cbd3e47c73a589012494ec7d4189. --- src/editor/serialize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index c0d9509ffa..72a551a4a3 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = md = md.replace(reg, function(match, p1) { const p1e = AllHtmlEntities.encode(p1); if (d.display == true) { - return `
${p1e}
`; + return `
${p1e}
`; } else { - return `
${p1e}
`; + return `${p1e}`; } }); }); From 1f24b5b90c9fe6a743db17d14b726e1aefd15f6f Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 17:48:42 +0100 Subject: [PATCH 07/57] made math display slightly larger --- res/css/structures/_RoomView.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 572c7166d2..571c34fcb0 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -205,6 +205,10 @@ limitations under the License. clear: both; } +.mx_RoomView_MessageList .katex { + font-size: 1.3em; +} + li.mx_RoomView_myReadMarker_container { height: 0px; margin: 0px; From 24a1834f9b37993b79ec92c1c3081d6aa7777d37 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 21 Sep 2020 09:00:24 +0100 Subject: [PATCH 08/57] support multi-line and escaped $ --- src/HtmlUtils.tsx | 6 +++--- src/editor/serialize.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 047a891847..569b1662fe 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -415,12 +415,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts safeBody = sanitizeHtml(formattedBody, sanitizeParams); if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ - { left: "
.*?
", display: true }, - { left: ".*?", display: false } + { pattern: "
(.|\\s)*?
", display: true }, + { pattern: "(.|\\s)*?", display: false } ]; mathDelimiters.forEach(function (d) { - var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + var reg = RegExp(d.pattern, "gm"); safeBody = safeBody.replace(reg, function(match, p1) { return katex.renderToString( diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 72a551a4a3..d0a28266eb 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -44,12 +44,12 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ // TODO: make customizable - { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true }, - { left: "\\$\\$", right: "\\$\\$", display: false } + { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true }, + { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false } ]; mathDelimiters.forEach(function (d) { - var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + var reg = RegExp(d.pattern, "gm"); md = md.replace(reg, function(match, p1) { const p1e = AllHtmlEntities.encode(p1); if (d.display == true) { From 4df8754aad0333c840eceb1892faa9f3c90f2405 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 21 Sep 2020 11:00:39 +0100 Subject: [PATCH 09/57] allow custom latex delimiters in config.json --- src/editor/deserialize.ts | 10 ++++++++-- src/editor/serialize.ts | 26 ++++++++++++-------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index edaa330e50..e27eecd2db 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -21,6 +21,7 @@ import { walkDOMDepthFirst } from "./dom"; import { checkBlockNode } from "../HtmlUtils"; import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks"; import { PartCreator } from "./parts"; +import SdkConfig from "../SdkConfig"; function parseAtRoomMentions(text: string, partCreator: PartCreator) { const ATROOM = "@room"; @@ -134,9 +135,14 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl case "SPAN": { // math nodes are translated back into delimited latex strings if (n.hasAttribute("data-mx-maths")) { - const delim = (n.nodeName == "SPAN") ? "$$" : "$$$"; + const delimLeft = (n.nodeName == "SPAN") ? + (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$$" : + (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$$"; + const delimRight = (n.nodeName == "SPAN") ? + (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$$" : + (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$$"; const tex = n.getAttribute("data-mx-maths"); - return partCreator.plain(delim + tex + delim); + return partCreator.plain(delimLeft + tex + delimRight); } else if (!checkDescendInto(n)) { return partCreator.plain(n.textContent); } diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index d0a28266eb..da8ae4e820 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -43,21 +43,19 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = var md = mdSerialize(model); if (SdkConfig.get()['latex_maths']) { - const mathDelimiters = [ // TODO: make customizable - { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true }, - { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false } - ]; + const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] || + "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$"; + const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] || + "\\$\\$(([^$]|\\\\\\$)*)\\$\\$"; - mathDelimiters.forEach(function (d) { - var reg = RegExp(d.pattern, "gm"); - md = md.replace(reg, function(match, p1) { - const p1e = AllHtmlEntities.encode(p1); - if (d.display == true) { - return `
${p1e}
`; - } else { - return `${p1e}`; - } - }); + md = md.replace(RegExp(displayPattern, "gm"), function(m,p1) { + const p1e = AllHtmlEntities.encode(p1); + return `
${p1e}
`; + }); + + md = md.replace(RegExp(inlinePattern, "gm"), function(m,p1) { + const p1e = AllHtmlEntities.encode(p1); + return `${p1e}`; }); } From 1b689bb4e11c1329072a85002ea90abfaf9043df Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 21 Sep 2020 22:02:19 +0100 Subject: [PATCH 10/57] tell markdown to ignore math tags --- src/Markdown.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Markdown.js b/src/Markdown.js index 492450e87d..dc15e7d6b3 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -16,13 +16,19 @@ limitations under the License. import commonmark from 'commonmark'; import {escape} from "lodash"; +import SdkConfig from './SdkConfig'; -const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; +const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u', 'code']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; function is_allowed_html_tag(node) { + if (SdkConfig.get()['latex_maths'] && + node.literal.match(/^<\/?(div|span)( data-mx-maths="[^"]*")?>$/) != null) { + return true; + } + // Regex won't work for tags with attrs, but we only // allow anyway. const matches = /^<\/?(.*)>$/.exec(node.literal); @@ -30,6 +36,7 @@ function is_allowed_html_tag(node) { const tag = matches[1]; return ALLOWED_HTML_TAGS.indexOf(tag) > -1; } + return false; } From aded3c9de2b14010612b7d9581b10366d9dc3be2 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Tue, 22 Sep 2020 11:54:23 +0100 Subject: [PATCH 11/57] cosmetic changes (lint) --- src/HtmlUtils.tsx | 13 +++++-------- src/editor/serialize.ts | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 569b1662fe..7bccd47622 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -416,24 +416,21 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ { pattern: "
(.|\\s)*?
", display: true }, - { pattern: "(.|\\s)*?", display: false } + { pattern: "(.|\\s)*?", display: false }, ]; - mathDelimiters.forEach(function (d) { - var reg = RegExp(d.pattern, "gm"); - - safeBody = safeBody.replace(reg, function(match, p1) { + mathDelimiters.forEach(function(d) { + safeBody = safeBody.replace(RegExp(d.pattern, "gm"), function(m, p1) { return katex.renderToString( AllHtmlEntities.decode(p1), { throwOnError: false, displayMode: d.display, - output: "mathml" + output: "mathml", }) }); }); - } - + } } } finally { delete sanitizeParams.textFilter; diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index da8ae4e820..02194a1d59 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -40,7 +40,7 @@ export function mdSerialize(model: EditorModel) { } export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) { - var md = mdSerialize(model); + let md = mdSerialize(model); if (SdkConfig.get()['latex_maths']) { const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] || @@ -48,12 +48,12 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] || "\\$\\$(([^$]|\\\\\\$)*)\\$\\$"; - md = md.replace(RegExp(displayPattern, "gm"), function(m,p1) { + md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) { const p1e = AllHtmlEntities.encode(p1); return `
${p1e}
`; }); - md = md.replace(RegExp(inlinePattern, "gm"), function(m,p1) { + md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) { const p1e = AllHtmlEntities.encode(p1); return `${p1e}`; }); From d2054ea685bad49af11ec9a64b5aa4218bc204c0 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Fri, 25 Sep 2020 09:05:22 +0100 Subject: [PATCH 12/57] HTML output for cross-browser support --- res/css/structures/_RoomView.scss | 4 ---- src/HtmlUtils.tsx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 571c34fcb0..572c7166d2 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -205,10 +205,6 @@ limitations under the License. clear: both; } -.mx_RoomView_MessageList .katex { - font-size: 1.3em; -} - li.mx_RoomView_myReadMarker_container { height: 0px; margin: 0px; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 7bccd47622..70a2a3f000 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -426,7 +426,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts { throwOnError: false, displayMode: d.display, - output: "mathml", + output: "htmlAndMathml", }) }); }); From 65c4460abcdb64bac14bdd72e3b970a96dd52299 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Fri, 9 Oct 2020 15:47:11 +0100 Subject: [PATCH 13/57] whitespace fixes --- src/HtmlUtils.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 70a2a3f000..da3eb3b128 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -51,7 +51,6 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g"); // Regex pattern for whitespace characters const WHITESPACE_REGEX = new RegExp("\\s", "g"); - const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; @@ -472,7 +471,6 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts 'markdown-body': isHtmlMessage && !emojiBody, }); - return isDisplayedWithHtml ? Date: Sat, 10 Oct 2020 09:12:53 +0100 Subject: [PATCH 14/57] only allow code tags inside math tag --- src/Markdown.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index dc15e7d6b3..9914cff85a 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -18,14 +18,21 @@ import commonmark from 'commonmark'; import {escape} from "lodash"; import SdkConfig from './SdkConfig'; -const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u', 'code']; +const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; +function is_math_node(node) { + return node != null && + node.literal != null && + node.literal.match(/^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$/) != null; +} + function is_allowed_html_tag(node) { if (SdkConfig.get()['latex_maths'] && - node.literal.match(/^<\/?(div|span)( data-mx-maths="[^"]*")?>$/) != null) { + (is_math_node(node) || + (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) { return true; } From 96742fc3093cc88cd609d731d932a05ab094262f Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sat, 10 Oct 2020 16:32:49 +0100 Subject: [PATCH 15/57] latex math as labs setting --- src/HtmlUtils.tsx | 4 ++-- src/Markdown.js | 4 ++-- src/editor/serialize.ts | 3 ++- src/settings/Settings.ts | 6 ++++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index da3eb3b128..ca718cd9aa 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -28,7 +28,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; -import SdkConfig from './SdkConfig'; +import SettingsStore from './settings/SettingsStore'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; @@ -412,7 +412,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); - if (SdkConfig.get()['latex_maths']) { + if (SettingsStore.getValue("feature_latex_maths")) { const mathDelimiters = [ { pattern: "
(.|\\s)*?
", display: true }, { pattern: "(.|\\s)*?", display: false }, diff --git a/src/Markdown.js b/src/Markdown.js index 9914cff85a..329dcdd996 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -16,7 +16,7 @@ limitations under the License. import commonmark from 'commonmark'; import {escape} from "lodash"; -import SdkConfig from './SdkConfig'; +import SettingsStore from './settings/SettingsStore'; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; @@ -30,7 +30,7 @@ function is_math_node(node) { } function is_allowed_html_tag(node) { - if (SdkConfig.get()['latex_maths'] && + if (SettingsStore.getValue("feature_latex_maths") && (is_math_node(node) || (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) { return true; diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 02194a1d59..9f24cd5eb2 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -19,6 +19,7 @@ import Markdown from '../Markdown'; import {makeGenericPermalink} from "../utils/permalinks/Permalinks"; import EditorModel from "./model"; import { AllHtmlEntities } from 'html-entities'; +import SettingsStore from '../settings/SettingsStore'; import SdkConfig from '../SdkConfig'; export function mdSerialize(model: EditorModel) { @@ -42,7 +43,7 @@ export function mdSerialize(model: EditorModel) { export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) { let md = mdSerialize(model); - if (SdkConfig.get()['latex_maths']) { + if (SettingsStore.getValue("feature_latex_maths")) { const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] || "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$"; const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] || diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 737c882919..2f817c264c 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -116,6 +116,12 @@ export interface ISetting { } export const SETTINGS: {[setting: string]: ISetting} = { + "feature_latex_maths": { + isFeature: true, + displayName: _td("LaTeX math in messages"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_communities_v2_prototypes": { isFeature: true, displayName: _td( From a89adb86a5912d3ce71171583181175fe2564a23 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sat, 10 Oct 2020 16:33:25 +0100 Subject: [PATCH 16/57] i18n en+nl for latex math labs setting --- src/i18n/strings/en_EN.json | 1 + src/i18n/strings/en_US.json | 1 + src/i18n/strings/nl.json | 1 + 3 files changed, 3 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d7360430ae..d7b40fc198 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -856,6 +856,7 @@ "click to reveal": "click to reveal", "Clear cache and reload": "Clear cache and reload", "Labs": "Labs", + "LaTeX math in messages": "LaTeX math in messages", "Customise your experience with experimental labs features. Learn more.": "Customise your experience with experimental labs features. Learn more.", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index a1275fb089..c00bf03b29 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -128,6 +128,7 @@ "Kick": "Kick", "Kicks user with given id": "Kicks user with given id", "Labs": "Labs", + "LaTeX math in messages": "LaTeX math in messages", "Ignore": "Ignore", "Unignore": "Unignore", "You are now ignoring %(userId)s": "You are now ignoring %(userId)s", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index bb0fb5def6..d991962eec 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -199,6 +199,7 @@ "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.", "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.", "Labs": "Experimenteel", + "LaTeX math in messages": "LaTeX wiskunde in berichten", "Last seen": "Laatst gezien", "Leave room": "Gesprek verlaten", "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.", From bdd332c8b5366398d4af166b49b3eaf1cddb6230 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sat, 10 Oct 2020 20:05:35 +0100 Subject: [PATCH 17/57] ran yarn i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a33104ab12..b41a19aa21 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -438,6 +438,7 @@ "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Change notification settings": "Change notification settings", + "LaTeX math in messages": "LaTeX math in messages", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "New spinner design": "New spinner design", "Message Pinning": "Message Pinning", @@ -848,7 +849,6 @@ "click to reveal": "click to reveal", "Clear cache and reload": "Clear cache and reload", "Labs": "Labs", - "LaTeX math in messages": "LaTeX math in messages", "Customise your experience with experimental labs features. Learn more.": "Customise your experience with experimental labs features. Learn more.", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", From f0c4473107d0c3589479809d8accd79b9c4dba08 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 12 Oct 2020 21:01:11 +0100 Subject: [PATCH 18/57] tell markdown parser to ignore properly-formatted math tags --- src/Markdown.js | 51 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 329dcdd996..564a2ed0a8 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -23,19 +23,47 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; -function is_math_node(node) { - return node != null && - node.literal != null && - node.literal.match(/^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$/) != null; +// prevent renderer from interpreting contents of AST node +function freeze_node(walker, node) { + const newNode = new commonmark.Node('custom_inline', node.sourcepos); + newNode.onEnter = node.literal; + node.insertAfter(newNode); + node.unlink(); + walker.resumeAt(newNode.next, true); +} + +// prevent renderer from interpreting contents of latex math tags +function freeze_math(parsed) { + const walker = parsed.walker(); + let ev; + let inMath = false; + while ( (ev = walker.next()) ) { + const node = ev.node; + if (ev.entering) { + if (!inMath) { + // entering a math tag + if (node.literal != null && node.literal.match('^<(div|span) data-mx-maths="[^"]*">$') != null) { + inMath = true; + freeze_node(walker, node); + } + } else { + // math tags should only contain a single code block, with URL-escaped latex as fallback output + if (node.literal != null && node.literal.match('^(||[^<>]*)$')) { + freeze_node(walker, node); + // leave when span or div is closed + } else if (node.literal == '
' || node.literal == '') { + inMath = false; + freeze_node(walker, node); + // this case only happens if we have improperly formatted math tags, so bail + } else { + inMath = false; + } + } + } + } } function is_allowed_html_tag(node) { - if (SettingsStore.getValue("feature_latex_maths") && - (is_math_node(node) || - (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) { - return true; - } - // Regex won't work for tags with attrs, but we only // allow anyway. const matches = /^<\/?(.*)>$/.exec(node.literal); @@ -173,6 +201,9 @@ export default class Markdown { */ }; + // prevent strange behaviour when mixing latex math and markdown + freeze_math(this.parsed); + return renderer.render(this.parsed); } From 38d1aac978d49160bed9c96b2a1205a4e7fb707f Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 12 Oct 2020 21:15:38 +0100 Subject: [PATCH 19/57] removed useless import and whitespace --- src/Markdown.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 564a2ed0a8..2e6f391818 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -16,7 +16,6 @@ limitations under the License. import commonmark from 'commonmark'; import {escape} from "lodash"; -import SettingsStore from './settings/SettingsStore'; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; @@ -71,7 +70,6 @@ function is_allowed_html_tag(node) { const tag = matches[1]; return ALLOWED_HTML_TAGS.indexOf(tag) > -1; } - return false; } From cc713aff72c56478edb4f1eafbdc55b8c9fd4248 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Wed, 14 Oct 2020 09:35:57 +0100 Subject: [PATCH 20/57] add fallback output in code block AFTER markdown processing --- src/Markdown.js | 49 +++++------------------------------------ src/editor/serialize.ts | 18 ++++++++++++--- 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 2e6f391818..dc4d442aff 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -22,47 +22,12 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; -// prevent renderer from interpreting contents of AST node -function freeze_node(walker, node) { - const newNode = new commonmark.Node('custom_inline', node.sourcepos); - newNode.onEnter = node.literal; - node.insertAfter(newNode); - node.unlink(); - walker.resumeAt(newNode.next, true); -} - -// prevent renderer from interpreting contents of latex math tags -function freeze_math(parsed) { - const walker = parsed.walker(); - let ev; - let inMath = false; - while ( (ev = walker.next()) ) { - const node = ev.node; - if (ev.entering) { - if (!inMath) { - // entering a math tag - if (node.literal != null && node.literal.match('^<(div|span) data-mx-maths="[^"]*">$') != null) { - inMath = true; - freeze_node(walker, node); - } - } else { - // math tags should only contain a single code block, with URL-escaped latex as fallback output - if (node.literal != null && node.literal.match('^(||[^<>]*)$')) { - freeze_node(walker, node); - // leave when span or div is closed - } else if (node.literal == '
' || node.literal == '') { - inMath = false; - freeze_node(walker, node); - // this case only happens if we have improperly formatted math tags, so bail - } else { - inMath = false; - } - } - } - } -} - function is_allowed_html_tag(node) { + if (node.literal != null && + node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) { + return true; + } + // Regex won't work for tags with attrs, but we only // allow anyway. const matches = /^<\/?(.*)>$/.exec(node.literal); @@ -70,6 +35,7 @@ function is_allowed_html_tag(node) { const tag = matches[1]; return ALLOWED_HTML_TAGS.indexOf(tag) > -1; } + return false; } @@ -199,9 +165,6 @@ export default class Markdown { */ }; - // prevent strange behaviour when mixing latex math and markdown - freeze_math(this.parsed); - return renderer.render(this.parsed); } diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 9f24cd5eb2..88fd1c90fc 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -21,6 +21,7 @@ import EditorModel from "./model"; import { AllHtmlEntities } from 'html-entities'; import SettingsStore from '../settings/SettingsStore'; import SdkConfig from '../SdkConfig'; +import cheerio from 'cheerio'; export function mdSerialize(model: EditorModel) { return model.parts.reduce((html, part) => { @@ -51,18 +52,29 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) { const p1e = AllHtmlEntities.encode(p1); - return `
${p1e}
`; + return `
`; }); md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) { const p1e = AllHtmlEntities.encode(p1); - return `${p1e}`; + return ``; }); } const parser = new Markdown(md); if (!parser.isPlainText() || forceHTML) { - return parser.toHTML(); + // feed Markdown output to HTML parser + const phtml = cheerio.load(parser.toHTML(), + { _useHtmlParser2: true, decodeEntities: false }) + + // add fallback output for latex math, which should not be interpreted as markdown + phtml('div, span').each(function() { + const tex = phtml(this).attr('data-mx-maths') + if (tex) { + phtml(this).html(`${tex}`) + } + }); + return phtml.html(); } // ensure removal of escape backslashes in non-Markdown messages if (md.indexOf("\\") > -1) { From 10b732131a7315aca652677857a285d7dabb243b Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Wed, 14 Oct 2020 22:16:28 +0100 Subject: [PATCH 21/57] use html parser rather than regexes --- src/HtmlUtils.tsx | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 6bae0b25b6..dc2f45210b 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -30,6 +30,7 @@ import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; import SettingsStore from './settings/SettingsStore'; +import cheerio from 'cheerio'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; @@ -414,23 +415,20 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); - if (SettingsStore.getValue("feature_latex_maths")) { - const mathDelimiters = [ - { pattern: "
(.|\\s)*?
", display: true }, - { pattern: "(.|\\s)*?", display: false }, - ]; + const phtml = cheerio.load(safeBody, + { _useHtmlParser2: true, decodeEntities: false }) - mathDelimiters.forEach(function(d) { - safeBody = safeBody.replace(RegExp(d.pattern, "gm"), function(m, p1) { - return katex.renderToString( - AllHtmlEntities.decode(p1), - { - throwOnError: false, - displayMode: d.display, - output: "htmlAndMathml", - }) - }); + if (SettingsStore.getValue("feature_latex_maths")) { + phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { + return katex.renderToString( + AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), + { + throwOnError: false, + displayMode: e.name == 'div', + output: "htmlAndMathml", + }); }); + safeBody = phtml.html(); } } } finally { From 173d79886544bc57c8de0b1ae4b16a346cd73bae Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Fri, 23 Oct 2020 18:41:24 +0100 Subject: [PATCH 22/57] added cheerio as explicit dep in package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0a3fd7a8b7..ca7d6ee0b7 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "html-entities": "^1.3.1", "is-ip": "^2.0.0", "katex": "^0.12.0", + "cheerio": "^1.0.0-rc.3", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", From 06b20fad9543063409823540fcd4416a12c3ee21 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Fri, 23 Oct 2020 18:49:56 +0100 Subject: [PATCH 23/57] removed implicit "this" --- src/editor/serialize.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 88fd1c90fc..f31dd67ae7 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -68,10 +68,10 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = { _useHtmlParser2: true, decodeEntities: false }) // add fallback output for latex math, which should not be interpreted as markdown - phtml('div, span').each(function() { - const tex = phtml(this).attr('data-mx-maths') + phtml('div, span').each(function(i, e) { + const tex = phtml(e).attr('data-mx-maths') if (tex) { - phtml(this).html(`${tex}`) + phtml(e).html(`${tex}`) } }); return phtml.html(); From 2204e6c64e0042e0b937cf7d42e07816608e0234 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 25 Oct 2020 18:32:24 +0000 Subject: [PATCH 24/57] generate valid block html for commonmark spec --- src/editor/serialize.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index f31dd67ae7..bd7845315e 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -52,13 +52,17 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) { const p1e = AllHtmlEntities.encode(p1); - return `
`; + return `
\n\n
\n\n`; }); md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) { const p1e = AllHtmlEntities.encode(p1); return ``; }); + + // make sure div tags always start on a new line, otherwise it will confuse + // the markdown parser + md = md.replace(/(.)
Date: Thu, 29 Oct 2020 13:22:09 +0000 Subject: [PATCH 25/57] stubbed isGuest for unit tests --- test/components/views/messages/TextualBody-test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 07cd51edbd..bf55e9c430 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -36,6 +36,7 @@ describe("", () => { MatrixClientPeg.matrixClient = { getRoom: () => mkStubRoom("room_id"), getAccountData: () => undefined, + isGuest: () => false, }; const ev = mkEvent({ @@ -59,6 +60,7 @@ describe("", () => { MatrixClientPeg.matrixClient = { getRoom: () => mkStubRoom("room_id"), getAccountData: () => undefined, + isGuest: () => false, }; const ev = mkEvent({ @@ -83,6 +85,7 @@ describe("", () => { MatrixClientPeg.matrixClient = { getRoom: () => mkStubRoom("room_id"), getAccountData: () => undefined, + isGuest: () => false, }; }); @@ -135,6 +138,7 @@ describe("", () => { getHomeserverUrl: () => "https://my_server/", on: () => undefined, removeListener: () => undefined, + isGuest: () => false, }; }); From 839bae21ae5078e25b7e6a03cc4a99725014b029 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Tue, 10 Nov 2020 18:18:53 +0000 Subject: [PATCH 26/57] made single and double $ default delimiters --- src/editor/deserialize.ts | 8 ++++---- src/editor/serialize.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index e27eecd2db..6336b4c46b 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -136,11 +136,11 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl // math nodes are translated back into delimited latex strings if (n.hasAttribute("data-mx-maths")) { const delimLeft = (n.nodeName == "SPAN") ? - (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$$" : - (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$$"; + (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$" : + (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$"; const delimRight = (n.nodeName == "SPAN") ? - (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$$" : - (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$$"; + (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$" : + (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$"; const tex = n.getAttribute("data-mx-maths"); return partCreator.plain(delimLeft + tex + delimRight); } else if (!checkDescendInto(n)) { diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index bd7845315e..c1f4da306b 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -46,9 +46,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = if (SettingsStore.getValue("feature_latex_maths")) { const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] || - "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$"; - const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] || "\\$\\$(([^$]|\\\\\\$)*)\\$\\$"; + const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] || + "\\$(([^$]|\\\\\\$)*)\\$"; md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) { const p1e = AllHtmlEntities.encode(p1); From 8233ce77cbeda9706932a5ff5d7083a6775a52e0 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Tue, 10 Nov 2020 18:26:09 +0000 Subject: [PATCH 27/57] fixed duplicate import from merge --- src/HtmlUtils.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index d25c420bc9..44fbffb97f 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -33,7 +33,6 @@ import SettingsStore from './settings/SettingsStore'; import cheerio from 'cheerio'; import {MatrixClientPeg} from './MatrixClientPeg'; -import SettingsStore from './settings/SettingsStore'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; From 5f23c9499c6a60ae52de1663724a712bf2749a11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Nov 2020 12:46:55 +0000 Subject: [PATCH 28/57] Simplify UserMenu for Guests as they can't use most of the options --- res/css/structures/_UserMenu.scss | 20 ++++++++++ src/Lifecycle.ts | 6 +-- src/components/structures/UserMenu.tsx | 53 +++++++++++++++++++++++--- src/i18n/strings/en_EN.json | 2 + 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 6a352d46a3..84c21364ce 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -231,9 +231,29 @@ limitations under the License. justify-content: center; } + &.mx_UserMenu_contextMenu_guestPrompts, &.mx_UserMenu_contextMenu_hostingLink { padding-top: 0; } + + &.mx_UserMenu_contextMenu_guestPrompts { + display: inline-block; + + > span { + font-weight: 600; + display: block; + + & + span { + margin-top: 8px; + } + } + + .mx_AccessibleButton_kind_link { + font-weight: normal; + font-size: inherit; + padding: 0; + } + } } .mx_IconizedContextMenu_icon { diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 7469624f5c..73c0ccce6d 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -588,9 +588,9 @@ export function logout(): void { if (MatrixClientPeg.get().isGuest()) { // logout doesn't work for guest sessions - // Also we sometimes want to re-log in a guest session - // if we abort the login - onLoggedOut(); + // Also we sometimes want to re-log in a guest session if we abort the login. + // defer until next tick because it calls a synchronous dispatch and we are likely here from a dispatch. + setImmediate(() => onLoggedOut()); return; } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 75208b8cfe..e38dd5c2b9 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -29,7 +29,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog"; import SettingsStore from "../../settings/SettingsStore"; import {getCustomTheme} from "../../theme"; import {getHostingLink} from "../../utils/HostingLink"; -import {ButtonEvent} from "../views/elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; import SdkConfig from "../../SdkConfig"; import {getHomePageUrl} from "../../utils/pages"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; @@ -205,6 +205,16 @@ export default class UserMenu extends React.Component { this.setState({contextMenuPosition: null}); // also close the menu }; + private onSignInClick = () => { + dis.dispatch({ action: 'start_login' }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onRegisterClick = () => { + dis.dispatch({ action: 'start_registration' }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + private onHomeClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -261,10 +271,29 @@ export default class UserMenu extends React.Component { const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); - let hostingLink; + let topSection; const signupLink = getHostingLink("user-context-menu"); - if (signupLink) { - hostingLink = ( + if (MatrixClientPeg.get().isGuest()) { + topSection = ( +
+ {_t("Not you? Sign in", {}, { + a: sub => ( + + {sub} + + ), + })} + {_t("New here? Create an account", {}, { + a: sub => ( + + {sub} + + ), + })} +
+ ) + } else if (signupLink) { + topSection = (
{_t( "Upgrade to your own domain", {}, @@ -422,6 +451,20 @@ export default class UserMenu extends React.Component { ) + } else if (MatrixClientPeg.get().isGuest()) { + primaryOptionList = ( + + + { homeButton } + this.onSettingsOpen(e, null)} + /> + { feedbackButton } + + + ); } const classes = classNames({ @@ -451,7 +494,7 @@ export default class UserMenu extends React.Component { />
- {hostingLink} + {topSection} {primaryOptionList} {secondarySection} ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 830d3cdee4..4de5c297dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2408,6 +2408,8 @@ "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Failed to find the general chat for this community": "Failed to find the general chat for this community", + "Not you? Sign in": "Not you? Sign in", + "New here? Create an account": "New here? Create an account", "Notification settings": "Notification settings", "Security & privacy": "Security & privacy", "All settings": "All settings", From ca9e43f118bb7386e3bd65e0c92755b0fd26a8bb Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Thu, 19 Nov 2020 07:58:37 +0000 Subject: [PATCH 29/57] reverted translation --- src/i18n/strings/nl.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 000beb915d..1ec887c364 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -199,7 +199,6 @@ "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.", "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.", "Labs": "Experimenteel", - "LaTeX math in messages": "LaTeX wiskunde in berichten", "Last seen": "Laatst gezien", "Leave room": "Gesprek verlaten", "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.", From 7e786e67a8a8d615999d336e15c2c4a7ec14b0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 21 Nov 2020 20:10:38 +0100 Subject: [PATCH 30/57] Added live validation --- .../views/settings/ChangePassword.js | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index bafbc816b9..3e3254c666 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -21,9 +21,16 @@ import PropTypes from 'prop-types'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import AccessibleButton from '../elements/AccessibleButton'; import Spinner from '../elements/Spinner'; +import withValidation from '../elements/Validation'; import { _t } from '../../../languageHandler'; import * as sdk from "../../../index"; import Modal from "../../../Modal"; +import PassphraseField from "../auth/PassphraseField"; + +const FIELD_NEW_PASSWORD = 'field_new_password'; +const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm'; + +const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. export default class ChangePassword extends React.Component { static propTypes = { @@ -63,6 +70,7 @@ export default class ChangePassword extends React.Component { } state = { + fieldValid: {}, phase: ChangePassword.Phases.Edit, oldPassword: "", newPassword: "", @@ -168,6 +176,14 @@ export default class ChangePassword extends React.Component { ); }; + markFieldValid(fieldID, valid) { + const { fieldValid } = this.state; + fieldValid[fieldID] = valid; + this.setState({ + fieldValid, + }); + } + onChangeOldPassword = (ev) => { this.setState({ oldPassword: ev.target.value, @@ -180,12 +196,39 @@ export default class ChangePassword extends React.Component { }); }; + onNewPasswordValidate = result => { + this.markFieldValid(FIELD_NEW_PASSWORD, result.valid); + }; + onChangeNewPasswordConfirm = (ev) => { this.setState({ newPasswordConfirm: ev.target.value, }); }; + onNewPasswordConfirmValidate = async fieldState => { + const result = await this.validatePasswordConfirmRules(fieldState); + this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid); + return result; + }; + + validatePasswordConfirmRules = withValidation({ + rules: [ + { + key: "required", + test: ({ value, allowEmpty }) => allowEmpty || !!value, + invalid: () => _t("Confirm password"), + }, + { + key: "match", + test({ value }) { + return !value || value === this.state.newPassword; + }, + invalid: () => _t("Passwords don't match"), + }, + ], + }); + onClickChange = (ev) => { ev.preventDefault(); const oldPassword = this.state.oldPassword; @@ -202,8 +245,6 @@ export default class ChangePassword extends React.Component { }; render() { - // TODO: Live validation on `new pw == confirm pw` - const rowClassName = this.props.rowClassName; const buttonClassName = this.props.buttonClassName; @@ -220,21 +261,26 @@ export default class ChangePassword extends React.Component { />
- this[FIELD_NEW_PASSWORD] = field} type="password" - label={_t('New Password')} + label='New Password' + minScore={PASSWORD_MIN_SCORE} value={this.state.newPassword} autoFocus={this.props.autoFocusNewPasswordInput} onChange={this.onChangeNewPassword} + onValidate={this.onNewPasswordValidate} autoComplete="new-password" />
this[FIELD_NEW_PASSWORD_CONFIRM] = field} type="password" label={_t("Confirm password")} value={this.state.newPasswordConfirm} onChange={this.onChangeNewPasswordConfirm} + onValidate={this.onNewPasswordConfirmValidate} autoComplete="new-password" />
From 4d7886d1773554e1e47cde096431530b8f1eb636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 21 Nov 2020 21:18:26 +0100 Subject: [PATCH 31/57] Fix i18n --- src/i18n/strings/en_EN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc707222e7..1b54d33bf9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -952,9 +952,9 @@ "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", "Export E2E room keys": "Export E2E room keys", "Do you want to set an email address?": "Do you want to set an email address?", - "Current password": "Current password", - "New Password": "New Password", "Confirm password": "Confirm password", + "Passwords don't match": "Passwords don't match", + "Current password": "Current password", "Change Password": "Change Password", "Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.", "Cross-signing is ready for use.": "Cross-signing is ready for use.", @@ -2301,7 +2301,6 @@ "Use an email address to recover your account": "Use an email address to recover your account", "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)", "Doesn't look like a valid email address": "Doesn't look like a valid email address", - "Passwords don't match": "Passwords don't match", "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details", "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)", "Doesn't look like a valid phone number": "Doesn't look like a valid phone number", @@ -2490,6 +2489,7 @@ "Your Matrix account on ": "Your Matrix account on ", "No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.", "Sign in instead": "Sign in instead", + "New Password": "New Password", "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", "Send Reset Email": "Send Reset Email", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", From cd197133aadc6e188f7276573ac5f265452223c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 22 Nov 2020 08:49:20 +0100 Subject: [PATCH 32/57] Button click validation Check validity when clicking change password button --- .../views/settings/ChangePassword.js | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 3e3254c666..e8ac419c89 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler'; import * as sdk from "../../../index"; import Modal from "../../../Modal"; import PassphraseField from "../auth/PassphraseField"; +import CountlyAnalytics from "../../../CountlyAnalytics"; const FIELD_NEW_PASSWORD = 'field_new_password'; const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm'; @@ -229,8 +230,15 @@ export default class ChangePassword extends React.Component { ], }); - onClickChange = (ev) => { + onClickChange = async (ev) => { ev.preventDefault(); + + const allFieldsValid = await this.verifyFieldsBeforeSubmit(); + if (!allFieldsValid) { + CountlyAnalytics.instance.track("onboarding_registration_submit_failed"); + return; + } + const oldPassword = this.state.oldPassword; const newPassword = this.state.newPassword; const confirmPassword = this.state.newPasswordConfirm; @@ -244,6 +252,73 @@ export default class ChangePassword extends React.Component { } }; + async verifyFieldsBeforeSubmit() { + // Blur the active element if any, so we first run its blur validation, + // which is less strict than the pass we're about to do below for all fields. + const activeElement = document.activeElement; + if (activeElement) { + activeElement.blur(); + } + + const fieldIDsInDisplayOrder = [ + FIELD_NEW_PASSWORD, + FIELD_NEW_PASSWORD_CONFIRM + ]; + + // Run all fields with stricter validation that no longer allows empty + // values for required fields. + for (const fieldID of fieldIDsInDisplayOrder) { + const field = this[fieldID]; + if (!field) { + continue; + } + // We must wait for these validations to finish before queueing + // up the setState below so our setState goes in the queue after + // all the setStates from these validate calls (that's how we + // know they've finished). + await field.validate({ allowEmpty: false }); + } + + // Validation and state updates are async, so we need to wait for them to complete + // first. Queue a `setState` callback and wait for it to resolve. + await new Promise(resolve => this.setState({}, resolve)); + + if (this.allFieldsValid()) { + return true; + } + + const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder); + + if (!invalidField) { + return true; + } + + // Focus the first invalid field and show feedback in the stricter mode + // that no longer allows empty values for required fields. + invalidField.focus(); + invalidField.validate({ allowEmpty: false, focused: true }); + return false; + } + + allFieldsValid() { + const keys = Object.keys(this.state.fieldValid); + for (let i = 0; i < keys.length; ++i) { + if (!this.state.fieldValid[keys[i]]) { + return false; + } + } + return true; + } + + findFirstInvalidField(fieldIDs) { + for (const fieldID of fieldIDs) { + if (!this.state.fieldValid[fieldID] && this[fieldID]) { + return this[fieldID]; + } + } + return null; + } + render() { const rowClassName = this.props.rowClassName; const buttonClassName = this.props.buttonClassName; @@ -271,6 +346,7 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPassword} onValidate={this.onNewPasswordValidate} autoComplete="new-password" + onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
@@ -282,6 +358,7 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPasswordConfirm} onValidate={this.onNewPasswordConfirmValidate} autoComplete="new-password" + onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
From dbce418b63b84ab1ef5c955f52cc4877b7366a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 22 Nov 2020 09:26:51 +0100 Subject: [PATCH 33/57] Check if old password is empty --- .../views/settings/ChangePassword.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index e8ac419c89..557ca6298d 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -28,6 +28,7 @@ import Modal from "../../../Modal"; import PassphraseField from "../auth/PassphraseField"; import CountlyAnalytics from "../../../CountlyAnalytics"; +const FIELD_OLD_PASSWORD = 'field_old_password'; const FIELD_NEW_PASSWORD = 'field_new_password'; const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm'; @@ -191,6 +192,22 @@ export default class ChangePassword extends React.Component { }); }; + onOldPasswordValidate = async fieldState => { + const result = await this.validateOldPasswordRules(fieldState); + this.markFieldValid(FIELD_OLD_PASSWORD, result.valid); + return result; + }; + + validateOldPasswordRules = withValidation({ + rules: [ + { + key: "required", + test: ({ value, allowEmpty }) => allowEmpty || !!value, + invalid: () => _t("Passwords can't be empty"), + } + ], + }); + onChangeNewPassword = (ev) => { this.setState({ newPassword: ev.target.value, @@ -261,8 +278,9 @@ export default class ChangePassword extends React.Component { } const fieldIDsInDisplayOrder = [ + FIELD_OLD_PASSWORD, FIELD_NEW_PASSWORD, - FIELD_NEW_PASSWORD_CONFIRM + FIELD_NEW_PASSWORD_CONFIRM, ]; // Run all fields with stricter validation that no longer allows empty @@ -329,10 +347,13 @@ export default class ChangePassword extends React.Component {
this[FIELD_OLD_PASSWORD] = field} type="password" label={_t('Current password')} value={this.state.oldPassword} onChange={this.onChangeOldPassword} + onValidate={this.onOldPasswordValidate} + onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
From 15ffdcb6525d0c1fa3fbbde6f1965bbc104a51fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 22 Nov 2020 09:57:22 +0100 Subject: [PATCH 34/57] Added trailing comma --- src/components/views/settings/ChangePassword.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 557ca6298d..b4585452f8 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -204,7 +204,7 @@ export default class ChangePassword extends React.Component { key: "required", test: ({ value, allowEmpty }) => allowEmpty || !!value, invalid: () => _t("Passwords can't be empty"), - } + }, ], }); From 3780afff7d77338895b342269fdd6b4f9106a967 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Nov 2020 14:40:01 -0700 Subject: [PATCH 35/57] Fix existing widgets not having approved capabilities for their function Fixes https://github.com/vector-im/element-web/issues/15827 This also fixes sticker pickers. --- src/stores/widgets/StopGapWidgetDriver.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 59cdbfe3e5..830d679968 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -16,6 +16,7 @@ import { Capability, + EventDirection, IOpenIDCredentials, IOpenIDUpdate, ISendEventDetails, @@ -24,6 +25,7 @@ import { SimpleObservable, Widget, WidgetDriver, + WidgetEventCapability, WidgetKind, } from "matrix-widget-api"; import { iterableDiff, iterableUnion } from "../../utils/iterables"; @@ -37,6 +39,8 @@ import WidgetCapabilitiesPromptDialog, { getRememberedCapabilitiesForWidget, } from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog"; import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions"; +import { WidgetType } from "../../widgets/WidgetType"; +import { EventType } from "matrix-js-sdk/src/@types/event"; // TODO: Purge this from the universe @@ -51,6 +55,15 @@ export class StopGapWidgetDriver extends WidgetDriver { // spew screenshots at us and can't request screenshots of us, so it's up to us to provide the // button if the widget says it supports screenshots. this.allowedCapabilities = new Set([...allowedCapabilities, MatrixCapabilities.Screenshots]); + + // Grant the permissions that are specific to given widget types + if (WidgetType.JITSI.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) { + this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen); + } else if (WidgetType.STICKERPICKER.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Account) { + const stickerSendingCap = WidgetEventCapability.forRoomEvent(EventDirection.Send, EventType.Sticker).raw; + this.allowedCapabilities.add(MatrixCapabilities.StickerSending); // legacy as far as MSC2762 is concerned + this.allowedCapabilities.add(stickerSendingCap); + } } public async validateCapabilities(requested: Set): Promise> { From c91dc55bc1fc5aa653bc6e4662d74335d13f0a01 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Nov 2020 18:35:00 -0700 Subject: [PATCH 36/57] Make modal widgets static to avoid being destroyed Fixes https://github.com/vector-im/element-web/issues/15818 --- src/stores/ModalWidgetStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts index 0485afd106..c0b64d76fe 100644 --- a/src/stores/ModalWidgetStore.ts +++ b/src/stores/ModalWidgetStore.ts @@ -64,7 +64,7 @@ export class ModalWidgetStore extends AsyncStoreWithClient { this.openSourceWidgetId = null; this.modalInstance = null; }, - }); + }, null, /* priority = */ false, /* static = */ true); }; public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => { From 5da27aed945aae35c9f977c03d139c4a8ba00bda Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Nov 2020 18:39:11 -0700 Subject: [PATCH 37/57] Replace the concept of a Widget Security Key with an OIDC state The security key naming/practice was misguided, so let's call it what it is (a settings key) and abstract away the complexity to a new store. Fixes https://github.com/vector-im/element-web/issues/15820 while we're here. --- .../dialogs/WidgetOpenIDPermissionsDialog.js | 27 ++---- src/stores/widgets/StopGapWidget.ts | 2 +- src/stores/widgets/StopGapWidgetDriver.ts | 25 +++-- src/stores/widgets/WidgetPermissionStore.ts | 93 +++++++++++++++++++ src/utils/WidgetUtils.ts | 22 ----- 5 files changed, 119 insertions(+), 50 deletions(-) create mode 100644 src/stores/widgets/WidgetPermissionStore.ts diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index e793b85079..7ed3d04318 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -17,18 +17,17 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; import * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; -import WidgetUtils from "../../../utils/WidgetUtils"; -import {SettingLevel} from "../../../settings/SettingLevel"; +import {Widget} from "matrix-widget-api"; +import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore"; export default class WidgetOpenIDPermissionsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, - widgetUrl: PropTypes.string.isRequired, - widgetId: PropTypes.string.isRequired, - isUserWidget: PropTypes.bool.isRequired, + widget: PropTypes.objectOf(Widget).isRequired, + widgetKind: PropTypes.string.isRequired, // WidgetKind from widget-api + inRoomId: PropTypes.string, }; constructor() { @@ -51,16 +50,10 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { if (this.state.rememberSelection) { console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`); - const currentValues = SettingsStore.getValue("widgetOpenIDPermissions"); - if (!currentValues.allow) currentValues.allow = []; - if (!currentValues.deny) currentValues.deny = []; - - const securityKey = WidgetUtils.getWidgetSecurityKey( - this.props.widgetId, - this.props.widgetUrl, - this.props.isUserWidget); - (allowed ? currentValues.allow : currentValues.deny).push(securityKey); - SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); + WidgetPermissionStore.instance.setOIDCState( + this.props.widget, this.props.widgetKind, this.props.inRoomId, + allowed ? OIDCState.Allowed : OIDCState.Denied, + ); } this.props.onFinished(allowed); @@ -84,7 +77,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { "A widget located at %(widgetUrl)s would like to verify your identity. " + "By allowing this, the widget will be able to verify your user ID, but not " + "perform actions as you.", { - widgetUrl: this.props.widgetUrl.split("?")[0], + widgetUrl: this.props.widget.templateUrl.split("?")[0], }, )}

diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 3485e153e1..29d63cf3fa 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -246,7 +246,7 @@ export class StopGapWidget extends EventEmitter { public start(iframe: HTMLIFrameElement) { if (this.started) return; const allowedCapabilities = this.appTileProps.whitelistCapabilities || []; - const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget, this.kind); + const driver = new StopGapWidgetDriver(allowedCapabilities, this.mockWidget, this.kind, this.roomId); this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging.on("preparing", () => this.emit("preparing")); this.messaging.on("ready", () => this.emit("ready")); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 59cdbfe3e5..b6e2f6c681 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -30,13 +30,12 @@ import { iterableDiff, iterableUnion } from "../../utils/iterables"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import ActiveRoomObserver from "../../ActiveRoomObserver"; import Modal from "../../Modal"; -import WidgetUtils from "../../utils/WidgetUtils"; -import SettingsStore from "../../settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetCapabilitiesPromptDialog, { getRememberedCapabilitiesForWidget, } from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog"; import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions"; +import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore"; // TODO: Purge this from the universe @@ -44,7 +43,12 @@ export class StopGapWidgetDriver extends WidgetDriver { private allowedCapabilities: Set; // TODO: Refactor widgetKind into the Widget class - constructor(allowedCapabilities: Capability[], private forWidget: Widget, private forWidgetKind: WidgetKind) { + constructor( + allowedCapabilities: Capability[], + private forWidget: Widget, + private forWidgetKind: WidgetKind, + private inRoomId?: string, + ) { super(); // Always allow screenshots to be taken because it's a client-induced flow. The widget can't @@ -114,26 +118,27 @@ export class StopGapWidgetDriver extends WidgetDriver { public async askOpenID(observer: SimpleObservable) { const isUserWidget = this.forWidgetKind !== WidgetKind.Room; // modal and account widgets are "user" widgets const rawUrl = this.forWidget.templateUrl; - const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.forWidget.id, rawUrl, isUserWidget); + const oidcState = WidgetPermissionStore.instance.getOIDCState( + this.forWidget, this.forWidgetKind, this.inRoomId, + ); const getToken = (): Promise => { return MatrixClientPeg.get().getOpenIdToken(); }; - const settings = SettingsStore.getValue("widgetOpenIDPermissions"); - if (settings?.deny?.includes(widgetSecurityKey)) { + if (oidcState === OIDCState.Denied) { return observer.update({state: OpenIDRequestState.Blocked}); } - if (settings?.allow?.includes(widgetSecurityKey)) { + if (oidcState === OIDCState.Allowed) { return observer.update({state: OpenIDRequestState.Allowed, token: await getToken()}); } observer.update({state: OpenIDRequestState.PendingUserConfirmation}); Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, { - widgetUrl: rawUrl, - widgetId: this.forWidget.id, - isUserWidget: isUserWidget, + widget: this.forWidget, + widgetKind: this.forWidgetKind, + inRoomId: this.inRoomId, onFinished: async (confirm) => { if (!confirm) { diff --git a/src/stores/widgets/WidgetPermissionStore.ts b/src/stores/widgets/WidgetPermissionStore.ts new file mode 100644 index 0000000000..c2c30911c9 --- /dev/null +++ b/src/stores/widgets/WidgetPermissionStore.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 { AsyncStore } from "../AsyncStore"; +import { ActionPayload } from "../../dispatcher/payloads"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import SettingsStore from "../../settings/SettingsStore"; +import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; +import { IWidget, Widget, WidgetKind } from "matrix-widget-api"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import WidgetUtils from "../../utils/WidgetUtils"; +import { SettingLevel } from "../../settings/SettingLevel"; + +export enum OIDCState { + Allowed, // user has set the remembered value as allowed + Denied, // user has set the remembered value as disallowed + Unknown, // user has not set a remembered value +} + +export class WidgetPermissionStore { + private static internalInstance: WidgetPermissionStore; + + private constructor() { + } + + public static get instance(): WidgetPermissionStore { + if (!WidgetPermissionStore.internalInstance) { + WidgetPermissionStore.internalInstance = new WidgetPermissionStore(); + } + return WidgetPermissionStore.internalInstance; + } + + // TODO (all functions here): Merge widgetKind with the widget definition + + private packSettingKey(widget: Widget, kind: WidgetKind, roomId?: string): string { + let location = roomId; + if (kind !== WidgetKind.Room) { + location = MatrixClientPeg.get().getUserId(); + } + if (kind === WidgetKind.Modal) { + location = '*MODAL*-' + location; // to guarantee differentiation from whatever spawned it + } + if (!location) { + throw new Error("Failed to determine a location to check the widget's OIDC state with"); + } + + return encodeURIComponent(`${location}::${widget.templateUrl}`); + } + + public getOIDCState(widget: Widget, kind: WidgetKind, roomId?: string): OIDCState { + const settingsKey = this.packSettingKey(widget, kind, roomId); + const settings = SettingsStore.getValue("widgetOpenIDPermissions"); + if (settings?.deny?.includes(settingsKey)) { + return OIDCState.Denied; + } + if (settings?.allow?.includes(settingsKey)) { + return OIDCState.Allowed; + } + return OIDCState.Unknown; + } + + public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string, newState: OIDCState) { + const settingsKey = this.packSettingKey(widget, kind, roomId); + + const currentValues = SettingsStore.getValue("widgetOpenIDPermissions"); + if (!currentValues.allow) currentValues.allow = []; + if (!currentValues.deny) currentValues.deny = []; + + if (newState === OIDCState.Allowed) { + currentValues.allow.push(settingsKey); + } else if (newState === OIDCState.Denied) { + currentValues.deny.push(settingsKey); + } else { + currentValues.allow = currentValues.allow.filter(c => c !== settingsKey); + currentValues.deny = currentValues.deny.filter(c => c !== settingsKey); + } + + SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); + } +} diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 526c2d5ce7..986c68342c 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -22,7 +22,6 @@ import SdkConfig from "../SdkConfig"; import dis from '../dispatcher/dispatcher'; import WidgetEchoStore from '../stores/WidgetEchoStore'; import SettingsStore from "../settings/SettingsStore"; -import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {Room} from "matrix-js-sdk/src/models/room"; import {WidgetType} from "../widgets/WidgetType"; @@ -457,27 +456,6 @@ export default class WidgetUtils { return capWhitelist; } - static getWidgetSecurityKey(widgetId: string, widgetUrl: string, isUserWidget: boolean): string { - let widgetLocation = ActiveWidgetStore.getRoomId(widgetId); - - if (isUserWidget) { - const userWidget = WidgetUtils.getUserWidgetsArray() - .find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl); - - if (!userWidget) { - throw new Error("No matching user widget to form security key"); - } - - widgetLocation = userWidget.sender; - } - - if (!widgetLocation) { - throw new Error("Failed to locate where the widget resides"); - } - - return encodeURIComponent(`${widgetLocation}::${widgetUrl}`); - } - static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) { // NB. we can't just encodeURIComponent all of these because the $ signs need to be there const queryStringParts = [ From 51f62052587e1ee392dbd151d62c755945125375 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Nov 2020 18:48:18 -0700 Subject: [PATCH 38/57] Fix modal buttons not being disabled by disabling them Looks like this was just a missed block of code, but also the important bit. Fixes https://github.com/vector-im/element-web/issues/15801 --- src/components/views/dialogs/ModalWidgetDialog.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index e722374555..520972975e 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -161,7 +161,9 @@ export default class ModalWidgetDialog extends React.PureComponent + const isDisabled = this.state.disabledButtonIds.includes(def.id); + + return { def.label } ; }); From fc820c4b1a9fa91ec285c1c7f02f980bace576ec Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Nov 2020 18:51:27 -0700 Subject: [PATCH 39/57] Construct modal widgets in the same way we do any other widget Fixes https://github.com/vector-im/element-web/issues/15800 --- src/components/views/dialogs/ModalWidgetDialog.tsx | 3 ++- src/stores/widgets/StopGapWidget.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index 520972975e..484e8f0dcf 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import RoomViewStore from "../../../stores/RoomViewStore"; import {OwnProfileStore} from "../../../stores/OwnProfileStore"; import { arrayFastClone } from "../../../utils/arrays"; +import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; interface IProps { widgetDefinition: IModalWidgetOpenRequestData; @@ -64,7 +65,7 @@ export default class ModalWidgetDialog extends React.PureComponent Date: Wed, 25 Nov 2020 18:58:30 -0700 Subject: [PATCH 40/57] Appease the linter --- src/stores/widgets/StopGapWidgetDriver.ts | 2 -- src/stores/widgets/WidgetPermissionStore.ts | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index b6e2f6c681..2535f205fc 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -116,8 +116,6 @@ export class StopGapWidgetDriver extends WidgetDriver { } public async askOpenID(observer: SimpleObservable) { - const isUserWidget = this.forWidgetKind !== WidgetKind.Room; // modal and account widgets are "user" widgets - const rawUrl = this.forWidget.templateUrl; const oidcState = WidgetPermissionStore.instance.getOIDCState( this.forWidget, this.forWidgetKind, this.inRoomId, ); diff --git a/src/stores/widgets/WidgetPermissionStore.ts b/src/stores/widgets/WidgetPermissionStore.ts index c2c30911c9..41e8bc6652 100644 --- a/src/stores/widgets/WidgetPermissionStore.ts +++ b/src/stores/widgets/WidgetPermissionStore.ts @@ -14,14 +14,9 @@ * limitations under the License. */ -import { AsyncStore } from "../AsyncStore"; -import { ActionPayload } from "../../dispatcher/payloads"; -import defaultDispatcher from "../../dispatcher/dispatcher"; import SettingsStore from "../../settings/SettingsStore"; -import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; -import { IWidget, Widget, WidgetKind } from "matrix-widget-api"; +import { Widget, WidgetKind } from "matrix-widget-api"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import WidgetUtils from "../../utils/WidgetUtils"; import { SettingLevel } from "../../settings/SettingLevel"; export enum OIDCState { From b9af446c1bcd28e9c3d99364c8f9b7cdb5f0df83 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Nov 2020 19:42:57 -0700 Subject: [PATCH 41/57] Make it possible in-code to hide rooms from the room list Fixes https://github.com/vector-im/element-web/issues/15745 This was surprisingly easy given the number of errors I remember last time, but here it is. This also includes an over-engineered VisibilityProvider with the intention that it'll get used in the future for things like Spaces and other X as Rooms stuff. --- src/customisations/RoomList.ts | 45 ++++++++++++++ src/stores/room-list/RoomListStore.ts | 4 +- src/stores/room-list/algorithms/Algorithm.ts | 5 ++ .../room-list/filters/VisibilityProvider.ts | 59 +++++++++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/customisations/RoomList.ts create mode 100644 src/stores/room-list/filters/VisibilityProvider.ts diff --git a/src/customisations/RoomList.ts b/src/customisations/RoomList.ts new file mode 100644 index 0000000000..758b212aa2 --- /dev/null +++ b/src/customisations/RoomList.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 { Room } from "matrix-js-sdk/src/models/room"; + +// Populate this file with the details of your customisations when copying it. + +/** + * Determines if a room is visible in the room list or not. By default, + * all rooms are visible. Where special handling is performed by Element, + * those rooms will not be able to override their visibility in the room + * list - Element will make the decision without calling this function. + * + * This function should be as fast as possible to avoid slowing down the + * client. + * @param {Room} room The room to check the visibility of. + * @returns {boolean} True if the room should be visible, false otherwise. + */ +function isRoomVisible(room: Room): boolean { + return true; +} + +// This interface summarises all available customisation points and also marks +// them all as optional. This allows customisers to only define and export the +// customisations they need while still maintaining type safety. +export interface IRoomListCustomisations { + isRoomVisible?: typeof isRoomVisible; +} + +// A real customisation module will define and export one or more of the +// customisation points that make up the interface above. +export const RoomListCustomisations: IRoomListCustomisations = {}; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 0f3138fe9e..c60f35118b 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -34,6 +34,7 @@ import { MarkedExecution } from "../../utils/MarkedExecution"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { NameFilterCondition } from "./filters/NameFilterCondition"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; +import { VisibilityProvider } from "./filters/VisibilityProvider"; interface IState { tagsEnabled?: boolean; @@ -544,7 +545,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient { public async regenerateAllLists({trigger = true}) { console.warn("Regenerating all room lists"); - const rooms = this.matrixClient.getVisibleRooms(); + const rooms = this.matrixClient.getVisibleRooms() + .filter(r => VisibilityProvider.instance.isRoomVisible(r)); const customTags = new Set(); if (this.state.tagsEnabled) { for (const room of rooms) { diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 439141edb4..25059aabe7 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -34,6 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; import SettingsStore from "../../../settings/SettingsStore"; +import { VisibilityProvider } from "../filters/VisibilityProvider"; /** * Fired when the Algorithm has determined a list has been updated. @@ -188,6 +189,10 @@ export class Algorithm extends EventEmitter { // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing, // otherwise we risk duplicating rooms. + if (val && !VisibilityProvider.instance.isRoomVisible(val)) { + val = null; // the room isn't visible - lie to the rest of this function + } + // Set the last sticky room to indicate that we're in a change. The code throughout the // class can safely handle a null room, so this should be safe to do as a backup. this._lastStickyRoom = this._stickyRoom || {}; diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts new file mode 100644 index 0000000000..def2c20514 --- /dev/null +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 {Room} from "matrix-js-sdk/src/models/room"; +import { RoomListCustomisations } from "../../../customisations/RoomList"; + +export class VisibilityProvider { + private static internalInstance: VisibilityProvider; + + private constructor() { + } + + public static get instance(): VisibilityProvider { + if (!VisibilityProvider.internalInstance) { + VisibilityProvider.internalInstance = new VisibilityProvider(); + } + return VisibilityProvider.internalInstance; + } + + public isRoomVisible(room: Room): boolean { + let isVisible = true; // Returned at the end of this function + let forced = false; // When true, this function won't bother calling the customisation points + + // ------ + // TODO: The `if` statements to control visibility of custom room types + // would go here. The remainder of this function assumes that the statements + // will be here. + + // An example of how the `if` statements mentioned above would look follows. + // A real check would probably check for a `type` or something instead of the room ID. + // Note: the room ID here is intentionally invalid to prevent accidental hiding of someone's room. + // TODO: Remove this statement once we have a statement to replace it (just keeping the reference count up) + if (room.roomId === '~!JFmkoouJANxFGtmMYC:localhost') { + isVisible = false; + forced = true; + } + // ------ + + const isVisibleFn = RoomListCustomisations.isRoomVisible; + if (!forced && isVisibleFn) { + isVisible = isVisibleFn(room); + } + + return isVisible; + } +} From 80b93e0843c01eeb5196dc5ece41218921b682da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Nov 2020 20:03:58 -0700 Subject: [PATCH 42/57] Mute all updates from rooms that are invisible --- src/stores/room-list/RoomListStore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index c60f35118b..b2fe630760 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -402,6 +402,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + if (!VisibilityProvider.instance.isRoomVisible(room)) { + return; // don't do anything on rooms that aren't visible + } + const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); if (shouldUpdate) { if (SettingsStore.getValue("advancedRoomListLogging")) { From b9c57f47b04df359eb840a53ce94f08778864e7a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 26 Nov 2020 08:01:38 -0700 Subject: [PATCH 43/57] Remove example --- src/stores/room-list/filters/VisibilityProvider.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index def2c20514..2e4eb485c0 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -38,15 +38,6 @@ export class VisibilityProvider { // TODO: The `if` statements to control visibility of custom room types // would go here. The remainder of this function assumes that the statements // will be here. - - // An example of how the `if` statements mentioned above would look follows. - // A real check would probably check for a `type` or something instead of the room ID. - // Note: the room ID here is intentionally invalid to prevent accidental hiding of someone's room. - // TODO: Remove this statement once we have a statement to replace it (just keeping the reference count up) - if (room.roomId === '~!JFmkoouJANxFGtmMYC:localhost') { - isVisible = false; - forced = true; - } // ------ const isVisibleFn = RoomListCustomisations.isRoomVisible; From c2c328e23c66f4d1a50e163432b1fd029fbbe0cd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 26 Nov 2020 08:06:48 -0700 Subject: [PATCH 44/57] Appease the linter --- src/stores/room-list/filters/VisibilityProvider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index 2e4eb485c0..553dd33ce0 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -31,13 +31,17 @@ export class VisibilityProvider { } public isRoomVisible(room: Room): boolean { + /* eslint-disable prefer-const */ let isVisible = true; // Returned at the end of this function let forced = false; // When true, this function won't bother calling the customisation points + /* eslint-enable prefer-const */ // ------ // TODO: The `if` statements to control visibility of custom room types // would go here. The remainder of this function assumes that the statements // will be here. + // + // When removing this comment block, please remove the lint disable lines in the area. // ------ const isVisibleFn = RoomListCustomisations.isRoomVisible; From acd148d807446cecf71bbe5d68fb87aa3a814edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 26 Nov 2020 16:58:34 +0100 Subject: [PATCH 45/57] Remove nonsense lines --- src/components/views/settings/ChangePassword.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index b4585452f8..22b758b1ca 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -353,7 +353,6 @@ export default class ChangePassword extends React.Component { value={this.state.oldPassword} onChange={this.onChangeOldPassword} onValidate={this.onOldPasswordValidate} - onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
@@ -367,7 +366,6 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPassword} onValidate={this.onNewPasswordValidate} autoComplete="new-password" - onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
@@ -379,7 +377,6 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPasswordConfirm} onValidate={this.onNewPasswordConfirmValidate} autoComplete="new-password" - onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
From dacef10fa605107ab6b512a0e5e255c5696e051c Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Thu, 26 Nov 2020 16:22:10 +0000 Subject: [PATCH 46/57] reverted US translation --- src/i18n/strings/en_US.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index c00bf03b29..a1275fb089 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -128,7 +128,6 @@ "Kick": "Kick", "Kicks user with given id": "Kicks user with given id", "Labs": "Labs", - "LaTeX math in messages": "LaTeX math in messages", "Ignore": "Ignore", "Unignore": "Unignore", "You are now ignoring %(userId)s": "You are now ignoring %(userId)s", From 7013483dadfeea29d4aa9a942537d30aff240f24 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Thu, 26 Nov 2020 17:26:42 +0000 Subject: [PATCH 47/57] UK spelling maths --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e8a2fb53c2..faa376f333 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -699,7 +699,7 @@ "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Change notification settings": "Change notification settings", - "LaTeX math in messages": "LaTeX math in messages", + "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "New spinner design": "New spinner design", "Message Pinning": "Message Pinning", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 5600a1346d..a7c1f849fc 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -118,7 +118,7 @@ export interface ISetting { export const SETTINGS: {[setting: string]: ISetting} = { "feature_latex_maths": { isFeature: true, - displayName: _td("LaTeX math in messages"), + displayName: _td("Render LaTeX maths in messages"), supportedLevels: LEVELS_FEATURE, default: false, }, From 494ae3e4215cc2fe0583c316b8ea1d895503d39e Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Thu, 26 Nov 2020 17:45:11 +0000 Subject: [PATCH 48/57] parse html for latex rendering inside settings block --- src/HtmlUtils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 44fbffb97f..43aeae24e6 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -418,10 +418,10 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); - const phtml = cheerio.load(safeBody, - { _useHtmlParser2: true, decodeEntities: false }) if (SettingsStore.getValue("feature_latex_maths")) { + const phtml = cheerio.load(safeBody, + { _useHtmlParser2: true, decodeEntities: false }) phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { return katex.renderToString( AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), From 79baea9c4a7c236e63622b5189806016ecd5f999 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Thu, 26 Nov 2020 17:54:11 +0000 Subject: [PATCH 49/57] fixed indent --- src/HtmlUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 43aeae24e6..2301ad250b 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -421,7 +421,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (SettingsStore.getValue("feature_latex_maths")) { const phtml = cheerio.load(safeBody, - { _useHtmlParser2: true, decodeEntities: false }) + { _useHtmlParser2: true, decodeEntities: false }) phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { return katex.renderToString( AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), From 80f1df6d954eeb2ed666d302c485d15dc6773fd1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 26 Nov 2020 15:09:08 -0700 Subject: [PATCH 50/57] Don't needlessly persist user widgets Fixes https://github.com/vector-im/element-web/issues/15842 We don't have a concept of a stickerpicker staying on screen, so don't make it a thing yet. --- src/components/views/elements/AppTile.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b862a1e912..7e0ae965bb 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -375,17 +375,20 @@ export default class AppTile extends React.Component { ); - // all widgets can theoretically be allowed to remain on screen, so we wrap - // them all in a PersistedElement from the get-go. If we wait, the iframe will - // be re-mounted later, which means the widget has to start over, which is bad. + if (!this.props.userWidget) { + // All room widgets can theoretically be allowed to remain on screen, so we + // wrap them all in a PersistedElement from the get-go. If we wait, the iframe + // will be re-mounted later, which means the widget has to start over, which is + // bad. - // Also wrap the PersistedElement in a div to fix the height, otherwise - // AppTile's border is in the wrong place - appTileBody =
- - {appTileBody} - -
; + // Also wrap the PersistedElement in a div to fix the height, otherwise + // AppTile's border is in the wrong place + appTileBody =
+ + {appTileBody} + +
; + } } } From f2bc3db8fd4fdd8410f42b67f2f1c76f65e992da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 26 Nov 2020 15:09:44 -0700 Subject: [PATCH 51/57] Fix visual gap of sticker picker at bottom Fixes https://github.com/vector-im/element-web/issues/15690 --- res/css/views/rooms/_Stickers.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_Stickers.scss b/res/css/views/rooms/_Stickers.scss index 94f42efe83..da86797f42 100644 --- a/res/css/views/rooms/_Stickers.scss +++ b/res/css/views/rooms/_Stickers.scss @@ -22,7 +22,7 @@ iframe { // Sticker picker depends on the fixed height previously used for all tiles - height: 273px; + height: 283px; // height of the popout minus the AppTile menu bar } } From 00b1cd01eb451ea988421689afbd4d9c880c8fd6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 27 Nov 2020 09:44:04 +0000 Subject: [PATCH 52/57] Update copy --- src/components/structures/UserMenu.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index e38dd5c2b9..08bd472225 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -276,7 +276,7 @@ export default class UserMenu extends React.Component { if (MatrixClientPeg.get().isGuest()) { topSection = (
- {_t("Not you? Sign in", {}, { + {_t("Got an account? Sign in", {}, { a: sub => ( {sub} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4de5c297dd..c865d304bf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2408,7 +2408,7 @@ "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Failed to find the general chat for this community": "Failed to find the general chat for this community", - "Not you? Sign in": "Not you? Sign in", + "Got an account? Sign in": "Got an account? Sign in", "New here? Create an account": "New here? Create an account", "Notification settings": "Notification settings", "Security & privacy": "Security & privacy", From 86b2cd1f8295ebbef3767a40e2716bf55b9c661e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 27 Nov 2020 11:11:11 +0000 Subject: [PATCH 53/57] Use typeof in customisations to avoid repeating --- src/customisations/Security.ts | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts index eb7c27dcc5..96b5b62cdb 100644 --- a/src/customisations/Security.ts +++ b/src/customisations/Security.ts @@ -67,24 +67,13 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean { // them all as optional. This allows customisers to only define and export the // customisations they need while still maintaining type safety. export interface ISecurityCustomisations { - examineLoginResponse?: ( - response: any, - credentials: IMatrixClientCreds, - ) => void; - persistCredentials?: ( - credentials: IMatrixClientCreds, - ) => void; - createSecretStorageKey?: () => Uint8Array, - getSecretStorageKey?: () => Uint8Array, - catchAccessSecretStorageError?: ( - e: Error, - ) => void, - setupEncryptionNeeded?: ( - kind: SetupEncryptionKind, - ) => boolean, - getDehydrationKey?: ( - keyInfo: ISecretStorageKeyInfo, - ) => Promise, + examineLoginResponse?: typeof examineLoginResponse; + persistCredentials?: typeof persistCredentials; + createSecretStorageKey?: typeof createSecretStorageKey, + getSecretStorageKey?: typeof getSecretStorageKey, + catchAccessSecretStorageError?: typeof catchAccessSecretStorageError, + setupEncryptionNeeded?: typeof setupEncryptionNeeded, + getDehydrationKey?: typeof getDehydrationKey, } // A real customisation module will define and export one or more of the From 25cc4b89b8a7da956b452b74b7fbedf7be3a6567 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 27 Nov 2020 11:19:44 +0000 Subject: [PATCH 54/57] Add lifecycle customisation point after logout This will help specific deployments that need to do something custom here such as redirect the user or call some API after Element has logged out and cleared storage. --- src/Lifecycle.ts | 2 ++ src/customisations/Lifecycle.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/customisations/Lifecycle.ts diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 8451568dd1..6c9c21ffc0 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -49,6 +49,7 @@ import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; import CallHandler from './CallHandler'; +import LifecycleCustomisations from "./customisations/Lifecycle"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -716,6 +717,7 @@ export async function onLoggedOut(): Promise { dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); await clearStorage({deleteEverything: true}); + LifecycleCustomisations.onLoggedOutAndStorageCleared?.(); } /** diff --git a/src/customisations/Lifecycle.ts b/src/customisations/Lifecycle.ts new file mode 100644 index 0000000000..eba2af715a --- /dev/null +++ b/src/customisations/Lifecycle.ts @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +function onLoggedOutAndStorageCleared(): void { + // E.g. redirect user or call other APIs after logout +} + +// This interface summarises all available customisation points and also marks +// them all as optional. This allows customisers to only define and export the +// customisations they need while still maintaining type safety. +export interface ILifecycleCustomisations { + onLoggedOutAndStorageCleared?: typeof onLoggedOutAndStorageCleared; +} + +// A real customisation module will define and export one or more of the +// customisation points that make up `ILifecycleCustomisations`. +export default {} as ILifecycleCustomisations; From 65ab0ee6650badd474c6f322b69f3fda9df5f319 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Nov 2020 12:53:09 +0000 Subject: [PATCH 55/57] Slightly better error if we can't capture user media Fixes https://github.com/vector-im/element-web/issues/15837 --- src/CallHandler.tsx | 39 +++++++++++++++++++++++++++++++++++-- src/i18n/strings/en_EN.json | 7 +++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 3be203ab98..710cd10f99 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -80,6 +80,7 @@ import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } import Analytics from './Analytics'; import CountlyAnalytics from "./CountlyAnalytics"; import {UIFeature} from "./settings/UIFeature"; +import { CallError } from "matrix-js-sdk/src/webrtc/call"; enum AudioID { Ring = 'ringAudio', @@ -226,11 +227,17 @@ export default class CallHandler { } private setCallListeners(call: MatrixCall) { - call.on(CallEvent.Error, (err) => { + call.on(CallEvent.Error, (err: CallError) => { if (!this.matchesCallForThisRoom(call)) return; - Analytics.trackEvent('voip', 'callError', 'error', err); + Analytics.trackEvent('voip', 'callError', 'error', err.toString()); console.error("Call error:", err); + + if (err.code === CallErrorCode.NoUserMedia) { + this.showMediaCaptureError(call); + return; + } + if ( MatrixClientPeg.get().getTurnServers().length === 0 && SettingsStore.getValue("fallbackICEServerAllowed") === null @@ -377,6 +384,34 @@ export default class CallHandler { }, null, true); } + private showMediaCaptureError(call: MatrixCall) { + let title; + let description; + + if (call.type === CallType.Voice) { + title = _t("Unable to access microphone"); + description =
+ {_t( + "Call failed because no microphone could not be accessed. " + + "Check that a microphone is plugged in and set up correctly.", + )} +
; + } else if (call.type === CallType.Video) { + title = _t("Unable to access webcam / microphone"); + description =
+ {_t("Call failed because no webcam or microphone could not be accessed. Check that:")} +
    +
  • {_t("A microphone and webcam are plugged in and set up correctly")}
  • +
  • {_t("Permission is granted to usethe webcam")}
  • +
  • {_t("No other application is using the webcam")}
  • +
+
; + } + + Modal.createTrackedDialog('Media capture failed', '', ErrorDialog, { + title, description, + }, null, true); + } private placeCall( roomId: string, type: PlaceCallType, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0d50128f32..165a312332 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -46,6 +46,13 @@ "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", "Try using turn.matrix.org": "Try using turn.matrix.org", "OK": "OK", + "Unable to access microphone": "Unable to access microphone", + "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", + "Unable to access webcam / microphone": "Unable to access webcam / microphone", + "Call failed because no webcam or microphone could not be accessed. Check that:": "Call failed because no webcam or microphone could not be accessed. Check that:", + "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", + "Permission is granted to usethe webcam": "Permission is granted to usethe webcam", + "No other application is using the webcam": "No other application is using the webcam", "Unable to capture screen": "Unable to capture screen", "Existing Call": "Existing Call", "You are already in a call.": "You are already in a call.", From 522c2d9dc77d38db6f6447597102ed5494f959b7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Nov 2020 14:03:52 +0000 Subject: [PATCH 56/57] Typo Co-authored-by: J. Ryan Stinnett --- src/CallHandler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 710cd10f99..abfe5cc9bf 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -402,7 +402,7 @@ export default class CallHandler { {_t("Call failed because no webcam or microphone could not be accessed. Check that:")}
  • {_t("A microphone and webcam are plugged in and set up correctly")}
  • -
  • {_t("Permission is granted to usethe webcam")}
  • +
  • {_t("Permission is granted to use the webcam")}
  • {_t("No other application is using the webcam")}
; From 9a5f2c85cd03bf27e0cfc7b8070acd901f49078e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Nov 2020 14:04:27 +0000 Subject: [PATCH 57/57] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 165a312332..cc85a95271 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -51,7 +51,7 @@ "Unable to access webcam / microphone": "Unable to access webcam / microphone", "Call failed because no webcam or microphone could not be accessed. Check that:": "Call failed because no webcam or microphone could not be accessed. Check that:", "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", - "Permission is granted to usethe webcam": "Permission is granted to usethe webcam", + "Permission is granted to use the webcam": "Permission is granted to use the webcam", "No other application is using the webcam": "No other application is using the webcam", "Unable to capture screen": "Unable to capture screen", "Existing Call": "Existing Call",