From e768ecb3d0806e5cc1e247ae24b666e512b0fb6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Jun 2021 13:01:26 +0100 Subject: [PATCH 01/23] Typescript conversion of Composer components and more --- src/CountlyAnalytics.ts | 3 +- src/HtmlUtils.tsx | 4 +- src/SlashCommands.tsx | 15 +- .../{TextualBody.js => TextualBody.tsx} | 196 +++++++-------- .../{TextualEvent.js => TextualEvent.tsx} | 19 +- .../views/rooms/BasicMessageComposer.tsx | 96 ++++---- ...ageComposer.js => EditMessageComposer.tsx} | 223 +++++++++--------- .../views/rooms/MessageComposer.tsx | 10 +- ...matBar.js => MessageComposerFormatBar.tsx} | 84 ++++--- ...ageComposer.js => SendMessageComposer.tsx} | 186 ++++++++------- src/editor/model.ts | 54 ++--- src/editor/parts.ts | 8 +- src/utils/EditorStateTransfer.ts | 14 +- src/utils/{pillify.js => pillify.tsx} | 20 +- .../views/rooms/SendMessageComposer-test.js | 4 +- 15 files changed, 492 insertions(+), 444 deletions(-) rename src/components/views/messages/{TextualBody.js => TextualBody.tsx} (80%) rename src/components/views/messages/{TextualEvent.js => TextualEvent.tsx} (72%) rename src/components/views/rooms/{EditMessageComposer.js => EditMessageComposer.tsx} (70%) rename src/components/views/rooms/{MessageComposerFormatBar.js => MessageComposerFormatBar.tsx} (55%) rename src/components/views/rooms/{SendMessageComposer.js => SendMessageComposer.tsx} (78%) rename src/utils/{pillify.js => pillify.tsx} (91%) diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 39dcac4048..3ad56fe3bf 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { randomString } from "matrix-js-sdk/src/randomstring"; +import { IContent } from "matrix-js-sdk/src/models/event"; import { getCurrentLanguage } from './languageHandler'; import PlatformPeg from './PlatformPeg'; @@ -868,7 +869,7 @@ export default class CountlyAnalytics { roomId: string, isEdit: boolean, isReply: boolean, - content: {format?: string, msgtype: string}, + content: IContent, ) { if (this.disabled) return; const cli = MatrixClientPeg.get(); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index c80b50c566..90ee8067bb 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -358,11 +358,11 @@ interface IOpts { stripReplyFallback?: boolean; returnString?: boolean; forComposerQuote?: boolean; - ref?: React.Ref; + ref?: React.Ref; } export interface IOptsReturnNode extends IOpts { - returnString: false; + returnString: false | undefined; } export interface IOptsReturnString extends IOpts { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 0f38c5fffc..128ca9e5e2 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1181,7 +1181,7 @@ export const Commands = [ ]; // build a map from names and aliases to the Command objects. -export const CommandMap = new Map(); +export const CommandMap = new Map(); Commands.forEach(cmd => { CommandMap.set(cmd.command, cmd); cmd.aliases.forEach(alias => { @@ -1189,15 +1189,15 @@ Commands.forEach(cmd => { }); }); -export function parseCommandString(input: string) { +export function parseCommandString(input: string): { cmd?: string, args?: string } { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); if (input[0] !== '/') return {}; // not a command const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/); - let cmd; - let args; + let cmd: string; + let args: string; if (bits) { cmd = bits[1].substring(1).toLowerCase(); args = bits[2]; @@ -1208,6 +1208,11 @@ export function parseCommandString(input: string) { return { cmd, args }; } +interface ICmd { + cmd?: Command; + args?: string; +} + /** * Process the given text for /commands and return a bound method to perform them. * @param {string} roomId The room in which the command was performed. @@ -1216,7 +1221,7 @@ export function parseCommandString(input: string) { * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ -export function getCommand(input: string) { +export function getCommand(input: string): ICmd { const { cmd, args } = parseCommandString(input); if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) { diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.tsx similarity index 80% rename from src/components/views/messages/TextualBody.js rename to src/components/views/messages/TextualBody.tsx index ffaaaada4d..cb9de013bf 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.tsx @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 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. @@ -16,134 +14,151 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { createRef, SyntheticEvent } from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; import highlight from 'highlight.js'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { MsgType } from "matrix-js-sdk/lib/@types/event"; + import * as HtmlUtils from '../../../HtmlUtils'; import { formatDate } from '../../../DateUtils'; -import * as sdk from '../../../index'; import Modal from '../../../Modal'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import * as ContextMenu from '../../structures/ContextMenu'; +import { toRightOf } from '../../structures/ContextMenu'; import SettingsStore from "../../../settings/SettingsStore"; import ReplyThread from "../elements/ReplyThread"; import { pillifyLinks, unmountPills } from '../../../utils/pillify'; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { isPermalinkHost } from "../../../utils/permalinks/Permalinks"; -import { toRightOf } from "../../structures/ContextMenu"; import { copyPlaintext } from "../../../utils/strings"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from "../../../stores/UIStore"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; +import { TileShape } from '../rooms/EventTile'; +import EditorStateTransfer from "../../../utils/EditorStateTransfer"; +import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; +import Spoiler from "../elements/Spoiler"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog"; +import EditMessageComposer from '../rooms/EditMessageComposer'; +import LinkPreviewWidget from '../rooms/LinkPreviewWidget'; + +interface IProps { + /* the MatrixEvent to show */ + mxEvent: MatrixEvent; + + /* a list of words to highlight */ + highlights?: string[]; + + /* link URL for the highlights */ + highlightLink?: string; + + /* should show URL previews for this event */ + showUrlPreview?: boolean; + + /* the shape of the tile, used */ + tileShape?: TileShape; + + editState?: EditorStateTransfer; + replacingEventId?: string; + + /* callback for when our widget has loaded */ + onHeightChanged(): void, +} + +interface IState { + // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. + links: string[]; + + // track whether the preview widget is hidden + widgetHidden: boolean; +} @replaceableComponent("views.messages.TextualBody") -export default class TextualBody extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, +export default class TextualBody extends React.Component { + private readonly contentRef = createRef(); - /* a list of words to highlight */ - highlights: PropTypes.array, - - /* link URL for the highlights */ - highlightLink: PropTypes.string, - - /* should show URL previews for this event */ - showUrlPreview: PropTypes.bool, - - /* callback for when our widget has loaded */ - onHeightChanged: PropTypes.func, - - /* the shape of the tile, used */ - tileShape: PropTypes.string, - }; + private unmounted = false; + private pills: Element[] = []; constructor(props) { super(props); - this._content = createRef(); - this.state = { - // the URLs (if any) to be previewed with a LinkPreviewWidget - // inside this TextualBody. links: [], - - // track whether the preview widget is hidden widgetHidden: false, }; } componentDidMount() { - this._unmounted = false; - this._pills = []; if (!this.props.editState) { - this._applyFormatting(); + this.applyFormatting(); } } - _applyFormatting() { + private applyFormatting(): void { const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); - this.activateSpoilers([this._content.current]); + this.activateSpoilers([this.contentRef.current]); // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. - pillifyLinks([this._content.current], this.props.mxEvent, this._pills); - HtmlUtils.linkifyElement(this._content.current); + pillifyLinks([this.contentRef.current], this.props.mxEvent, this.pills); + HtmlUtils.linkifyElement(this.contentRef.current); this.calculateUrlPreview(); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons - const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre"); + const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre"); if (pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. // This happens after the codeblock was edited. - if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue; + if (pres[i].parentElement.className == "mx_EventTile_pre_container") continue; // Add code element if it's missing since we depend on it if (pres[i].getElementsByTagName("code").length == 0) { - this._addCodeElement(pres[i]); + this.addCodeElement(pres[i]); } // Wrap a div around
 so that the copy button can be correctly positioned
                     // when the 
 overflows and is scrolled horizontally.
-                    const div = this._wrapInDiv(pres[i]);
-                    this._handleCodeBlockExpansion(pres[i]);
-                    this._addCodeExpansionButton(div, pres[i]);
-                    this._addCodeCopyButton(div);
+                    const div = this.wrapInDiv(pres[i]);
+                    this.handleCodeBlockExpansion(pres[i]);
+                    this.addCodeExpansionButton(div, pres[i]);
+                    this.addCodeCopyButton(div);
                     if (showLineNumbers) {
-                        this._addLineNumbers(pres[i]);
+                        this.addLineNumbers(pres[i]);
                     }
                 }
             }
             // Highlight code
-            const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
+            const codes = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("code");
             if (codes.length > 0) {
                 // Do this asynchronously: parsing code takes time and we don't
                 // need to block the DOM update on it.
                 setTimeout(() => {
-                    if (this._unmounted) return;
+                    if (this.unmounted) return;
                     for (let i = 0; i < codes.length; i++) {
                         // If the code already has the hljs class we want to skip this.
                         // This happens after the codeblock was edited.
                         if (codes[i].className.includes("hljs")) continue;
-                        this._highlightCode(codes[i]);
+                        this.highlightCode(codes[i]);
                     }
                 }, 10);
             }
         }
     }
 
