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<any>;
+    ref?: React.Ref<HTMLSpanElement>;
 }
 
 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<string, Command>();
 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<IProps, IState> {
+    private readonly contentRef = createRef<HTMLSpanElement>();
 
-        /* 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 <pre> so that the copy button can be correctly positioned
                     // when the <pre> 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 = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
@@ -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<Element>): 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 = <Spoiler
-                    reason={reason}
-                    contentHtml={node.outerHTML}
-                />;
+                const spoiler = <Spoiler reason={reason} contentHtml={node.outerHTML} />;
 
                 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<Element>);
             }
 
-            node = node.nextSibling;
+            node = node.nextSibling as Element;
         }
     }
 
-    findLinks(nodes) {
-        let links = [];
+    private findLinks(nodes: ArrayLike<Element>): 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<ComposerInsertPayload>({
             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<void> => {
         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 (
             <AccessibleTooltipButton
                 className="mx_EventTile_edited"
-                onClick={this._openHistoryDialog}
+                onClick={this.openHistoryDialog}
                 title={_t("Edited at %(date)s. Click to view edits.", { date: dateString })}
                 tooltip={tooltip}
             >
@@ -490,24 +498,25 @@ export default class TextualBody extends React.Component {
 
     render() {
         if (this.props.editState) {
-            const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
             return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
         }
         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<boolean>('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 <LinkPreviewWidget
                     key={link}
@@ -534,7 +542,7 @@ export default class TextualBody extends React.Component {
         }
 
         switch (content.msgtype) {
-            case "m.emote":
+            case MsgType.Emote:
                 return (
                     <span className="mx_MEmoteBody mx_EventTile_content">
                         *&nbsp;
@@ -549,7 +557,7 @@ export default class TextualBody extends React.Component {
                         { widgets }
                     </span>
                 );
-            case "m.notice":
+            case MsgType.Notice:
                 return (
                     <span className="mx_MNoticeBody mx_EventTile_content">
                         { 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<IProps> {
     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 (
             <div className="mx_TextualEvent">{ text }</div>
         );
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<Selection>, 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<IProps, IState> {
-    private editorRef = createRef<HTMLDivElement>();
+    public readonly editorRef = createRef<HTMLDivElement>();
     private autocompleteRef = createRef<Autocomplete>();
-    private formatBarRef = createRef<typeof MessageComposerFormatBar>();
+    private formatBarRef = createRef<MessageComposerFormatBar>();
 
     private modifiedFlag = false;
     private isIMEComposing = false;
@@ -156,7 +148,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         }
     }
 
-    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<IProps, IState>
         }
     };
 
-    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<IProps, IState>
         }
     };
 
-    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<IProps, IState>
         }
     };
 
-    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<IProps, IState>
         }
     };
 
-    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<HTMLDivElement>) => {
+    private onPaste = (event: ClipboardEvent<HTMLDivElement>): 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<IProps, IState>
         replaceRangeAndMoveCaret(range, parts);
     };
 
-    private onInput = (event: Partial<InputEvent>) => {
+    private onInput = (event: Partial<InputEvent>): void => {
         // ignore any input while doing IME compositions
         if (this.isIMEComposing) {
             return;
@@ -339,7 +331,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         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<IProps, IState>
     // 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<IProps, IState>
         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<IProps, IState>
         }
     };
 
-    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<IProps, IState>
         }
     };
 
-    private async tabCompleteName() {
+    private async tabCompleteName(): Promise<void> {
         try {
             await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));
             const { model } = this.props;
@@ -557,27 +549,27 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         }
     }
 
-    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<IProps, IState>
         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<IProps, IState>
         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<IProps, IState>
         });
 
         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<IProps, IState>
         </div>);
     }
 
-    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<IProps, IState>
         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<IProps, IState>
         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<IProps, IState> {
     static contextType = MatrixClientContext;
+    context: React.ContextType<typeof MatrixClientContext>;
 
-    constructor(props, context) {
+    private readonly editorRef = createRef<BasicMessageComposer>();
+    private readonly dispatcherRef: string;
+    private model: EditorModel = null;
+
+    constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
         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<void> {
         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<void> => {
         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: <div>
@@ -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 (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this._onKeyDown}>
+        return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this.onKeyDown}>
             <BasicMessageComposer
-                ref={this._setEditorRef}
+                ref={this.editorRef}
                 model={this.model}
-                room={this._getRoom()}
+                room={this.getRoom()}
                 initialCaret={this.props.editState.getCaret()}
                 label={_t("Edit message")}
-                onChange={this._onChange}
+                onChange={this.onChange}
             />
             <div className="mx_EditMessageComposer_buttons">
-                <AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
-                <AccessibleButton kind="primary" onClick={this._sendEdit} disabled={this.state.saveDisabled}>
-                    {_t("Save")}
+                <AccessibleButton kind="secondary" onClick={this.cancelEdit}>
+                    { _t("Cancel") }
+                </AccessibleButton>
+                <AccessibleButton kind="primary" onClick={this.sendEdit} disabled={this.state.saveDisabled}>
+                    { _t("Save") }
                 </AccessibleButton>
             </div>
         </div>);
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<IProps, IState> {
         }
     };
 
-    addEmoji(emoji: string) {
+    private addEmoji(emoji: string) {
         dis.dispatch<ComposerInsertPayload>({
             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<IProps, IState> {
             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<Record<Formatting, string>>;
+    onAction(action: Formatting): void;
+}
+
+interface IState {
+    visible: boolean;
+}
+
+@replaceableComponent("views.rooms.MessageComposerFormatBar")
+export default class MessageComposerFormatBar extends React.PureComponent<IProps, IState> {
+    private readonly formatBarRef = createRef<HTMLDivElement>();
+
+    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 (<div className={classes} ref={ref => this._formatBarRef = ref}>
-            <FormatButton label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
-            <FormatButton label={_t("Italics")} onClick={() => this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
-            <FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} />
-            <FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" visible={this.state.visible} />
-            <FormatButton label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
+        return (<div className={classes} ref={this.formatBarRef}>
+            <FormatButton label={_t("Bold")} onClick={() => this.props.onAction(Formatting.Bold)} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
+            <FormatButton label={_t("Italics")} onClick={() => this.props.onAction(Formatting.Italics)} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
+            <FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} />
+            <FormatButton label={_t("Code block")} onClick={() => this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} />
+            <FormatButton label={_t("Quote")} onClick={() => this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
         </div>);
     }
 
-    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<IFormatButtonProps> {
     render() {
         const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
         let shortcut;
         if (this.props.shortcut) {
-            shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">{this.props.shortcut}</div>;
+            shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">
+                { this.props.shortcut }
+            </div>;
         }
         const tooltip = <div>
             <div className="mx_Tooltip_title">
-                {this.props.label}
+                { this.props.label }
             </div>
             <div className="mx_Tooltip_sub">
-                {shortcut}
+                { shortcut }
             </div>
         </div>;
 
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<IProps> {
     static contextType = MatrixClientContext;
+    context: React.ContextType<typeof MatrixClientContext>;
 
-    constructor(props, context) {
+    private readonly prepareToEncrypt?: RateLimitedFunc;
+    private readonly editorRef = createRef<BasicMessageComposer>();
+    private model: EditorModel = null;
+    private currentlyComposedEditorState: SerializedPart[] = null;
+    private dispatcherRef: string;
+    private sendHistoryManager: SendHistoryManager;
+
+    constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
         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<void> {
         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<void> {
         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: <div>
@@ -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<HTMLDivElement>): 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 (
-            <div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this._onKeyDown}>
+            <div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this.onKeyDown}>
                 <BasicMessageComposer
                     onChange={this.onChange}
-                    ref={this._setEditorRef}
+                    ref={this.editorRef}
                     model={this.model}
                     room={this.props.room}
                     label={this.props.placeholder}
                     placeholder={this.props.placeholder}
-                    onPaste={this._onPaste}
+                    onPaste={this.onPaste}
                     disabled={this.props.disabled}
                 />
             </div>
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<void> {
         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<void> {
         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<void> {
         const pos = callback();
-        let acPromise = null;
+        let acPromise: Promise<void> = 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<Element>, 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<Element>, 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('<SendMessageComposer/>', () => {
                 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('<SendMessageComposer/>', () => {
                 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<IProps, IState>
         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<IProps> {
 
     // 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<IProps> {
             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<IProps, IState> {
     static contextType = MatrixClientContext;
-    context: React.ContextType<typeof MatrixClientContext>;
+    context!: React.ContextType<typeof MatrixClientContext>;
 
     private readonly editorRef = createRef<BasicMessageComposer>();
     private readonly dispatcherRef: string;
     private model: EditorModel = null;
 
     constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
-        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<IProps> {
     static contextType = MatrixClientContext;
-    context: React.ContextType<typeof MatrixClientContext>;
+    context!: React.ContextType<typeof MatrixClientContext>;
 
     private readonly prepareToEncrypt?: RateLimitedFunc;
     private readonly editorRef = createRef<BasicMessageComposer>();
@@ -146,8 +146,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
     private sendHistoryManager: SendHistoryManager;
 
     constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
-        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 <germain@souquet.com>
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<IProps> {
+    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 <germain@souquet.com>
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 `<foo` will make the browser crash
+                .filter((highlight: string): boolean => !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 <germain@souquet.com>
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<IProps, IState> {
     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 <germain@souquet.com>
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 <germain@souquet.com>
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 `<foo` will make the browser crash
+                // an alternative would be to escape HTML special characters
+                // but that would bring no additional benefit as the highlighter
+                // does not work with those special chars
                 .filter((highlight: string): boolean => !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 <germain@souquet.com>
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 <dave@matrix.org>
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<IProps, IState> {
+    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 <EncryptionPanel
                 layout="dialog"
+                inDialog={true}
                 verificationRequest={this.state.verificationRequest}
-                onClose={this.props.onFinished}
+                onClose={this.onEncryptionPanelClose}
                 member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
+                isRoomEncrypted={false}
             />;
         } 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<VerificationRequest>;
+    verificationRequestPromise?: Promise<VerificationRequest>;
     layout: string;
     inDialog: boolean;
     isRoomEncrypted: boolean;

From 70a3679d4363fb3dd227635d2c4b278daddb12f4 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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<IProps, IState>
     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<IProps, IState>
         };
     }
 
-    _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<IProps, IState>
         });
     };
 
-    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<IProps, IState>
         });
     }
 
-    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<IProps, IState>
 
             let useRecoveryKeyButton;
             if (recoveryKeyPrompt) {
-                useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
+                useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
                     {recoveryKeyPrompt}
                 </AccessibleButton>;
             }
 
             let verifyButton;
             if (store.hasDevicesToVerifyAgainst) {
-                verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
+                verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
                     { _t("Use another login") }
                 </AccessibleButton>;
             }
@@ -229,7 +229,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
                 </div>
             );
         } else if (phase === Phase.Busy || phase === Phase.Loading) {
-            const Spinner = sdk.getComponent('views.elements.Spinner');
             return <Spinner />;
         } else {
             console.log(`SetupEncryptionBody: Unknown phase ${phase}`);

From 642405dbd04cd4ca7676cbad5d92e873a1fe7186 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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 <dave@matrix.org>
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<IProps, IState>
         if (this.state.verificationRequest) {
             return <EncryptionPanel
                 layout="dialog"
-                inDialog={true}
                 verificationRequest={this.state.verificationRequest}
                 onClose={this.onEncryptionPanelClose}
                 member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx
index b2e6e5fc2d..251c04d3cc 100644
--- a/src/components/views/right_panel/EncryptionPanel.tsx
+++ b/src/components/views/right_panel/EncryptionPanel.tsx
@@ -41,7 +41,6 @@ interface IProps {
     verificationRequest: VerificationRequest;
     verificationRequestPromise?: Promise<VerificationRequest>;
     layout: string;
-    inDialog: boolean;
     isRoomEncrypted: boolean;
 }
 

From 5d3b94b7c9e8e2c8c97a41bc43bae1fdbd2b4b0f Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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<VerificationRequest>;
+    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<IProps, IState> {
+    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}
             />
         </BaseDialog>;
     }
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<IProps
                     },
                 });
             } else {
-                const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
                 Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
                     verificationRequest: request,
                     onFinished: () => {

From 676163d5cd81312c813662343176c710ff885c52 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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<IProps> {
+    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 = <span className="mx_NewSessionReviewDialog_headerIcon mx_E2EIcon_warning"></span>;

From 66d95ed7b24dd8b59adee380fa29c2aec6a7f3f0 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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<IProps> {
                     <AccessibleButton kind="danger" onClick={this.onRejectClicked}>
                         {_t("Decline")}
                     </AccessibleButton>
-                    <AccessibleButton onClick={this.onAcceptClicked}>
+                    <AccessibleButton kind="primary" onClick={this.onAcceptClicked}>
                         {_t("Accept")}
                     </AccessibleButton>
                 </div>);

From 7e8bb70621db1e293503f5c5fbfdae19c660135d Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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<IProps> {
-    private onCancelClick = () => {
-        Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
-            headerImage: require("../../../../res/img/e2e/warning.svg"),
-            title: _t("Your account is not secure"),
-            description: <div>
-                {_t("One of the following may be compromised:")}
-                <ul>
-                    <li>{_t("Your password")}</li>
-                    <li>{_t("Your homeserver")}</li>
-                    <li>{_t("This session, or the other session")}</li>
-                    <li>{_t("The internet connection either session is using")}</li>
-                </ul>
-                <div>
-                    {_t("We recommend you change your password and Security Key in Settings immediately")}
-                </div>
-            </div>,
-            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 = <span className="mx_NewSessionReviewDialog_headerIcon mx_E2EIcon_warning"></span>;
-        const titleText = _t("New session");
-
-        const title = <h2 className="mx_NewSessionReviewDialog_header">
-            {icon}
-            {titleText}
-        </h2>;
-
-        return (
-            <BaseDialog
-                title={title}
-                onFinished={this.props.onFinished}
-            >
-                <div className="mx_NewSessionReviewDialog_body">
-                    <p>{_t(
-                        "Use this session to verify your new one, " +
-                        "granting it access to encrypted messages:",
-                    )}</p>
-                    <div className="mx_NewSessionReviewDialog_deviceInfo">
-                        <div>
-                            <span className="mx_NewSessionReviewDialog_deviceName">
-                                {device.getDisplayName()}
-                            </span> <span className="mx_NewSessionReviewDialog_deviceID">
-                                ({device.deviceId})
-                            </span>
-                        </div>
-                    </div>
-                    <p>{_t(
-                        "If you didn’t sign in to this session, " +
-                        "your account may be compromised.",
-                    )}</p>
-                    <DialogButtons
-                        cancelButton={_t("This wasn't me")}
-                        cancelButtonClass="danger"
-                        primaryButton={_t("Continue")}
-                        onCancel={this.onCancelClick}
-                        onPrimaryButtonClick={this.onContinueClick}
-                    />
-                </div>
-            </BaseDialog>
-        );
-    }
-}

From c6d1dc7e8e0c194f7d94ce801dc744024e1d38b6 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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<IProps, IState>
     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<IProps, IState>
                 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 <dave@matrix.org>
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 <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",

From 5e3c8fae5c9277fa556081d33e1411fa704ec88a Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
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 <dave@matrix.org>
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<IProps, IState> {
     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<IProps, IState> {
             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<IProps, IState> {
             );
         }
 
-        // 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<IProps, IState> {
             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<IProps, IState> {
     }
 
     // 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<IProps, IState> {
         }
     }
 
-    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<IProps, IState> {
         }
 
         // 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<IProps, IState> {
         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<IProps> {
     static contextType = MatrixClientContext;
     context!: React.ContextType<typeof MatrixClientContext>;
 
-    private readonly prepareToEncrypt?: RateLimitedFunc;
+    private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
     private readonly editorRef = createRef<BasicMessageComposer>();
     private model: EditorModel = null;
     private currentlyComposedEditorState: SerializedPart[] = null;
@@ -149,9 +149,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
         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 <dave@matrix.org>
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<IProps, IState> {
     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<IProps, IState> {
             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() {