-    _addCodeElement(pre) {
+    private addCodeElement(pre: HTMLPreElement): void {
         const code = document.createElement("code");
         code.append(...pre.childNodes);
         pre.appendChild(code);
     }
 
-    _addCodeExpansionButton(div, pre) {
+    private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
         // Calculate how many percent does the pre element take up.
         // If it's less than 30% we don't add the expansion button.
         const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
@@ -175,7 +190,7 @@ export default class TextualBody extends React.Component {
         div.appendChild(button);
     }
 
-    _addCodeCopyButton(div) {
+    private addCodeCopyButton(div: HTMLDivElement): void {
         const button = document.createElement("span");
         button.className = "mx_EventTile_button mx_EventTile_copyButton ";
 
@@ -185,11 +200,10 @@ export default class TextualBody extends React.Component {
         if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
 
         button.onclick = async () => {
-            const copyCode = button.parentNode.getElementsByTagName("code")[0];
+            const copyCode = button.parentElement.getElementsByTagName("code")[0];
             const successful = await copyPlaintext(copyCode.textContent);
 
             const buttonRect = button.getBoundingClientRect();
-            const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
             const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
                 ...toRightOf(buttonRect, 2),
                 message: successful ? _t('Copied!') : _t('Failed to copy'),
@@ -200,7 +214,7 @@ export default class TextualBody extends React.Component {
         div.appendChild(button);
     }
 
-    _wrapInDiv(pre) {
+    private wrapInDiv(pre: HTMLPreElement): HTMLDivElement {
         const div = document.createElement("div");
         div.className = "mx_EventTile_pre_container";
 
@@ -212,13 +226,13 @@ export default class TextualBody extends React.Component {
         return div;
     }
 
-    _handleCodeBlockExpansion(pre) {
+    private handleCodeBlockExpansion(pre: HTMLPreElement): void {
         if (!SettingsStore.getValue("expandCodeByDefault")) {
             pre.className = "mx_EventTile_collapsedCodeBlock";
         }
     }
 
-    _addLineNumbers(pre) {
+    private addLineNumbers(pre: HTMLPreElement): void {
         // Calculate number of lines in pre
         const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
         pre.innerHTML = '' + pre.innerHTML + '';
@@ -229,7 +243,7 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    _highlightCode(code) {
+    private highlightCode(code: HTMLElement): void {
         if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
             highlight.highlightBlock(code);
         } else {
@@ -249,14 +263,14 @@ export default class TextualBody extends React.Component {
             const stoppedEditing = prevProps.editState && !this.props.editState;
             const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
             if (messageWasEdited || stoppedEditing) {
-                this._applyFormatting();
+                this.applyFormatting();
             }
         }
     }
 
     componentWillUnmount() {
-        this._unmounted = true;
-        unmountPills(this._pills);
+        this.unmounted = true;
+        unmountPills(this.pills);
     }
 
     shouldComponentUpdate(nextProps, nextState) {
@@ -273,12 +287,12 @@ export default class TextualBody extends React.Component {
                 nextState.widgetHidden !== this.state.widgetHidden);
     }
 
-    calculateUrlPreview() {
+    private calculateUrlPreview(): void {
         //console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
 
         if (this.props.showUrlPreview) {
             // pass only the first child which is the event tile otherwise this recurses on edited events
-            let links = this.findLinks([this._content.current]);
+            let links = this.findLinks([this.contentRef.current]);
             if (links.length) {
                 // de-duplicate the links after stripping hashes as they don't affect the preview
                 // using a set here maintains the order
@@ -291,8 +305,8 @@ export default class TextualBody extends React.Component {
                 this.setState({ links });
 
                 // lazy-load the hidden state of the preview widget from localstorage
-                if (global.localStorage) {
-                    const hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
+                if (window.localStorage) {
+                    const hidden = !!window.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
                     this.setState({ widgetHidden: hidden });
                 }
             } else if (this.state.links.length) {
@@ -301,19 +315,15 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    activateSpoilers(nodes) {
+    private activateSpoilers(nodes: ArrayLike): void {
         let node = nodes[0];
         while (node) {
             if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
                 const spoilerContainer = document.createElement('span');
 
                 const reason = node.getAttribute("data-mx-spoiler");
-                const Spoiler = sdk.getComponent('elements.Spoiler');
                 node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
-                const spoiler = ;
+                const spoiler = ;
 
                 ReactDOM.render(spoiler, spoilerContainer);
                 node.parentNode.replaceChild(spoilerContainer, node);
@@ -322,15 +332,15 @@ export default class TextualBody extends React.Component {
             }
 
             if (node.childNodes && node.childNodes.length) {
-                this.activateSpoilers(node.childNodes);
+                this.activateSpoilers(node.childNodes as NodeListOf);
             }
 
-            node = node.nextSibling;
+            node = node.nextSibling as Element;
         }
     }
 
-    findLinks(nodes) {
-        let links = [];
+    private findLinks(nodes: ArrayLike): string[] {
+        let links: string[] = [];
 
         for (let i = 0; i < nodes.length; i++) {
             const node = nodes[i];
@@ -348,7 +358,7 @@ export default class TextualBody extends React.Component {
         return links;
     }
 
-    isLinkPreviewable(node) {
+    private isLinkPreviewable(node: Element): boolean {
         // don't try to preview relative links
         if (!node.getAttribute("href").startsWith("http://") &&
             !node.getAttribute("href").startsWith("https://")) {
@@ -381,7 +391,7 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    onCancelClick = event => {
+    private onCancelClick = (): void => {
         this.setState({ widgetHidden: true });
         // FIXME: persist this somewhere smarter than local storage
         if (global.localStorage) {
@@ -390,7 +400,7 @@ export default class TextualBody extends React.Component {
         this.forceUpdate();
     };
 
-    onEmoteSenderClick = event => {
+    private onEmoteSenderClick = (): void => {
         const mxEvent = this.props.mxEvent;
         dis.dispatch({
             action: Action.ComposerInsert,
@@ -398,7 +408,7 @@ export default class TextualBody extends React.Component {
         });
     };
 
-    getEventTileOps = () => ({
+    public getEventTileOps = () => ({
         isWidgetHidden: () => {
             return this.state.widgetHidden;
         },
@@ -411,7 +421,7 @@ export default class TextualBody extends React.Component {
         },
     });
 
-    onStarterLinkClick = (starterLink, ev) => {
+    private onStarterLinkClick = (starterLink: string, ev: SyntheticEvent): void => {
         ev.preventDefault();
         // We need to add on our scalar token to the starter link, but we may not have one!
         // In addition, we can't fetch one on click and then go to it immediately as that
@@ -431,7 +441,6 @@ export default class TextualBody extends React.Component {
         const scalarClient = integrationManager.getScalarClient();
         scalarClient.connect().then(() => {
             const completeUrl = scalarClient.getStarterLink(starterLink);
-            const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
             const integrationsUrl = integrationManager.uiUrl;
             Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
                 title: _t("Add an Integration"),
@@ -458,12 +467,11 @@ export default class TextualBody extends React.Component {
         });
     };
 
-    _openHistoryDialog = async () => {
-        const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
+    private openHistoryDialog = async (): Promise => {
         Modal.createDialog(MessageEditHistoryDialog, { mxEvent: this.props.mxEvent });
     };
 
-    _renderEditedMarker() {
+    private renderEditedMarker() {
         const date = this.props.mxEvent.replacingEventDate();
         const dateString = date && formatDate(date);
 
@@ -479,7 +487,7 @@ export default class TextualBody extends React.Component {
         return (
             
@@ -490,24 +498,25 @@ export default class TextualBody extends React.Component {
 
     render() {
         if (this.props.editState) {
-            const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
             return ;
         }
         const mxEvent = this.props.mxEvent;
         const content = mxEvent.getContent();
 
         // only strip reply if this is the original replying event, edits thereafter do not have the fallback
-        const stripReply = !mxEvent.replacingEvent() && ReplyThread.getParentEventId(mxEvent);
+        const stripReply = !mxEvent.replacingEvent() && !!ReplyThread.getParentEventId(mxEvent);
         let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
-            disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
+            disableBigEmoji: content.msgtype === MsgType.Emote
+                || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
             // Part of Replies fallback support
             stripReplyFallback: stripReply,
-            ref: this._content,
+            ref: this.contentRef,
+            returnString: false,
         });
         if (this.props.replacingEventId) {
             body = <>
                 {body}
-                {this._renderEditedMarker()}
+                {this.renderEditedMarker()}
             ;
         }
 
@@ -521,7 +530,6 @@ export default class TextualBody extends React.Component {
 
         let widgets;
         if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
-            const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
             widgets = this.state.links.map((link)=>{
                 return 
                         * 
@@ -549,7 +557,7 @@ export default class TextualBody extends React.Component {
                         { widgets }
                     
                 );
-            case "m.notice":
+            case MsgType.Notice:
                 return (
                     
                         { body }
diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.tsx
similarity index 72%
rename from src/components/views/messages/TextualEvent.js
rename to src/components/views/messages/TextualEvent.tsx
index 663f47dd2a..70f90a33e4 100644
--- a/src/components/views/messages/TextualEvent.js
+++ b/src/components/views/messages/TextualEvent.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2015 - 2021 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.
@@ -16,20 +15,20 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+
 import * as TextForEvent from "../../../TextForEvent";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
-@replaceableComponent("views.messages.TextualEvent")
-export default class TextualEvent extends React.Component {
-    static propTypes = {
-        /* the MatrixEvent to show */
-        mxEvent: PropTypes.object.isRequired,
-    };
+interface IProps {
+    mxEvent: MatrixEvent;
+}
 
+@replaceableComponent("views.messages.TextualEvent")
+export default class TextualEvent extends React.Component {
     render() {
         const text = TextForEvent.textForEvent(this.props.mxEvent, true);
-        if (text == null || text.length === 0) return null;
+        if (!text || (text as string).length === 0) return null;
         return (
             
{ text }
); diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 94a292afe7..d317aa409b 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -41,7 +41,7 @@ import { Key } from "../../../Keyboard"; import { EMOTICON_TO_EMOJI } from "../../../emoji"; import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands"; import Range from "../../../editor/range"; -import MessageComposerFormatBar from "./MessageComposerFormatBar"; +import MessageComposerFormatBar, { Formatting } from "./MessageComposerFormatBar"; import DocumentOffset from "../../../editor/offset"; import { IDiff } from "../../../editor/diff"; import AutocompleteWrapperModel from "../../../editor/autocomplete"; @@ -55,7 +55,7 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc const IS_MAC = navigator.platform.indexOf("Mac") !== -1; -function ctrlShortcutLabel(key) { +function ctrlShortcutLabel(key: string): string { return (IS_MAC ? "⌘" : "Ctrl") + "+" + key; } @@ -81,14 +81,6 @@ function selectionEquals(a: Partial, b: Selection): boolean { a.type === b.type; } -enum Formatting { - Bold = "bold", - Italics = "italics", - Strikethrough = "strikethrough", - Code = "code", - Quote = "quote", -} - interface IProps { model: EditorModel; room: Room; @@ -111,9 +103,9 @@ interface IState { @replaceableComponent("views.rooms.BasicMessageEditor") export default class BasicMessageEditor extends React.Component { - private editorRef = createRef(); + public readonly editorRef = createRef(); private autocompleteRef = createRef(); - private formatBarRef = createRef(); + private formatBarRef = createRef(); private modifiedFlag = false; private isIMEComposing = false; @@ -156,7 +148,7 @@ export default class BasicMessageEditor extends React.Component } } - private replaceEmoticon = (caretPosition: DocumentPosition) => { + private replaceEmoticon = (caretPosition: DocumentPosition): number => { const { model } = this.props; const range = model.startRange(caretPosition); // expand range max 8 characters backwards from caretPosition, @@ -188,7 +180,7 @@ export default class BasicMessageEditor extends React.Component } }; - private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff) => { + private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => { renderModel(this.editorRef.current, this.props.model); if (selection) { // set the caret/selection try { @@ -230,25 +222,25 @@ export default class BasicMessageEditor extends React.Component } }; - private showPlaceholder() { + private showPlaceholder(): void { // escape single quotes const placeholder = this.props.placeholder.replace(/'/g, '\\\''); this.editorRef.current.style.setProperty("--placeholder", `'${placeholder}'`); this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty"); } - private hidePlaceholder() { + private hidePlaceholder(): void { this.editorRef.current.classList.remove("mx_BasicMessageComposer_inputEmpty"); this.editorRef.current.style.removeProperty("--placeholder"); } - private onCompositionStart = () => { + private onCompositionStart = (): void => { this.isIMEComposing = true; // even if the model is empty, the composition text shouldn't be mixed with the placeholder this.hidePlaceholder(); }; - private onCompositionEnd = () => { + private onCompositionEnd = (): void => { this.isIMEComposing = false; // some browsers (Chrome) don't fire an input event after ending a composition, // so trigger a model update after the composition is done by calling the input handler. @@ -271,14 +263,14 @@ export default class BasicMessageEditor extends React.Component } }; - isComposing(event: React.KeyboardEvent) { + public isComposing(event: React.KeyboardEvent): boolean { // checking the event.isComposing flag just in case any browser out there // emits events related to the composition after compositionend // has been fired return !!(this.isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing)); } - private onCutCopy = (event: ClipboardEvent, type: string) => { + private onCutCopy = (event: ClipboardEvent, type: string): void => { const selection = document.getSelection(); const text = selection.toString(); if (text) { @@ -296,15 +288,15 @@ export default class BasicMessageEditor extends React.Component } }; - private onCopy = (event: ClipboardEvent) => { + private onCopy = (event: ClipboardEvent): void => { this.onCutCopy(event, "copy"); }; - private onCut = (event: ClipboardEvent) => { + private onCut = (event: ClipboardEvent): void => { this.onCutCopy(event, "cut"); }; - private onPaste = (event: ClipboardEvent) => { + private onPaste = (event: ClipboardEvent): boolean => { event.preventDefault(); // we always handle the paste ourselves if (this.props.onPaste && this.props.onPaste(event, this.props.model)) { // to prevent double handling, allow props.onPaste to skip internal onPaste @@ -328,7 +320,7 @@ export default class BasicMessageEditor extends React.Component replaceRangeAndMoveCaret(range, parts); }; - private onInput = (event: Partial) => { + private onInput = (event: Partial): void => { // ignore any input while doing IME compositions if (this.isIMEComposing) { return; @@ -339,7 +331,7 @@ export default class BasicMessageEditor extends React.Component this.props.model.update(text, event.inputType, caret); }; - private insertText(textToInsert: string, inputType = "insertText") { + private insertText(textToInsert: string, inputType = "insertText"): void { const sel = document.getSelection(); const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel); const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset); @@ -353,14 +345,14 @@ export default class BasicMessageEditor extends React.Component // we don't need to. But if the user is navigating the caret without input // we need to recalculate it, to be able to know where to insert content after // losing focus - private setLastCaretFromPosition(position: DocumentPosition) { + private setLastCaretFromPosition(position: DocumentPosition): void { const { model } = this.props; this._isCaretAtEnd = position.isAtEnd(model); this.lastCaret = position.asOffset(model); this.lastSelection = cloneSelection(document.getSelection()); } - private refreshLastCaretIfNeeded() { + private refreshLastCaretIfNeeded(): DocumentOffset { // XXX: needed when going up and down in editing messages ... not sure why yet // because the editors should stop doing this when when blurred ... // maybe it's on focus and the _editorRef isn't available yet or something. @@ -377,38 +369,38 @@ export default class BasicMessageEditor extends React.Component return this.lastCaret; } - clearUndoHistory() { + public clearUndoHistory(): void { this.historyManager.clear(); } - getCaret() { + public getCaret(): DocumentOffset { return this.lastCaret; } - isSelectionCollapsed() { + public isSelectionCollapsed(): boolean { return !this.lastSelection || this.lastSelection.isCollapsed; } - isCaretAtStart() { + public isCaretAtStart(): boolean { return this.getCaret().offset === 0; } - isCaretAtEnd() { + public isCaretAtEnd(): boolean { return this._isCaretAtEnd; } - private onBlur = () => { + private onBlur = (): void => { document.removeEventListener("selectionchange", this.onSelectionChange); }; - private onFocus = () => { + private onFocus = (): void => { document.addEventListener("selectionchange", this.onSelectionChange); // force to recalculate this.lastSelection = null; this.refreshLastCaretIfNeeded(); }; - private onSelectionChange = () => { + private onSelectionChange = (): void => { const { isEmpty } = this.props.model; this.refreshLastCaretIfNeeded(); @@ -427,7 +419,7 @@ export default class BasicMessageEditor extends React.Component } }; - private onKeyDown = (event: React.KeyboardEvent) => { + private onKeyDown = (event: React.KeyboardEvent): void => { const model = this.props.model; let handled = false; const action = getKeyBindingsManager().getMessageComposerAction(event); @@ -523,7 +515,7 @@ export default class BasicMessageEditor extends React.Component } }; - private async tabCompleteName() { + private async tabCompleteName(): Promise { try { await new Promise(resolve => this.setState({ showVisualBell: false }, resolve)); const { model } = this.props; @@ -557,27 +549,27 @@ export default class BasicMessageEditor extends React.Component } } - isModified() { + public isModified(): boolean { return this.modifiedFlag; } - private onAutoCompleteConfirm = (completion: ICompletion) => { + private onAutoCompleteConfirm = (completion: ICompletion): void => { this.modifiedFlag = true; this.props.model.autoComplete.onComponentConfirm(completion); }; - private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { + private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number): void => { this.modifiedFlag = true; this.props.model.autoComplete.onComponentSelectionChange(completion); this.setState({ completionIndex }); }; - private configureEmoticonAutoReplace = () => { + private configureEmoticonAutoReplace = (): void => { const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji'); this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null); }; - private configureShouldShowPillAvatar = () => { + private configureShouldShowPillAvatar = (): void => { const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); this.setState({ showPillAvatar }); }; @@ -611,8 +603,8 @@ export default class BasicMessageEditor extends React.Component this.editorRef.current.focus(); } - private getInitialCaretPosition() { - let caretPosition; + private getInitialCaretPosition(): DocumentPosition { + let caretPosition: DocumentPosition; if (this.props.initialCaret) { // if restoring state from a previous editor, // restore caret position from the state @@ -625,7 +617,7 @@ export default class BasicMessageEditor extends React.Component return caretPosition; } - private onFormatAction = (action: Formatting) => { + private onFormatAction = (action: Formatting): void => { const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); // trim the range as we want it to exclude leading/trailing spaces range.trim(); @@ -680,9 +672,9 @@ export default class BasicMessageEditor extends React.Component }); const shortcuts = { - bold: ctrlShortcutLabel("B"), - italics: ctrlShortcutLabel("I"), - quote: ctrlShortcutLabel(">"), + [Formatting.Bold]: ctrlShortcutLabel("B"), + [Formatting.Italics]: ctrlShortcutLabel("I"), + [Formatting.Quote]: ctrlShortcutLabel(">"), }; const { completionIndex } = this.state; @@ -714,11 +706,11 @@ export default class BasicMessageEditor extends React.Component ); } - focus() { + public focus(): void { this.editorRef.current.focus(); } - public insertMention(userId: string) { + public insertMention(userId: string): void { const { model } = this.props; const { partCreator } = model; const member = this.props.room.getMember(userId); @@ -736,7 +728,7 @@ export default class BasicMessageEditor extends React.Component this.focus(); } - public insertQuotedMessage(event: MatrixEvent) { + public insertQuotedMessage(event: MatrixEvent): void { const { model } = this.props; const { partCreator } = model; const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true }); @@ -751,7 +743,7 @@ export default class BasicMessageEditor extends React.Component this.focus(); } - public insertPlaintext(text: string) { + public insertPlaintext(text: string): void { const { model } = this.props; const { partCreator } = model; const caret = this.getCaret(); diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.tsx similarity index 70% rename from src/components/views/rooms/EditMessageComposer.js rename to src/components/views/rooms/EditMessageComposer.tsx index 0ab972b5f1..b06b57dced 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 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. @@ -14,37 +13,42 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import * as sdk from '../../../index'; + +import React, { createRef, KeyboardEvent } from 'react'; +import classNames from 'classnames'; +import { EventStatus, IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; + import { _t, _td } from '../../../languageHandler'; -import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { getCaretOffsetAndText } from '../../../editor/dom'; import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; import { findEditableEvent } from '../../../utils/EventUtils'; import { parseEvent } from '../../../editor/deserialize'; -import { CommandPartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; -import classNames from 'classnames'; -import { EventStatus } from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { CommandCategories, getCommand } from '../../../SlashCommands'; +import { Command, CommandCategories, getCommand } from '../../../SlashCommands'; import { Action } from "../../../dispatcher/actions"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SendHistoryManager from '../../../SendHistoryManager'; import Modal from '../../../Modal'; +import { MsgType } from 'matrix-js-sdk/src/@types/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import ErrorDialog from "../dialogs/ErrorDialog"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import { ActionPayload } from "../../../dispatcher/payloads"; +import AccessibleButton from '../elements/AccessibleButton'; -function _isReply(mxEvent) { +function eventIsReply(mxEvent: MatrixEvent): boolean { const relatesTo = mxEvent.getContent()["m.relates_to"]; - const isReply = !!(relatesTo && relatesTo["m.in_reply_to"]); - return isReply; + return !!(relatesTo && relatesTo["m.in_reply_to"]); } -function getHtmlReplyFallback(mxEvent) { +function getHtmlReplyFallback(mxEvent: MatrixEvent): string { const html = mxEvent.getContent().formatted_body; if (!html) { return ""; @@ -54,7 +58,7 @@ function getHtmlReplyFallback(mxEvent) { return (mxReply && mxReply.outerHTML) || ""; } -function getTextReplyFallback(mxEvent) { +function getTextReplyFallback(mxEvent: MatrixEvent): string { const body = mxEvent.getContent().body; const lines = body.split("\n").map(l => l.trim()); if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { @@ -63,12 +67,12 @@ function getTextReplyFallback(mxEvent) { return ""; } -function createEditContent(model, editedEvent) { +function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); } - const isReply = _isReply(editedEvent); + const isReply = eventIsReply(editedEvent); let plainPrefix = ""; let htmlPrefix = ""; @@ -79,11 +83,11 @@ function createEditContent(model, editedEvent) { const body = textSerialize(model); - const newContent = { - "msgtype": isEmote ? "m.emote" : "m.text", + const newContent: IContent = { + "msgtype": isEmote ? MsgType.Emote : MsgType.Text, "body": body, }; - const contentBody = { + const contentBody: IContent = { msgtype: newContent.msgtype, body: `${plainPrefix} * ${body}`, }; @@ -105,55 +109,59 @@ function createEditContent(model, editedEvent) { }, contentBody); } +interface IProps { + editState: EditorStateTransfer; + className?: string; +} + +interface IState { + saveDisabled: boolean; +} + @replaceableComponent("views.rooms.EditMessageComposer") -export default class EditMessageComposer extends React.Component { - static propTypes = { - // the message event being edited - editState: PropTypes.instanceOf(EditorStateTransfer).isRequired, - }; - +export default class EditMessageComposer extends React.Component { static contextType = MatrixClientContext; + context: React.ContextType; - constructor(props, context) { + private readonly editorRef = createRef(); + private readonly dispatcherRef: string; + private model: EditorModel = null; + + constructor(props: IProps, context: React.ContextType) { super(props, context); - this.model = null; - this._editorRef = null; this.state = { saveDisabled: true, }; - this._createEditorModel(); - window.addEventListener("beforeunload", this._saveStoredEditorState); + + this.createEditorModel(); + window.addEventListener("beforeunload", this.saveStoredEditorState); this.dispatcherRef = dis.register(this.onAction); } - _setEditorRef = ref => { - this._editorRef = ref; - }; - - _getRoom() { + private getRoom(): Room { return this.context.getRoom(this.props.editState.getEvent().getRoomId()); } - _onKeyDown = (event) => { + private onKeyDown = (event: KeyboardEvent): void => { // ignore any keypress while doing IME compositions - if (this._editorRef.isComposing(event)) { + if (this.editorRef.current?.isComposing(event)) { return; } const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.Send: - this._sendEdit(); + this.sendEdit(); event.preventDefault(); break; case MessageComposerAction.CancelEditing: - this._cancelEdit(); + this.cancelEdit(); break; case MessageComposerAction.EditPrevMessage: { - if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) { + if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) { return; } - const previousEvent = findEditableEvent(this._getRoom(), false, + const previousEvent = findEditableEvent(this.getRoom(), false, this.props.editState.getEvent().getId()); if (previousEvent) { dis.dispatch({ action: 'edit_event', event: previousEvent }); @@ -162,14 +170,14 @@ export default class EditMessageComposer extends React.Component { break; } case MessageComposerAction.EditNextMessage: { - if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) { + if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) { return; } - const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId()); + const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId()); if (nextEvent) { dis.dispatch({ action: 'edit_event', event: nextEvent }); } else { - this._clearStoredEditorState(); + this.clearStoredEditorState(); dis.dispatch({ action: 'edit_event', event: null }); dis.fire(Action.FocusComposer); } @@ -177,32 +185,32 @@ export default class EditMessageComposer extends React.Component { break; } } + }; + + private get editorRoomKey(): string { + return `mx_edit_room_${this.getRoom().roomId}`; } - get _editorRoomKey() { - return `mx_edit_room_${this._getRoom().roomId}`; - } - - get _editorStateKey() { + private get editorStateKey(): string { return `mx_edit_state_${this.props.editState.getEvent().getId()}`; } - _cancelEdit = () => { - this._clearStoredEditorState(); + private cancelEdit = (): void => { + this.clearStoredEditorState(); dis.dispatch({ action: "edit_event", event: null }); dis.fire(Action.FocusComposer); + }; + + private get shouldSaveStoredEditorState(): boolean { + return localStorage.getItem(this.editorRoomKey) !== null; } - get _shouldSaveStoredEditorState() { - return localStorage.getItem(this._editorRoomKey) !== null; - } - - _restoreStoredEditorState(partCreator) { - const json = localStorage.getItem(this._editorStateKey); + private restoreStoredEditorState(partCreator: PartCreator): Part[] { + const json = localStorage.getItem(this.editorStateKey); if (json) { try { const { parts: serializedParts } = JSON.parse(json); - const parts = serializedParts.map(p => partCreator.deserializePart(p)); + const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p)); return parts; } catch (e) { console.error("Error parsing editing state: ", e); @@ -210,25 +218,25 @@ export default class EditMessageComposer extends React.Component { } } - _clearStoredEditorState() { - localStorage.removeItem(this._editorRoomKey); - localStorage.removeItem(this._editorStateKey); + private clearStoredEditorState(): void { + localStorage.removeItem(this.editorRoomKey); + localStorage.removeItem(this.editorStateKey); } - _clearPreviousEdit() { - if (localStorage.getItem(this._editorRoomKey)) { - localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`); + private clearPreviousEdit(): void { + if (localStorage.getItem(this.editorRoomKey)) { + localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this.editorRoomKey)}`); } } - _saveStoredEditorState() { + private saveStoredEditorState(): void { const item = SendHistoryManager.createItem(this.model); - this._clearPreviousEdit(); - localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId()); - localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + this.clearPreviousEdit(); + localStorage.setItem(this.editorRoomKey, this.props.editState.getEvent().getId()); + localStorage.setItem(this.editorStateKey, JSON.stringify(item)); } - _isSlashCommand() { + private isSlashCommand(): boolean { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { @@ -244,10 +252,10 @@ export default class EditMessageComposer extends React.Component { return false; } - _isContentModified(newContent) { + private isContentModified(newContent: IContent): boolean { // if nothing has changed then bail const oldContent = this.props.editState.getEvent().getContent(); - if (!this._editorRef.isModified() || + if (!this.editorRef.current?.isModified() || (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] && oldContent["format"] === newContent["format"] && oldContent["formatted_body"] === newContent["formatted_body"])) { @@ -256,7 +264,7 @@ export default class EditMessageComposer extends React.Component { return true; } - _getSlashCommand() { + private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -268,7 +276,7 @@ export default class EditMessageComposer extends React.Component { return [cmd, args, commandText]; } - async _runSlashCommand(cmd, args, roomId) { + private async runSlashCommand(cmd: Command, args: string, roomId: string): Promise { const result = cmd.run(roomId, args); let messageContent; let error = result.error; @@ -285,7 +293,6 @@ export default class EditMessageComposer extends React.Component { } if (error) { console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // assume the error is a server error when the command is async const isServerError = !!result.promise; const title = isServerError ? _td("Server error") : _td("Command error"); @@ -309,7 +316,7 @@ export default class EditMessageComposer extends React.Component { } } - _sendEdit = async () => { + private sendEdit = async (): Promise => { const startTime = CountlyAnalytics.getTimestamp(); const editedEvent = this.props.editState.getEvent(); const editContent = createEditContent(this.model, editedEvent); @@ -318,20 +325,19 @@ export default class EditMessageComposer extends React.Component { let shouldSend = true; // If content is modified then send an updated event into the room - if (this._isContentModified(newContent)) { + if (this.isContentModified(newContent)) { const roomId = editedEvent.getRoomId(); - if (!containsEmote(this.model) && this._isSlashCommand()) { - const [cmd, args, commandText] = this._getSlashCommand(); + if (!containsEmote(this.model) && this.isSlashCommand()) { + const [cmd, args, commandText] = this.getSlashCommand(); if (cmd) { if (cmd.category === CommandCategories.messages) { - editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId); + editContent["m.new_content"] = await this.runSlashCommand(cmd, args, roomId); } else { - this._runSlashCommand(cmd, args, roomId); + this.runSlashCommand(cmd, args, roomId); shouldSend = false; } } else { // ask the user if their unknown command should be sent as a message - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), description:
@@ -358,9 +364,9 @@ export default class EditMessageComposer extends React.Component { } } if (shouldSend) { - this._cancelPreviousPendingEdit(); + this.cancelPreviousPendingEdit(); const prom = this.context.sendMessage(roomId, editContent); - this._clearStoredEditorState(); + this.clearStoredEditorState(); dis.dispatch({ action: "message_sent" }); CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); } @@ -371,7 +377,7 @@ export default class EditMessageComposer extends React.Component { dis.fire(Action.FocusComposer); }; - _cancelPreviousPendingEdit() { + private cancelPreviousPendingEdit(): void { const originalEvent = this.props.editState.getEvent(); const previousEdit = originalEvent.replacingEvent(); if (previousEdit && ( @@ -389,23 +395,23 @@ export default class EditMessageComposer extends React.Component { const sel = document.getSelection(); let caret; if (sel.focusNode) { - caret = getCaretOffsetAndText(this._editorRef, sel).caret; + caret = getCaretOffsetAndText(this.editorRef.current, sel).caret; } const parts = this.model.serializeParts(); // if caret is undefined because for some reason there isn't a valid selection, // then when mounting the editor again with the same editor state, // it will set the cursor at the end. this.props.editState.setEditorState(caret, parts); - window.removeEventListener("beforeunload", this._saveStoredEditorState); - if (this._shouldSaveStoredEditorState) { - this._saveStoredEditorState(); + window.removeEventListener("beforeunload", this.saveStoredEditorState); + if (this.shouldSaveStoredEditorState) { + this.saveStoredEditorState(); } dis.unregister(this.dispatcherRef); } - _createEditorModel() { + private createEditorModel(): void { const { editState } = this.props; - const room = this._getRoom(); + const room = this.getRoom(); const partCreator = new CommandPartCreator(room, this.context); let parts; if (editState.hasEditorState()) { @@ -414,13 +420,13 @@ export default class EditMessageComposer extends React.Component { parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); } else { //otherwise, either restore serialized parts from localStorage or parse the body of the event - parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); + parts = this.restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); } this.model = new EditorModel(parts, partCreator); - this._saveStoredEditorState(); + this.saveStoredEditorState(); } - _getInitialCaretPosition() { + private getInitialCaretPosition(): CaretPosition { const { editState } = this.props; let caretPosition; if (editState.hasEditorState() && editState.getCaret()) { @@ -435,8 +441,8 @@ export default class EditMessageComposer extends React.Component { return caretPosition; } - _onChange = () => { - if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) { + private onChange = (): void => { + if (!this.state.saveDisabled || !this.editorRef.current?.isModified()) { return; } @@ -445,33 +451,34 @@ export default class EditMessageComposer extends React.Component { }); }; - onAction = payload => { - if (payload.action === "edit_composer_insert" && this._editorRef) { + private onAction = (payload: ActionPayload) => { + if (payload.action === "edit_composer_insert" && this.editorRef.current) { if (payload.userId) { - this._editorRef.insertMention(payload.userId); + this.editorRef.current?.insertMention(payload.userId); } else if (payload.event) { - this._editorRef.insertQuotedMessage(payload.event); + this.editorRef.current?.insertQuotedMessage(payload.event); } else if (payload.text) { - this._editorRef.insertPlaintext(payload.text); + this.editorRef.current?.insertPlaintext(payload.text); } } }; render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return (
+ return (
- {_t("Cancel")} - - {_t("Save")} + + { _t("Cancel") } + + + { _t("Save") }
); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index db57f98025..7d61ba5ec6 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -43,6 +43,7 @@ import { E2EStatus } from '../../../utils/ShieldUtils'; import SendMessageComposer from "./SendMessageComposer"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; +import EditorModel from "../../../editor/model"; interface IComposerAvatarProps { me: object; @@ -318,14 +319,14 @@ export default class MessageComposer extends React.Component { } }; - addEmoji(emoji: string) { + private addEmoji(emoji: string) { dis.dispatch({ action: Action.ComposerInsert, text: emoji, }); } - sendMessage = async () => { + private sendMessage = async () => { if (this.state.haveRecording && this.voiceRecordingButton) { // There shouldn't be any text message to send when a voice recording is active, so // just send out the voice recording. @@ -333,11 +334,10 @@ export default class MessageComposer extends React.Component { return; } - // XXX: Private function access - this.messageComposerInput._sendMessage(); + this.messageComposerInput.sendMessage(); }; - onChange = (model) => { + private onChange = (model: EditorModel) => { this.setState({ isComposerEmpty: model.isEmpty, }); diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.tsx similarity index 55% rename from src/components/views/rooms/MessageComposerFormatBar.js rename to src/components/views/rooms/MessageComposerFormatBar.tsx index c31538c6cd..75bca8aac7 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 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. @@ -14,21 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +import React, { createRef } from 'react'; import classNames from 'classnames'; + +import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.rooms.MessageComposerFormatBar") -export default class MessageComposerFormatBar extends React.PureComponent { - static propTypes = { - onAction: PropTypes.func.isRequired, - shortcuts: PropTypes.object.isRequired, - }; +export enum Formatting { + Bold = "bold", + Italics = "italics", + Strikethrough = "strikethrough", + Code = "code", + Quote = "quote", +} - constructor(props) { +interface IProps { + shortcuts: Partial>; + onAction(action: Formatting): void; +} + +interface IState { + visible: boolean; +} + +@replaceableComponent("views.rooms.MessageComposerFormatBar") +export default class MessageComposerFormatBar extends React.PureComponent { + private readonly formatBarRef = createRef(); + + constructor(props: IProps) { super(props); this.state = { visible: false }; } @@ -37,49 +51,53 @@ export default class MessageComposerFormatBar extends React.PureComponent { const classes = classNames("mx_MessageComposerFormatBar", { "mx_MessageComposerFormatBar_shown": this.state.visible, }); - return (
this._formatBarRef = ref}> - this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} /> - this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} /> - this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} /> - this.props.onAction("code")} icon="Code" visible={this.state.visible} /> - this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} /> + return (
+ this.props.onAction(Formatting.Bold)} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} /> + this.props.onAction(Formatting.Italics)} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} /> + this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} /> + this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} /> + this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
); } - showAt(selectionRect) { + public showAt(selectionRect: DOMRect): void { + if (!this.formatBarRef.current) return; + this.setState({ visible: true }); - const parentRect = this._formatBarRef.parentElement.getBoundingClientRect(); - this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`; + const parentRect = this.formatBarRef.current.parentElement.getBoundingClientRect(); + this.formatBarRef.current.style.left = `${selectionRect.left - parentRect.left}px`; // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. - this._formatBarRef.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`; + this.formatBarRef.current.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`; } - hide() { + public hide(): void { this.setState({ visible: false }); } } -class FormatButton extends React.PureComponent { - static propTypes = { - label: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - icon: PropTypes.string.isRequired, - shortcut: PropTypes.string, - visible: PropTypes.bool, - }; +interface IFormatButtonProps { + label: string; + icon: string; + shortcut?: string; + visible?: boolean; + onClick(): void; +} +class FormatButton extends React.PureComponent { render() { const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; let shortcut; if (this.props.shortcut) { - shortcut =
{this.props.shortcut}
; + shortcut =
+ { this.props.shortcut } +
; } const tooltip =
- {this.props.label} + { this.props.label }
- {shortcut} + { shortcut }
; diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.tsx similarity index 78% rename from src/components/views/rooms/SendMessageComposer.js rename to src/components/views/rooms/SendMessageComposer.tsx index cc819e05e1..2c670f6f64 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 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. @@ -14,8 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; + +import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react'; +import EMOJI_REGEX from 'emojibase-regex'; +import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; + import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { @@ -27,13 +29,12 @@ import { startsWith, stripPrefix, } from '../../../editor/serialize'; -import { CommandPartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; import { findEditableEvent } from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import { CommandCategories, getCommand } from '../../../SlashCommands'; -import * as sdk from '../../../index'; +import { Command, CommandCategories, getCommand } from '../../../SlashCommands'; import Modal from '../../../Modal'; import { _t, _td } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; @@ -44,12 +45,20 @@ import { containsEmoji } from "../../../effects/utils"; import { CHAT_EFFECTS } from '../../../effects'; import CountlyAnalytics from "../../../CountlyAnalytics"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import EMOJI_REGEX from 'emojibase-regex'; import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from '../../../settings/SettingsStore'; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { Room } from 'matrix-js-sdk/src/models/room'; +import ErrorDialog from "../dialogs/ErrorDialog"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import { ActionPayload } from "../../../dispatcher/payloads"; -function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { +function addReplyToMessageContent( + content: IContent, + repliedToEvent: MatrixEvent, + permalinkCreator: RoomPermalinkCreator, +): void { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); Object.assign(content, replyContent); @@ -65,7 +74,11 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } // exported for tests -export function createMessageContent(model, permalinkCreator, replyToEvent) { +export function createMessageContent( + model: EditorModel, + permalinkCreator: RoomPermalinkCreator, + replyToEvent: MatrixEvent, +): IContent { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -76,7 +89,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) { model = unescapeMessage(model); const body = textSerialize(model); - const content = { + const content: IContent = { msgtype: isEmote ? "m.emote" : "m.text", body: body, }; @@ -94,7 +107,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) { } // exported for tests -export function isQuickReaction(model) { +export function isQuickReaction(model: EditorModel): boolean { const parts = model.parts; if (parts.length == 0) return false; const text = textSerialize(model); @@ -111,46 +124,47 @@ export function isQuickReaction(model) { return false; } +interface IProps { + room: Room; + placeholder?: string; + permalinkCreator: RoomPermalinkCreator; + replyToEvent?: MatrixEvent; + disabled?: boolean; + onChange?(model: EditorModel): void; +} + @replaceableComponent("views.rooms.SendMessageComposer") -export default class SendMessageComposer extends React.Component { - static propTypes = { - room: PropTypes.object.isRequired, - placeholder: PropTypes.string, - permalinkCreator: PropTypes.object.isRequired, - replyToEvent: PropTypes.object, - onChange: PropTypes.func, - disabled: PropTypes.bool, - }; - +export default class SendMessageComposer extends React.Component { static contextType = MatrixClientContext; + context: React.ContextType; - constructor(props, context) { + private readonly prepareToEncrypt?: RateLimitedFunc; + private readonly editorRef = createRef(); + private model: EditorModel = null; + private currentlyComposedEditorState: SerializedPart[] = null; + private dispatcherRef: string; + private sendHistoryManager: SendHistoryManager; + + constructor(props: IProps, context: React.ContextType) { super(props, context); - this.model = null; - this._editorRef = null; - this.currentlyComposedEditorState = null; - if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) { - this._prepareToEncrypt = new RateLimitedFunc(() => { + if (context.isCryptoEnabled() && context.isRoomEncrypted(this.props.room.roomId)) { + this.prepareToEncrypt = new RateLimitedFunc(() => { this.context.prepareToEncrypt(this.props.room); }, 60000); } - window.addEventListener("beforeunload", this._saveStoredEditorState); + window.addEventListener("beforeunload", this.saveStoredEditorState); } - _setEditorRef = ref => { - this._editorRef = ref; - }; - - _onKeyDown = (event) => { + private onKeyDown = (event: KeyboardEvent): void => { // ignore any keypress while doing IME compositions - if (this._editorRef.isComposing(event)) { + if (this.editorRef.current?.isComposing(event)) { return; } const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.Send: - this._sendMessage(); + this.sendMessage(); event.preventDefault(); break; case MessageComposerAction.SelectPrevSendHistory: @@ -165,7 +179,7 @@ export default class SendMessageComposer extends React.Component { } case MessageComposerAction.EditPrevMessage: // selection must be collapsed and caret at start - if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) { + if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) { const editEvent = findEditableEvent(this.props.room, false); if (editEvent) { // We're selecting history, so prevent the key event from doing anything else @@ -184,16 +198,16 @@ export default class SendMessageComposer extends React.Component { }); break; default: - if (this._prepareToEncrypt) { + if (this.prepareToEncrypt) { // This needs to be last! - this._prepareToEncrypt(); + this.prepareToEncrypt(); } } }; // we keep sent messages/commands in a separate history (separate from undo history) // so you can alt+up/down in them - selectSendHistory(up) { + private selectSendHistory(up: boolean): void { const delta = up ? -1 : 1; // True if we are not currently selecting history, but composing a message if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { @@ -215,11 +229,11 @@ export default class SendMessageComposer extends React.Component { }); if (parts) { this.model.reset(parts); - this._editorRef.focus(); + this.editorRef.current?.focus(); } } - _isSlashCommand() { + private isSlashCommand(): boolean { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { @@ -237,7 +251,7 @@ export default class SendMessageComposer extends React.Component { return false; } - _sendQuickReaction() { + private sendQuickReaction(): void { const timeline = this.props.room.getLiveTimeline(); const events = timeline.getEvents(); const reaction = this.model.parts[1].text; @@ -272,7 +286,7 @@ export default class SendMessageComposer extends React.Component { } } - _getSlashCommand() { + private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -284,7 +298,7 @@ export default class SendMessageComposer extends React.Component { return [cmd, args, commandText]; } - async _runSlashCommand(cmd, args) { + private async runSlashCommand(cmd: Command, args: string): Promise { const result = cmd.run(this.props.room.roomId, args); let messageContent; let error = result.error; @@ -302,7 +316,6 @@ export default class SendMessageComposer extends React.Component { } if (error) { console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // assume the error is a server error when the command is async const isServerError = !!result.promise; const title = isServerError ? _td("Server error") : _td("Command error"); @@ -326,7 +339,7 @@ export default class SendMessageComposer extends React.Component { } } - async _sendMessage() { + public async sendMessage(): Promise { if (this.model.isEmpty) { return; } @@ -335,21 +348,20 @@ export default class SendMessageComposer extends React.Component { let shouldSend = true; let content; - if (!containsEmote(this.model) && this._isSlashCommand()) { - const [cmd, args, commandText] = this._getSlashCommand(); + if (!containsEmote(this.model) && this.isSlashCommand()) { + const [cmd, args, commandText] = this.getSlashCommand(); if (cmd) { if (cmd.category === CommandCategories.messages) { - content = await this._runSlashCommand(cmd, args); + content = await this.runSlashCommand(cmd, args); if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); } } else { - this._runSlashCommand(cmd, args); + this.runSlashCommand(cmd, args); shouldSend = false; } } else { // ask the user if their unknown command should be sent as a message - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), description:
@@ -378,7 +390,7 @@ export default class SendMessageComposer extends React.Component { if (isQuickReaction(this.model)) { shouldSend = false; - this._sendQuickReaction(); + this.sendQuickReaction(); } if (shouldSend) { @@ -411,9 +423,9 @@ export default class SendMessageComposer extends React.Component { this.sendHistoryManager.save(this.model, replyToEvent); // clear composer this.model.reset([]); - this._editorRef.clearUndoHistory(); - this._editorRef.focus(); - this._clearStoredEditorState(); + this.editorRef.current?.clearUndoHistory(); + this.editorRef.current?.focus(); + this.clearStoredEditorState(); if (SettingsStore.getValue("scrollToBottomOnMessageSent")) { dis.dispatch({ action: "scroll_to_bottom" }); } @@ -421,33 +433,33 @@ export default class SendMessageComposer extends React.Component { componentWillUnmount() { dis.unregister(this.dispatcherRef); - window.removeEventListener("beforeunload", this._saveStoredEditorState); - this._saveStoredEditorState(); + window.removeEventListener("beforeunload", this.saveStoredEditorState); + this.saveStoredEditorState(); } // TODO: [REACT-WARNING] Move this to constructor UNSAFE_componentWillMount() { // eslint-disable-line camelcase const partCreator = new CommandPartCreator(this.props.room, this.context); - const parts = this._restoreStoredEditorState(partCreator) || []; + const parts = this.restoreStoredEditorState(partCreator) || []; this.model = new EditorModel(parts, partCreator); this.dispatcherRef = dis.register(this.onAction); this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_'); } - get _editorStateKey() { + private get editorStateKey() { return `mx_cider_state_${this.props.room.roomId}`; } - _clearStoredEditorState() { - localStorage.removeItem(this._editorStateKey); + private clearStoredEditorState(): void { + localStorage.removeItem(this.editorStateKey); } - _restoreStoredEditorState(partCreator) { - const json = localStorage.getItem(this._editorStateKey); + private restoreStoredEditorState(partCreator: PartCreator): Part[] { + const json = localStorage.getItem(this.editorStateKey); if (json) { try { const { parts: serializedParts, replyEventId } = JSON.parse(json); - const parts = serializedParts.map(p => partCreator.deserializePart(p)); + const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p)); if (replyEventId) { dis.dispatch({ action: 'reply_to_event', @@ -462,20 +474,20 @@ export default class SendMessageComposer extends React.Component { } // should save state when editor has contents or reply is open - _shouldSaveStoredEditorState = () => { - return !this.model.isEmpty || this.props.replyToEvent; - } + private shouldSaveStoredEditorState = (): boolean => { + return !this.model.isEmpty || !!this.props.replyToEvent; + }; - _saveStoredEditorState = () => { - if (this._shouldSaveStoredEditorState()) { + private saveStoredEditorState = (): void => { + if (this.shouldSaveStoredEditorState()) { const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent); - localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + localStorage.setItem(this.editorStateKey, JSON.stringify(item)); } else { - this._clearStoredEditorState(); + this.clearStoredEditorState(); } - } + }; - onAction = (payload) => { + private onAction = (payload: ActionPayload): void => { // don't let the user into the composer if it is disabled - all of these branches lead // to the cursor being in the composer if (this.props.disabled) return; @@ -483,21 +495,21 @@ export default class SendMessageComposer extends React.Component { switch (payload.action) { case 'reply_to_event': case Action.FocusComposer: - this._editorRef && this._editorRef.focus(); + this.editorRef.current?.focus(); break; case "send_composer_insert": if (payload.userId) { - this._editorRef && this._editorRef.insertMention(payload.userId); + this.editorRef.current?.insertMention(payload.userId); } else if (payload.event) { - this._editorRef && this._editorRef.insertQuotedMessage(payload.event); + this.editorRef.current?.insertQuotedMessage(payload.event); } else if (payload.text) { - this._editorRef && this._editorRef.insertPlaintext(payload.text); + this.editorRef.current?.insertPlaintext(payload.text); } break; } }; - _onPaste = (event) => { + private onPaste = (event: ClipboardEvent): boolean => { const { clipboardData } = event; // Prioritize text on the clipboard over files as Office on macOS puts a bitmap // in the clipboard as well as the content being copied. @@ -511,23 +523,27 @@ export default class SendMessageComposer extends React.Component { ); return true; // to skip internal onPaste handler } - } + }; - onChange = () => { + private onChange = (): void => { if (this.props.onChange) this.props.onChange(this.model); - } + }; + + private focusComposer = (): void => { + this.editorRef.current?.focus(); + }; render() { return ( -
+
diff --git a/src/editor/model.ts b/src/editor/model.ts index 1e8498a69e..da1c2f47f5 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -70,7 +70,7 @@ export default class EditorModel { * on the model that can span multiple parts. Also see `startRange()`. * @param {TransformCallback} transformCallback */ - setTransformCallback(transformCallback: TransformCallback) { + public setTransformCallback(transformCallback: TransformCallback): void { this.transformCallback = transformCallback; } @@ -78,23 +78,23 @@ export default class EditorModel { * Set a callback for rerendering the model after it has been updated. * @param {ModelCallback} updateCallback */ - setUpdateCallback(updateCallback: UpdateCallback) { + public setUpdateCallback(updateCallback: UpdateCallback): void { this.updateCallback = updateCallback; } - get partCreator() { + public get partCreator(): PartCreator { return this._partCreator; } - get isEmpty() { + public get isEmpty(): boolean { return this._parts.reduce((len, part) => len + part.text.length, 0) === 0; } - clone() { + public clone(): EditorModel { return new EditorModel(this._parts, this._partCreator, this.updateCallback); } - private insertPart(index: number, part: Part) { + private insertPart(index: number, part: Part): void { this._parts.splice(index, 0, part); if (this.activePartIdx >= index) { ++this.activePartIdx; @@ -104,7 +104,7 @@ export default class EditorModel { } } - private removePart(index: number) { + private removePart(index: number): void { this._parts.splice(index, 1); if (index === this.activePartIdx) { this.activePartIdx = null; @@ -118,22 +118,22 @@ export default class EditorModel { } } - private replacePart(index: number, part: Part) { + private replacePart(index: number, part: Part): void { this._parts.splice(index, 1, part); } - get parts() { + public get parts(): Part[] { return this._parts; } - get autoComplete() { + public get autoComplete(): AutocompleteWrapperModel { if (this.activePartIdx === this.autoCompletePartIdx) { return this._autoComplete; } return null; } - getPositionAtEnd() { + public getPositionAtEnd(): DocumentPosition { if (this._parts.length) { const index = this._parts.length - 1; const part = this._parts[index]; @@ -144,11 +144,11 @@ export default class EditorModel { } } - serializeParts() { + public serializeParts(): SerializedPart[] { return this._parts.map(p => p.serialize()); } - private diff(newValue: string, inputType: string, caret: DocumentOffset) { + private diff(newValue: string, inputType: string, caret: DocumentOffset): IDiff { const previousValue = this.parts.reduce((text, p) => text + p.text, ""); // can't use caret position with drag and drop if (inputType === "deleteByDrag") { @@ -158,7 +158,7 @@ export default class EditorModel { } } - reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string) { + public reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string): void { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); if (!caret) { caret = this.getPositionAtEnd(); @@ -180,7 +180,7 @@ export default class EditorModel { * @param {DocumentPosition} position the position to start inserting at * @return {Number} the amount of characters added */ - insert(parts: Part[], position: IPosition) { + public insert(parts: Part[], position: IPosition): number { const insertIndex = this.splitAt(position); let newTextLength = 0; for (let i = 0; i < parts.length; ++i) { @@ -191,7 +191,7 @@ export default class EditorModel { return newTextLength; } - update(newValue: string, inputType: string, caret: DocumentOffset) { + public update(newValue: string, inputType: string, caret: DocumentOffset): Promise { const diff = this.diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; @@ -220,7 +220,7 @@ export default class EditorModel { return Number.isFinite(result) ? result as number : 0; } - private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean) { + private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean): Promise { const { index } = pos; const part = this._parts[index]; if (part) { @@ -250,7 +250,7 @@ export default class EditorModel { return Promise.resolve(); } - private onAutoComplete = ({ replaceParts, close }: ICallback) => { + private onAutoComplete = ({ replaceParts, close }: ICallback): void => { let pos; if (replaceParts) { this._parts.splice(this.autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts); @@ -270,7 +270,7 @@ export default class EditorModel { this.updateCallback(pos); }; - private mergeAdjacentParts() { + private mergeAdjacentParts(): void { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -294,7 +294,7 @@ export default class EditorModel { * @return {Number} how many characters before pos were also removed, * usually because of non-editable parts that can only be removed in their entirety. */ - removeText(pos: IPosition, len: number) { + public removeText(pos: IPosition, len: number): number { let { index, offset } = pos; let removedOffsetDecrease = 0; while (len > 0) { @@ -329,7 +329,7 @@ export default class EditorModel { } // return part index where insertion will insert between at offset - private splitAt(pos: IPosition) { + private splitAt(pos: IPosition): number { if (pos.index === -1) { return 0; } @@ -356,7 +356,7 @@ export default class EditorModel { * @return {Number} how far from position (in characters) the insertion ended. * This can be more than the length of `str` when crossing non-editable parts, which are skipped. */ - private addText(pos: IPosition, str: string, inputType: string) { + private addText(pos: IPosition, str: string, inputType: string): number { let { index } = pos; const { offset } = pos; let addLen = str.length; @@ -390,7 +390,7 @@ export default class EditorModel { return addLen; } - positionForOffset(totalOffset: number, atPartEnd = false) { + public positionForOffset(totalOffset: number, atPartEnd = false): DocumentPosition { let currentOffset = 0; const index = this._parts.findIndex(part => { const partLen = part.text.length; @@ -416,11 +416,11 @@ export default class EditorModel { * @param {DocumentPosition?} positionB the other boundary of the range, optional * @return {Range} */ - startRange(positionA: DocumentPosition, positionB = positionA) { + public startRange(positionA: DocumentPosition, positionB = positionA): Range { return new Range(this, positionA, positionB); } - replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]) { + public replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]): void { // convert end position to offset, so it is independent of how the document is split into parts // which we'll change when splitting up at the start position const endOffset = endPosition.asOffset(this); @@ -445,9 +445,9 @@ export default class EditorModel { * @param {ManualTransformCallback} callback to run the transformations in * @return {Promise} a promise when auto-complete (if applicable) is done updating */ - transform(callback: ManualTransformCallback) { + public transform(callback: ManualTransformCallback): Promise { const pos = callback(); - let acPromise = null; + let acPromise: Promise = null; if (!(pos instanceof Range)) { acPromise = this.setActivePart(pos, true); } else { diff --git a/src/editor/parts.ts b/src/editor/parts.ts index c16a95dbc9..351df5062f 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -552,7 +552,7 @@ export class PartCreator { // part creator that support auto complete for /commands, // used in SendMessageComposer export class CommandPartCreator extends PartCreator { - createPartForInput(text: string, partIndex: number) { + public createPartForInput(text: string, partIndex: number): Part { // at beginning and starts with /? create if (partIndex === 0 && text[0] === "/") { // text will be inserted by model, so pass empty string @@ -562,11 +562,11 @@ export class CommandPartCreator extends PartCreator { } } - command(text: string) { + public command(text: string): CommandPart { return new CommandPart(text, this.autoCompleteCreator); } - deserializePart(part: Part): Part { + public deserializePart(part: SerializedPart): Part { if (part.type === "command") { return this.command(part.text); } else { @@ -576,7 +576,7 @@ export class CommandPartCreator extends PartCreator { } class CommandPart extends PillCandidatePart { - get type(): IPillCandidatePart["type"] { + public get type(): IPillCandidatePart["type"] { return Type.Command; } } diff --git a/src/utils/EditorStateTransfer.ts b/src/utils/EditorStateTransfer.ts index ba303f9b73..d2ce58f7dc 100644 --- a/src/utils/EditorStateTransfer.ts +++ b/src/utils/EditorStateTransfer.ts @@ -17,7 +17,7 @@ limitations under the License. import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SerializedPart } from "../editor/parts"; -import { Caret } from "../editor/caret"; +import DocumentOffset from "../editor/offset"; /** * Used while editing, to pass the event, and to preserve editor state @@ -26,28 +26,28 @@ import { Caret } from "../editor/caret"; */ export default class EditorStateTransfer { private serializedParts: SerializedPart[] = null; - private caret: Caret = null; + private caret: DocumentOffset = null; constructor(private readonly event: MatrixEvent) {} - public setEditorState(caret: Caret, serializedParts: SerializedPart[]) { + public setEditorState(caret: DocumentOffset, serializedParts: SerializedPart[]) { this.caret = caret; this.serializedParts = serializedParts; } - public hasEditorState() { + public hasEditorState(): boolean { return !!this.serializedParts; } - public getSerializedParts() { + public getSerializedParts(): SerializedPart[] { return this.serializedParts; } - public getCaret() { + public getCaret(): DocumentOffset { return this.caret; } - public getEvent() { + public getEvent(): MatrixEvent { return this.event; } } diff --git a/src/utils/pillify.js b/src/utils/pillify.tsx similarity index 91% rename from src/utils/pillify.js rename to src/utils/pillify.tsx index 489ba5d504..22240fcda5 100644 --- a/src/utils/pillify.js +++ b/src/utils/pillify.tsx @@ -16,9 +16,11 @@ limitations under the License. import React from "react"; import ReactDOM from 'react-dom'; +import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import { MatrixClientPeg } from '../MatrixClientPeg'; import SettingsStore from "../settings/SettingsStore"; -import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; import Pill from "../components/views/elements/Pill"; import { parseAppLocalLink } from "./permalinks/Permalinks"; @@ -27,15 +29,15 @@ import { parseAppLocalLink } from "./permalinks/Permalinks"; * into pills based on the context of a given room. Returns a list of * the resulting React nodes so they can be unmounted rather than leaking. * - * @param {Node[]} nodes - a list of sibling DOM nodes to traverse to try + * @param {Element[]} nodes - a list of sibling DOM nodes to traverse to try * to turn into pills. * @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are * part of representing. - * @param {Node[]} pills: an accumulator of the DOM nodes which contain + * @param {Element[]} pills: an accumulator of the DOM nodes which contain * React components which have been mounted as part of this. * The initial caller should pass in an empty array to seed the accumulator. */ -export function pillifyLinks(nodes, mxEvent, pills) { +export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pills: Element[]) { const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); let node = nodes[0]; @@ -73,7 +75,7 @@ export function pillifyLinks(nodes, mxEvent, pills) { // to clear the pills from the last run of pillifyLinks !node.parentElement.classList.contains("mx_AtRoomPill") ) { - let currentTextNode = node; + let currentTextNode = node as Node as Text; const roomNotifTextNodes = []; // Take a textNode and break it up to make all the instances of @room their @@ -125,10 +127,10 @@ export function pillifyLinks(nodes, mxEvent, pills) { } if (node.childNodes && node.childNodes.length && !pillified) { - pillifyLinks(node.childNodes, mxEvent, pills); + pillifyLinks(node.childNodes as NodeListOf, mxEvent, pills); } - node = node.nextSibling; + node = node.nextSibling as Element; } } @@ -140,10 +142,10 @@ export function pillifyLinks(nodes, mxEvent, pills) { * emitter on BaseAvatar as per * https://github.com/vector-im/element-web/issues/12417 * - * @param {Node[]} pills - array of pill containers whose React + * @param {Element[]} pills - array of pill containers whose React * components should be unmounted. */ -export function unmountPills(pills) { +export function unmountPills(pills: Element[]) { for (const pillContainer of pills) { ReactDOM.unmountComponentAtNode(pillContainer); } diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js index 2fddf8b691..2947f0fe60 100644 --- a/test/components/views/rooms/SendMessageComposer-test.js +++ b/test/components/views/rooms/SendMessageComposer-test.js @@ -147,7 +147,7 @@ describe('', () => { wrapper.update(); }); - const key = wrapper.find(SendMessageComposer).instance()._editorStateKey; + const key = wrapper.find(SendMessageComposer).instance().editorStateKey; expect(wrapper.text()).toBe("Test Text"); expect(localStorage.getItem(key)).toBeNull(); @@ -188,7 +188,7 @@ describe('', () => { wrapper.update(); }); - const key = wrapper.find(SendMessageComposer).instance()._editorStateKey; + const key = wrapper.find(SendMessageComposer).instance().editorStateKey; expect(wrapper.text()).toBe("Hello World"); expect(localStorage.getItem(key)).toBeNull(); From 0a5abb09f4111533aa44638788368a700ef38557 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Jun 2021 13:03:29 +0100 Subject: [PATCH 02/23] Fixes identified by TS --- src/components/views/rooms/EditMessageComposer.tsx | 2 +- src/components/views/rooms/SendMessageComposer.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index b06b57dced..9c0bea4e60 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -395,7 +395,7 @@ export default class EditMessageComposer extends React.Component const sel = document.getSelection(); let caret; if (sel.focusNode) { - caret = getCaretOffsetAndText(this.editorRef.current, sel).caret; + caret = getCaretOffsetAndText(this.editorRef.current?.editorRef.current, sel).caret; } const parts = this.model.serializeParts(); // if caret is undefined because for some reason there isn't a valid selection, diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 2c670f6f64..a934216884 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -207,20 +207,20 @@ export default class SendMessageComposer extends React.Component { // we keep sent messages/commands in a separate history (separate from undo history) // so you can alt+up/down in them - private selectSendHistory(up: boolean): void { + private selectSendHistory(up: boolean): boolean { const delta = up ? -1 : 1; // True if we are not currently selecting history, but composing a message if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { // We can't go any further - there isn't any more history, so nop. if (!up) { - return; + return false; } this.currentlyComposedEditorState = this.model.serializeParts(); } else if (this.sendHistoryManager.currentIndex + delta === this.sendHistoryManager.history.length) { // True when we return to the message being composed currently this.model.reset(this.currentlyComposedEditorState); this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length; - return; + return true; } const { parts, replyEventId } = this.sendHistoryManager.getItem(delta); dis.dispatch({ @@ -231,6 +231,7 @@ export default class SendMessageComposer extends React.Component { this.model.reset(parts); this.editorRef.current?.focus(); } + return true; } private isSlashCommand(): boolean { From 3a80df422225e5249f84976b7ec2f9e721935640 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Jun 2021 13:45:43 +0100 Subject: [PATCH 03/23] Fix react context not being assigned during construction --- src/components/views/rooms/EditMessageComposer.tsx | 5 +++-- src/components/views/rooms/SendMessageComposer.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 9c0bea4e60..4e51a0105b 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -121,14 +121,15 @@ interface IState { @replaceableComponent("views.rooms.EditMessageComposer") export default class EditMessageComposer extends React.Component { static contextType = MatrixClientContext; - context: React.ContextType; + context!: React.ContextType; private readonly editorRef = createRef(); private readonly dispatcherRef: string; private model: EditorModel = null; constructor(props: IProps, context: React.ContextType) { - super(props, context); + super(props); + this.context = context; // otherwise React will only set it prior to render due to type def above this.state = { saveDisabled: true, diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index a934216884..f088ec0c8e 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -136,7 +136,7 @@ interface IProps { @replaceableComponent("views.rooms.SendMessageComposer") export default class SendMessageComposer extends React.Component { static contextType = MatrixClientContext; - context: React.ContextType; + context!: React.ContextType; private readonly prepareToEncrypt?: RateLimitedFunc; private readonly editorRef = createRef(); @@ -146,8 +146,9 @@ export default class SendMessageComposer extends React.Component { private sendHistoryManager: SendHistoryManager; constructor(props: IProps, context: React.ContextType) { - super(props, context); - if (context.isCryptoEnabled() && context.isRoomEncrypted(this.props.room.roomId)) { + super(props); + this.context = context; // otherwise React will only set it prior to render due to type def above + if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) { this.prepareToEncrypt = new RateLimitedFunc(() => { this.context.prepareToEncrypt(this.props.room); }, 60000); From 30d027d2b46f7e4d9e91bd0315ce6bb95c1f3317 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 08:29:53 +0100 Subject: [PATCH 04/23] fix js-sdk lib import --- src/components/views/messages/TextualBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index cb9de013bf..f9e0f695ab 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -18,7 +18,7 @@ import React, { createRef, SyntheticEvent } from 'react'; import ReactDOM from 'react-dom'; import highlight from 'highlight.js'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { MsgType } from "matrix-js-sdk/lib/@types/event"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; import * as HtmlUtils from '../../../HtmlUtils'; import { formatDate } from '../../../DateUtils'; From d47194e61d1f6833bdf7f91c324b0fbf3a5e03fb Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 11:17:18 +0100 Subject: [PATCH 05/23] Migrate SearchResultTile to TypeScript --- src/components/views/rooms/EventTile.tsx | 2 +- ...archResultTile.js => SearchResultTile.tsx} | 36 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) rename src/components/views/rooms/{SearchResultTile.js => SearchResultTile.tsx} (76%) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 277f3ccb7c..baaaa16b57 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -267,7 +267,7 @@ interface IProps { showReactions?: boolean; // which layout to use - layout: Layout; + layout?: Layout; // whether or not to show flair at all enableFlair?: boolean; diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.tsx similarity index 76% rename from src/components/views/rooms/SearchResultTile.js rename to src/components/views/rooms/SearchResultTile.tsx index 3581a26351..766abaff69 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -16,31 +16,27 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import { haveTileForEvent } from "./EventTile"; +import EventTile, { haveTileForEvent } from "./EventTile"; +import DateSeparator from '../messages/DateSeparator'; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; +import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + // a matrix-js-sdk SearchResult containing the details of this result + searchResult: any; + // a list of strings to be highlighted in the results + searchHighlights?: string[]; + // href for the highlights in this result + resultLink?: string; + onHeightChanged?: () => void; + permalinkCreator?: RoomPermalinkCreator; +} + @replaceableComponent("views.rooms.SearchResultTile") -export default class SearchResultTile extends React.Component { - static propTypes = { - // a matrix-js-sdk SearchResult containing the details of this result - searchResult: PropTypes.object.isRequired, - - // a list of strings to be highlighted in the results - searchHighlights: PropTypes.array, - - // href for the highlights in this result - resultLink: PropTypes.string, - - onHeightChanged: PropTypes.func, - }; - - render() { - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - const EventTile = sdk.getComponent('rooms.EventTile'); +export default class SearchResultTile extends React.Component { + public render() { const result = this.props.searchResult; const mxEv = result.context.getEvent(); const eventId = mxEv.getId(); From 6f62233634b0913da5384c42153ad83768e86bb2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 11:18:07 +0100 Subject: [PATCH 06/23] Prevent browser to crash when unclosed HTML tag is sent to sanitizeHtml --- src/HtmlUtils.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index c80b50c566..59ec8811aa 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -37,6 +37,7 @@ import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks" import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; +import { highlight } from 'highlight.js'; linkifyMatrix(linkify); @@ -403,9 +404,11 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts try { if (highlights && highlights.length > 0) { const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); - const safeHighlights = highlights.map(function(highlight) { - return sanitizeHtml(highlight, sanitizeParams); - }); + const safeHighlights = highlights + // sanitizeHtml can hang if an unclosed HTML tag is thrown at it + // A search for ` !highlight.includes("<")) + .map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams)); // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join(''); From ede87129b2512146be554fff69dd994359f6f969 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 11:45:29 +0100 Subject: [PATCH 07/23] Add a layout fallback for EventTile --- src/components/views/rooms/EventTile.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index baaaa16b57..0c4d2f6fa9 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -267,7 +267,7 @@ interface IProps { showReactions?: boolean; // which layout to use - layout?: Layout; + layout: Layout; // whether or not to show flair at all enableFlair?: boolean; @@ -321,6 +321,7 @@ export default class EventTile extends React.Component { static defaultProps = { // no-op function because onHeightChanged is optional yet some sub-components assume its existence onHeightChanged: function() {}, + layout: Layout.Group, }; static contextType = MatrixClientContext; From 04db8333e3059b6ddc89f5e34d9e402461bf4c0b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 12:13:43 +0100 Subject: [PATCH 08/23] Fix typing and unused import --- src/HtmlUtils.tsx | 1 - src/components/views/rooms/SearchResultTile.tsx | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 59ec8811aa..4a1ad2f074 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -37,7 +37,6 @@ import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks" import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; -import { highlight } from 'highlight.js'; linkifyMatrix(linkify); diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 766abaff69..980e8835f8 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import EventTile, { haveTileForEvent } from "./EventTile"; import DateSeparator from '../messages/DateSeparator'; import SettingsStore from "../../../settings/SettingsStore"; @@ -25,7 +26,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // a matrix-js-sdk SearchResult containing the details of this result - searchResult: any; + searchResult: SearchResult; // a list of strings to be highlighted in the results searchHighlights?: string[]; // href for the highlights in this result From 9c93b9002f32dbb219b103ef7efb0544b639aaa5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 12:23:36 +0100 Subject: [PATCH 09/23] Add extra context for filtering out '>' for sanitizeHtml --- src/HtmlUtils.tsx | 3 +++ src/components/views/rooms/EventTile.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 4a1ad2f074..91245c943e 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -406,6 +406,9 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts const safeHighlights = highlights // sanitizeHtml can hang if an unclosed HTML tag is thrown at it // A search for ` !highlight.includes("<")) .map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams)); // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0c4d2f6fa9..cebb631708 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -267,7 +267,7 @@ interface IProps { showReactions?: boolean; // which layout to use - layout: Layout; + layout?: Layout; // whether or not to show flair at all enableFlair?: boolean; From c9fa3470156e567cc8af660cb811e2b2298adcdc Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 12:27:06 +0100 Subject: [PATCH 10/23] Upgrade browserlist target versions --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 89e11fcea5..6f08372e18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2345,9 +2345,9 @@ camelcase@^6.0.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001173: - version "1.0.30001178" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001178.tgz#3ad813b2b2c7d585b0be0a2440e1e233c6eabdbc" - integrity sha512-VtdZLC0vsXykKni8Uztx45xynytOi71Ufx9T8kHptSw9AL4dpqailUJJHavttuzUe1KYuBYtChiWv+BAb7mPmQ== + version "1.0.30001241" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001241.tgz" + integrity sha512-1uoSZ1Pq1VpH0WerIMqwptXHNNGfdl7d1cJUFs80CwQ/lVzdhTvsFZCeNFslze7AjsQnb4C85tzclPa1VShbeQ== capture-exit@^2.0.0: version "2.0.0" From 46b2f0404a6f192b29903a9b85a44b94ec757bdf Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 13:45:33 +0100 Subject: [PATCH 11/23] Basic ts-ification of SetupEncryptionBody --- ...ryptionBody.js => SetupEncryptionBody.tsx} | 40 ++++++++++++------- .../views/right_panel/EncryptionPanel.tsx | 2 +- 2 files changed, 27 insertions(+), 15 deletions(-) rename src/components/structures/auth/{SetupEncryptionBody.js => SetupEncryptionBody.tsx} (91%) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.tsx similarity index 91% rename from src/components/structures/auth/SetupEncryptionBody.js rename to src/components/structures/auth/SetupEncryptionBody.tsx index f0798b6d1a..0bda596b5c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 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. @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; @@ -23,23 +22,31 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi import * as sdk from '../../../index'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; +import EncryptionPanel from "../../views/right_panel/EncryptionPanel" -function keyHasPassphrase(keyInfo) { - return ( +function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { + return Boolean( keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations ); } -@replaceableComponent("structures.auth.SetupEncryptionBody") -export default class SetupEncryptionBody extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (boolean) => void; +} - constructor() { - super(); +interface IState { + phase: Phase; + verificationRequest: any; + backupInfo: any; +} + +@replaceableComponent("structures.auth.SetupEncryptionBody") +export default class SetupEncryptionBody extends React.Component { + constructor(props) { + super(props); const store = SetupEncryptionStore.sharedInstance(); store.on("update", this._onStoreUpdate); store.start(); @@ -56,7 +63,7 @@ export default class SetupEncryptionBody extends React.Component { _onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { - this.props.onFinished(); + this.props.onFinished(true); return; } this.setState({ @@ -113,6 +120,10 @@ export default class SetupEncryptionBody extends React.Component { store.done(); } + onEncryptionPanelClose = () => { + this.props.onFinished(false); + } + render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -121,12 +132,13 @@ export default class SetupEncryptionBody extends React.Component { } = this.state; if (this.state.verificationRequest) { - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); return ; } else if (phase === Phase.Intro) { const store = SetupEncryptionStore.sharedInstance(); diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index 3a26427246..b2e6e5fc2d 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -39,7 +39,7 @@ interface IProps { member: RoomMember | User; onClose: () => void; verificationRequest: VerificationRequest; - verificationRequestPromise: Promise; + verificationRequestPromise?: Promise; layout: string; inDialog: boolean; isRoomEncrypted: boolean; From 70a3679d4363fb3dd227635d2c4b278daddb12f4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 13:49:58 +0100 Subject: [PATCH 12/23] more ts --- .../structures/auth/SetupEncryptionBody.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 0bda596b5c..35377d2a0e 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -24,6 +24,8 @@ import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStor import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; import EncryptionPanel from "../../views/right_panel/EncryptionPanel" +import AccessibleButton from '../../views/elements/AccessibleButton'; +import Spinner from '../../views/elements/Spinner'; function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { return Boolean( @@ -48,7 +50,7 @@ export default class SetupEncryptionBody extends React.Component constructor(props) { super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this._onStoreUpdate); + store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase, @@ -60,7 +62,7 @@ export default class SetupEncryptionBody extends React.Component }; } - _onStoreUpdate = () => { + private onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { this.props.onFinished(true); @@ -73,18 +75,18 @@ export default class SetupEncryptionBody extends React.Component }); }; - componentWillUnmount() { + public componentWillUnmount() { const store = SetupEncryptionStore.sharedInstance(); - store.off("update", this._onStoreUpdate); + store.off("update", this.onStoreUpdate); store.stop(); } - _onUsePassphraseClick = async () => { + private onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); store.usePassPhrase(); } - _onVerifyClick = () => { + private onVerifyClick = () => { const cli = MatrixClientPeg.get(); const userId = cli.getUserId(); const requestPromise = cli.requestVerification(userId); @@ -100,33 +102,31 @@ export default class SetupEncryptionBody extends React.Component }); } - onSkipClick = () => { + private onSkipClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skip(); } - onSkipConfirmClick = () => { + private onSkipConfirmClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skipConfirm(); } - onSkipBackClick = () => { + private onSkipBackClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.returnAfterSkip(); } - onDoneClick = () => { + private onDoneClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.done(); } - onEncryptionPanelClose = () => { + private onEncryptionPanelClose = () => { this.props.onFinished(false); } - render() { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - + public render() { const { phase, } = this.state; @@ -151,14 +151,14 @@ export default class SetupEncryptionBody extends React.Component let useRecoveryKeyButton; if (recoveryKeyPrompt) { - useRecoveryKeyButton = + useRecoveryKeyButton = {recoveryKeyPrompt} ; } let verifyButton; if (store.hasDevicesToVerifyAgainst) { - verifyButton = + verifyButton = { _t("Use another login") } ; } @@ -229,7 +229,6 @@ export default class SetupEncryptionBody extends React.Component
); } else if (phase === Phase.Busy || phase === Phase.Loading) { - const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { console.log(`SetupEncryptionBody: Unknown phase ${phase}`); From 642405dbd04cd4ca7676cbad5d92e873a1fe7186 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 13:55:57 +0100 Subject: [PATCH 13/23] Use new types --- src/components/structures/auth/SetupEncryptionBody.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 35377d2a0e..bd03c5bddb 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -19,13 +19,14 @@ import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; -import * as sdk from '../../../index'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; import EncryptionPanel from "../../views/right_panel/EncryptionPanel" import AccessibleButton from '../../views/elements/AccessibleButton'; import Spinner from '../../views/elements/Spinner'; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { return Boolean( @@ -41,8 +42,8 @@ interface IProps { interface IState { phase: Phase; - verificationRequest: any; - backupInfo: any; + verificationRequest: VerificationRequest; + backupInfo: IKeyBackupInfo; } @replaceableComponent("structures.auth.SetupEncryptionBody") From 16fb24fa09c39c2ead32ddd6d58a7433d5c20cf5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:03:01 +0100 Subject: [PATCH 14/23] Remove unused prop --- src/components/structures/auth/SetupEncryptionBody.tsx | 1 - src/components/views/right_panel/EncryptionPanel.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index bd03c5bddb..81fee432f6 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -135,7 +135,6 @@ export default class SetupEncryptionBody extends React.Component if (this.state.verificationRequest) { return ; layout: string; - inDialog: boolean; isRoomEncrypted: boolean; } From 5d3b94b7c9e8e2c8c97a41bc43bae1fdbd2b4b0f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:22:58 +0100 Subject: [PATCH 15/23] Convert VerificationRequestDialog --- ...ialog.js => VerificationRequestDialog.tsx} | 43 +++++++++++-------- .../views/toasts/VerificationRequestToast.tsx | 3 +- 2 files changed, 25 insertions(+), 21 deletions(-) rename src/components/views/dialogs/{VerificationRequestDialog.js => VerificationRequestDialog.tsx} (70%) diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.tsx similarity index 70% rename from src/components/views/dialogs/VerificationRequestDialog.js rename to src/components/views/dialogs/VerificationRequestDialog.tsx index bf5d63b895..4d3123c274 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 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. @@ -15,27 +15,33 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import BaseDialog from "./BaseDialog"; +import EncryptionPanel from "../right_panel/EncryptionPanel"; +import { User } from 'matrix-js-sdk'; + +interface IProps { + verificationRequest: VerificationRequest; + verificationRequestPromise: Promise; + onFinished: () => void; + member: User; +} + +interface IState { + verificationRequest: VerificationRequest; +} @replaceableComponent("views.dialogs.VerificationRequestDialog") -export default class VerificationRequestDialog extends React.Component { - static propTypes = { - verificationRequest: PropTypes.object, - verificationRequestPromise: PropTypes.object, - onFinished: PropTypes.func.isRequired, - member: PropTypes.string, - }; - - constructor(...args) { - super(...args); - this.state = {}; - if (this.props.verificationRequest) { - this.state.verificationRequest = this.props.verificationRequest; - } else if (this.props.verificationRequestPromise) { +export default class VerificationRequestDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + verificationRequest: this.props.verificationRequest, + }; + if (this.props.verificationRequestPromise) { this.props.verificationRequestPromise.then(r => { this.setState({ verificationRequest: r }); }); @@ -43,8 +49,6 @@ export default class VerificationRequestDialog extends React.Component { } render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); const request = this.state.verificationRequest; const otherUserId = request && request.otherUserId; const member = this.props.member || @@ -65,6 +69,7 @@ export default class VerificationRequestDialog extends React.Component { verificationRequestPromise={this.props.verificationRequestPromise} onClose={this.props.onFinished} member={member} + isRoomEncrypted={false} /> ; } diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 8f6b552334..14b3c23737 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; -import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; @@ -30,6 +29,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import VerificationRequestDialog from "../dialogs/VerificationRequestDialog" interface IProps { toastKey: string; @@ -123,7 +123,6 @@ export default class VerificationRequestToast extends React.PureComponent { From 676163d5cd81312c813662343176c710ff885c52 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:51:07 +0100 Subject: [PATCH 16/23] convert NewSessionReviewDialog --- ...ewDialog.js => NewSessionReviewDialog.tsx} | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) rename src/components/views/dialogs/{NewSessionReviewDialog.js => NewSessionReviewDialog.tsx} (90%) diff --git a/src/components/views/dialogs/NewSessionReviewDialog.js b/src/components/views/dialogs/NewSessionReviewDialog.tsx similarity index 90% rename from src/components/views/dialogs/NewSessionReviewDialog.js rename to src/components/views/dialogs/NewSessionReviewDialog.tsx index 749f48ef48..ae334474ef 100644 --- a/src/components/views/dialogs/NewSessionReviewDialog.js +++ b/src/components/views/dialogs/NewSessionReviewDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 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. @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import { replaceableComponent } from '../../../utils/replaceableComponent'; @@ -23,18 +22,18 @@ import VerificationRequestDialog from './VerificationRequestDialog'; import BaseDialog from './BaseDialog'; import DialogButtons from '../elements/DialogButtons'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import * as sdk from '../../../index'; +import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo'; +import ErrorDialog from "./ErrorDialog"; + +interface IProps { + userId: string; + device: DeviceInfo, + onFinished: (boolean) => void; +} @replaceableComponent("views.dialogs.NewSessionReviewDialog") -export default class NewSessionReviewDialog extends React.PureComponent { - static propTypes = { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - } - - onCancelClick = () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); +export default class NewSessionReviewDialog extends React.PureComponent { + private onCancelClick = () => { Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, { headerImage: require("../../../../res/img/e2e/warning.svg"), title: _t("Your account is not secure"), @@ -54,7 +53,7 @@ export default class NewSessionReviewDialog extends React.PureComponent { }); } - onContinueClick = () => { + private onContinueClick = () => { const { userId, device } = this.props; const cli = MatrixClientPeg.get(); const requestPromise = cli.requestVerification( @@ -73,7 +72,7 @@ export default class NewSessionReviewDialog extends React.PureComponent { }); } - render() { + public render() { const { device } = this.props; const icon = ; From 66d95ed7b24dd8b59adee380fa29c2aec6a7f3f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:51:20 +0100 Subject: [PATCH 17/23] Fix button styling in verification bubbles --- res/css/views/messages/_common_CryptoEvent.scss | 1 + src/components/views/messages/MKeyVerificationRequest.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index bcc40f1181..d43df1b66c 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -43,6 +43,7 @@ limitations under the License. .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; grid-row: 1 / 3; + gap: 5px; } .mx_cryptoEvent_buttons { diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index d690513d55..8b8eb25f6b 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -154,7 +154,7 @@ export default class MKeyVerificationRequest extends React.Component { {_t("Decline")} - + {_t("Accept")}
); From 7e8bb70621db1e293503f5c5fbfdae19c660135d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:55:00 +0100 Subject: [PATCH 18/23] Never mind, it wasn't even used --- .../views/dialogs/NewSessionReviewDialog.tsx | 120 ------------------ 1 file changed, 120 deletions(-) delete mode 100644 src/components/views/dialogs/NewSessionReviewDialog.tsx diff --git a/src/components/views/dialogs/NewSessionReviewDialog.tsx b/src/components/views/dialogs/NewSessionReviewDialog.tsx deleted file mode 100644 index ae334474ef..0000000000 --- a/src/components/views/dialogs/NewSessionReviewDialog.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2020-2021 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 React from 'react'; -import { _t } from '../../../languageHandler'; -import Modal from '../../../Modal'; -import { replaceableComponent } from '../../../utils/replaceableComponent'; -import VerificationRequestDialog from './VerificationRequestDialog'; -import BaseDialog from './BaseDialog'; -import DialogButtons from '../elements/DialogButtons'; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo'; -import ErrorDialog from "./ErrorDialog"; - -interface IProps { - userId: string; - device: DeviceInfo, - onFinished: (boolean) => void; -} - -@replaceableComponent("views.dialogs.NewSessionReviewDialog") -export default class NewSessionReviewDialog extends React.PureComponent { - private onCancelClick = () => { - Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, { - headerImage: require("../../../../res/img/e2e/warning.svg"), - title: _t("Your account is not secure"), - description:
- {_t("One of the following may be compromised:")} -
    -
  • {_t("Your password")}
  • -
  • {_t("Your homeserver")}
  • -
  • {_t("This session, or the other session")}
  • -
  • {_t("The internet connection either session is using")}
  • -
-
- {_t("We recommend you change your password and Security Key in Settings immediately")} -
-
, - onFinished: () => this.props.onFinished(false), - }); - } - - private onContinueClick = () => { - const { userId, device } = this.props; - const cli = MatrixClientPeg.get(); - const requestPromise = cli.requestVerification( - userId, - [device.deviceId], - ); - - this.props.onFinished(true); - Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, { - verificationRequestPromise: requestPromise, - member: cli.getUser(userId), - onFinished: async () => { - const request = await requestPromise; - request.cancel(); - }, - }); - } - - public render() { - const { device } = this.props; - - const icon = ; - const titleText = _t("New session"); - - const title =

- {icon} - {titleText} -

; - - return ( - -
-

{_t( - "Use this session to verify your new one, " + - "granting it access to encrypted messages:", - )}

-
-
- - {device.getDisplayName()} - - ({device.deviceId}) - -
-
-

{_t( - "If you didn’t sign in to this session, " + - "your account may be compromised.", - )}

- -
-
- ); - } -} From c6d1dc7e8e0c194f7d94ce801dc744024e1d38b6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 15:11:18 +0100 Subject: [PATCH 19/23] lint --- .../structures/auth/SetupEncryptionBody.tsx | 18 +++++++++--------- .../views/toasts/VerificationRequestToast.tsx | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 81fee432f6..13790c2e47 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -22,7 +22,7 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; -import EncryptionPanel from "../../views/right_panel/EncryptionPanel" +import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton from '../../views/elements/AccessibleButton'; import Spinner from '../../views/elements/Spinner'; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; @@ -32,7 +32,7 @@ function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { return Boolean( keyInfo.passphrase && keyInfo.passphrase.salt && - keyInfo.passphrase.iterations + keyInfo.passphrase.iterations, ); } @@ -85,7 +85,7 @@ export default class SetupEncryptionBody extends React.Component private onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); store.usePassPhrase(); - } + }; private onVerifyClick = () => { const cli = MatrixClientPeg.get(); @@ -101,31 +101,31 @@ export default class SetupEncryptionBody extends React.Component request.cancel(); }, }); - } + }; private onSkipClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skip(); - } + }; private onSkipConfirmClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skipConfirm(); - } + }; private onSkipBackClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.returnAfterSkip(); - } + }; private onDoneClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.done(); - } + }; private onEncryptionPanelClose = () => { this.props.onFinished(false); - } + }; public render() { const { diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 14b3c23737..75254d7c62 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -29,7 +29,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import VerificationRequestDialog from "../dialogs/VerificationRequestDialog" +import VerificationRequestDialog from "../dialogs/VerificationRequestDialog"; interface IProps { toastKey: string; From 404aa96f487eb002bfb1e616a257d4e01007cfd2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 15:13:24 +0100 Subject: [PATCH 20/23] i18n --- src/i18n/strings/en_EN.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6f06e3d6a4..618d5763fa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2342,15 +2342,6 @@ "Message edits": "Message edits", "Modal Widget": "Modal Widget", "Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s", - "Your account is not secure": "Your account is not secure", - "Your password": "Your password", - "This session, or the other session": "This session, or the other session", - "The internet connection either session is using": "The internet connection either session is using", - "We recommend you change your password and Security Key in Settings immediately": "We recommend you change your password and Security Key in Settings immediately", - "New session": "New session", - "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", - "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", - "This wasn't me": "This wasn't me", "Doesn't look like a valid email address": "Doesn't look like a valid email address", "Continuing without email": "Continuing without email", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", From 5e3c8fae5c9277fa556081d33e1411fa704ec88a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 15:22:44 +0100 Subject: [PATCH 21/23] put this just on the buttons --- res/css/views/messages/_common_CryptoEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index d43df1b66c..afaed50fa4 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -43,12 +43,12 @@ limitations under the License. .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; grid-row: 1 / 3; - gap: 5px; } .mx_cryptoEvent_buttons { align-items: center; display: flex; + gap: 5px; } .mx_cryptoEvent_state { From ae16efcf5bdc4ffcdf6f893d94f9142663b2a1b5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 18:35:38 +0100 Subject: [PATCH 22/23] Remove rateLimitedFunc --- .eslintignore.errorfiles | 1 - src/components/structures/RightPanel.tsx | 8 ++-- src/components/structures/RoomView.tsx | 12 ++--- src/components/views/rooms/AuxPanel.tsx | 6 +-- src/components/views/rooms/MemberList.tsx | 8 ++-- src/components/views/rooms/RoomHeader.js | 7 ++- .../views/rooms/SendMessageComposer.tsx | 8 ++-- src/ratelimitedfunc.js | 47 ------------------- 8 files changed, 24 insertions(+), 73 deletions(-) delete mode 100644 src/ratelimitedfunc.js diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index d9177bebb5..ea66720529 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -4,7 +4,6 @@ src/Markdown.js src/NodeAnimator.js src/components/structures/RoomDirectory.js src/components/views/rooms/MemberList.js -src/ratelimitedfunc.js src/utils/DMRoomMap.js src/utils/MultiInviter.js test/components/structures/MessagePanel-test.js diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index c608f0eee9..338bfc06ab 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -23,7 +23,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import dis from '../../dispatcher/dispatcher'; -import RateLimitedFunc from '../../ratelimitedfunc'; import GroupStore from '../../stores/GroupStore'; import { RIGHT_PANEL_PHASES_NO_ARGS, @@ -48,6 +47,7 @@ import FilePanel from "./FilePanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; +import { DebouncedFunc, throttle } from 'lodash'; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -73,7 +73,7 @@ interface IState { export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - private readonly delayedUpdate: RateLimitedFunc; + private readonly delayedUpdate: DebouncedFunc<() => void>; private dispatcherRef: string; constructor(props, context) { @@ -85,9 +85,9 @@ export default class RightPanel extends React.Component { member: this.getUserForPanel(), }; - this.delayedUpdate = new RateLimitedFunc(() => { + this.delayedUpdate = throttle(() => { this.forceUpdate(); - }, 500); + }, 500, { leading: true, trailing: true }); } // Helper function to split out the logic for getPhaseFromProps() and the constructor diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 81000a87a6..d08eaa2ecd 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -37,7 +37,6 @@ import Modal from '../../Modal'; import * as sdk from '../../index'; import CallHandler, { PlaceCallType } from '../../CallHandler'; import dis from '../../dispatcher/dispatcher'; -import rateLimitedFunc from '../../ratelimitedfunc'; import * as Rooms from '../../Rooms'; import eventSearch, { searchPagination } from '../../Searching'; import MainSplit from './MainSplit'; @@ -82,6 +81,7 @@ import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; +import { throttle } from "lodash"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -675,8 +675,8 @@ export default class RoomView extends React.Component { ); } - // cancel any pending calls to the rate_limited_funcs - this.updateRoomMembers.cancelPendingCall(); + // cancel any pending calls to the throttled updated + this.updateRoomMembers.cancel(); for (const watcher of this.settingWatchers) { SettingsStore.unwatchSetting(watcher); @@ -1092,7 +1092,7 @@ export default class RoomView extends React.Component { return; } - this.updateRoomMembers(member); + this.updateRoomMembers(); }; private onMyMembership = (room: Room, membership: string, oldMembership: string) => { @@ -1114,10 +1114,10 @@ export default class RoomView extends React.Component { } // rate limited because a power level change will emit an event for every member in the room. - private updateRoomMembers = rateLimitedFunc(() => { + private updateRoomMembers = throttle(() => { this.updateDMState(); this.updateE2EStatus(this.state.room); - }, 500); + }, 500, { leading: true, trailing: true }); private checkDesktopNotifications() { const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 1c817140fa..99286dfa07 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -21,7 +21,6 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AppsDrawer from './AppsDrawer'; -import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { UIFeature } from "../../../settings/UIFeature"; @@ -29,6 +28,7 @@ import ResizeNotifier from "../../../utils/ResizeNotifier"; import CallViewForRoom from '../voip/CallViewForRoom'; import { objectHasDiff } from "../../../utils/objects"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { throttle } from 'lodash'; interface IProps { // js-sdk room object @@ -99,9 +99,9 @@ export default class AuxPanel extends React.Component { } } - private rateLimitedUpdate = new RateLimitedFunc(() => { + private rateLimitedUpdate = throttle(() => { this.setState({ counters: this.computeCounters() }); - }, 500); + }, 500, { leading: true, trailing: true }); private computeCounters() { const counters = []; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 68f87580df..f4df70c7ee 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher/dispatcher'; import { isValid3pidInvite } from "../../../RoomInvite"; -import rateLimitedFunction from "../../../ratelimitedfunc"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import BaseCard from "../right_panel/BaseCard"; @@ -43,6 +42,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import EntityTile from "./EntityTile"; import MemberTile from "./MemberTile"; import BaseAvatar from '../avatars/BaseAvatar'; +import { throttle } from 'lodash'; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -133,7 +133,7 @@ export default class MemberList extends React.Component { } // cancel any pending calls to the rate_limited_funcs - this.updateList.cancelPendingCall(); + this.updateList.cancel(); } /** @@ -237,9 +237,9 @@ export default class MemberList extends React.Component { if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); }; - private updateList = rateLimitedFunction(() => { + private updateList = throttle(() => { this.updateListNow(); - }, 500); + }, 500, { leading: true, trailing: true }); private updateListNow(): void { const members = this.roomMembers(); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 886317f2bf..b05b709e36 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -20,7 +20,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; @@ -31,6 +30,7 @@ import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { throttle } from 'lodash'; @replaceableComponent("views.rooms.RoomHeader") export default class RoomHeader extends React.Component { @@ -73,10 +73,9 @@ export default class RoomHeader extends React.Component { this._rateLimitedUpdate(); }; - _rateLimitedUpdate = new RateLimitedFunc(function() { - /* eslint-disable @babel/no-invalid-this */ + _rateLimitedUpdate = throttle(() => { this.forceUpdate(); - }, 500); + }, 500, { leading: true, trailing: true }); render() { let searchStatus = null; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index f088ec0c8e..ec190c829a 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -39,7 +39,6 @@ import Modal from '../../../Modal'; import { _t, _td } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RateLimitedFunc from '../../../ratelimitedfunc'; import { Action } from "../../../dispatcher/actions"; import { containsEmoji } from "../../../effects/utils"; import { CHAT_EFFECTS } from '../../../effects'; @@ -53,6 +52,7 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import { ActionPayload } from "../../../dispatcher/payloads"; +import { DebouncedFunc, throttle } from 'lodash'; function addReplyToMessageContent( content: IContent, @@ -138,7 +138,7 @@ export default class SendMessageComposer extends React.Component { static contextType = MatrixClientContext; context!: React.ContextType; - private readonly prepareToEncrypt?: RateLimitedFunc; + private readonly prepareToEncrypt?: DebouncedFunc<() => void>; private readonly editorRef = createRef(); private model: EditorModel = null; private currentlyComposedEditorState: SerializedPart[] = null; @@ -149,9 +149,9 @@ export default class SendMessageComposer extends React.Component { super(props); this.context = context; // otherwise React will only set it prior to render due to type def above if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) { - this.prepareToEncrypt = new RateLimitedFunc(() => { + this.prepareToEncrypt = throttle(() => { this.context.prepareToEncrypt(this.props.room); - }, 60000); + }, 60000, { leading: true, trailing: false }); } window.addEventListener("beforeunload", this.saveStoredEditorState); diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js deleted file mode 100644 index 3df3db615e..0000000000 --- a/src/ratelimitedfunc.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/** - * 'debounces' a function to only execute every n milliseconds. - * Useful when react-sdk gets many, many events but only wants - * to update the interface once for all of them. - * - * Note that the function must not take arguments, since the args - * could be different for each invocation of the function. - * - * The returned function has a 'cancelPendingCall' property which can be called - * on unmount or similar to cancel any pending update. - */ - -import {throttle} from "lodash"; - -export default function ratelimitedfunc(fn, time) { - const throttledFn = throttle(fn, time, { - leading: true, - trailing: true, - }); - const _bind = throttledFn.bind; - throttledFn.bind = function() { - const boundFn = _bind.apply(throttledFn, arguments); - boundFn.cancelPendingCall = throttledFn.cancelPendingCall; - return boundFn; - }; - - throttledFn.cancelPendingCall = function() { - throttledFn.cancel(); - }; - return throttledFn; -} From 9ea4ca39c63e7f4f98fee429b9681f78edc5fc6d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 18:54:51 +0100 Subject: [PATCH 23/23] Be more like other arrow functions --- src/components/structures/RightPanel.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 338bfc06ab..63027ab627 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -47,7 +47,7 @@ import FilePanel from "./FilePanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; -import { DebouncedFunc, throttle } from 'lodash'; +import { throttle } from 'lodash'; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -73,7 +73,6 @@ interface IState { export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - private readonly delayedUpdate: DebouncedFunc<() => void>; private dispatcherRef: string; constructor(props, context) { @@ -84,12 +83,12 @@ export default class RightPanel extends React.Component { isUserPrivilegedInGroup: null, member: this.getUserForPanel(), }; - - this.delayedUpdate = throttle(() => { - this.forceUpdate(); - }, 500, { leading: true, trailing: true }); } + private readonly delayedUpdate = throttle((): void => { + this.forceUpdate(); + }, 500, { leading: true, trailing: true }); + // Helper function to split out the logic for getPhaseFromProps() and the constructor // as both are called at the same time in the constructor. private getUserForPanel() {