From 6b746b5d1d1893eadb7300ed49fa237823da152e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 20:15:05 +0100 Subject: [PATCH 01/46] Migrate ConfirmDestroyCrossSigningDialog to TypeScript --- ...s => ConfirmDestroyCrossSigningDialog.tsx} | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) rename src/components/views/dialogs/security/{ConfirmDestroyCrossSigningDialog.js => ConfirmDestroyCrossSigningDialog.tsx} (83%) diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx similarity index 83% rename from src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js rename to src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx index e71983b074..6272302a76 100644 --- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx @@ -15,22 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {_t} from "../../../../languageHandler"; +import { _t } from "../../../../languageHandler"; import * as sdk from "../../../../index"; -import {replaceableComponent} from "../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../utils/replaceableComponent"; + +interface IProps { + onFinished: (success: boolean) => void; +} @replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog") -export default class ConfirmDestroyCrossSigningDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - - _onConfirm = () => { +export default class ConfirmDestroyCrossSigningDialog extends React.Component { + private onConfirm = (): void => { this.props.onFinished(true); }; - _onDecline = () => { + private onDecline = (): void => { this.props.onFinished(false); }; @@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component { ); From 0909112fa973a38ce871152ccd7d95aeb4a9cf96 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 20:21:11 +0100 Subject: [PATCH 02/46] Migrate CreateCrossSigningDialog to TypeScript --- ...Dialog.js => CreateCrossSigningDialog.tsx} | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) rename src/components/views/dialogs/security/{CreateCrossSigningDialog.js => CreateCrossSigningDialog.tsx} (85%) diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.js b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx similarity index 85% rename from src/components/views/dialogs/security/CreateCrossSigningDialog.js rename to src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index fedcc02f89..7770da3049 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.js +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; @@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons'; import BaseDialog from '../BaseDialog'; import Spinner from '../../elements/Spinner'; import InteractiveAuthDialog from '../InteractiveAuthDialog'; -import {replaceableComponent} from "../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../utils/replaceableComponent"; + +interface IProps { + accountPassword?: string; + tokenLogin?: boolean; + onFinished?: (success: boolean) => void; +} + +interface IState { + error: Error | null; + canUploadKeysWithPasswordOnly: boolean | null; + accountPassword: string; +} /* * Walks the user through the process of creating a cross-signing keys. In most @@ -33,12 +44,7 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent"; * may need to complete some steps to proceed. */ @replaceableComponent("views.dialogs.security.CreateCrossSigningDialog") -export default class CreateCrossSigningDialog extends React.PureComponent { - static propTypes = { - accountPassword: PropTypes.string, - tokenLogin: PropTypes.bool, - }; - +export default class CreateCrossSigningDialog extends React.PureComponent { constructor(props) { super(props); @@ -46,26 +52,24 @@ export default class CreateCrossSigningDialog extends React.PureComponent { error: null, // Does the server offer a UI auth flow with just m.login.password // for /keys/device_signing/upload? - canUploadKeysWithPasswordOnly: null, - accountPassword: props.accountPassword || "", - }; - - if (this.state.accountPassword) { // If we have an account password in memory, let's simplify and // assume it means password auth is also supported for device // signing key upload as well. This avoids hitting the server to // test auth flows, which may be slow under high load. - this.state.canUploadKeysWithPasswordOnly = true; - } else { - this._queryKeyUploadAuth(); + canUploadKeysWithPasswordOnly: props.accountPassword ? true : null, + accountPassword: props.accountPassword || "", + }; + + if (!this.state.accountPassword) { + this.queryKeyUploadAuth(); } } - componentDidMount() { - this._bootstrapCrossSigning(); + public componentDidMount(): void { + this.bootstrapCrossSigning(); } - async _queryKeyUploadAuth() { + private async queryKeyUploadAuth(): Promise { try { await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); // We should never get here: the server should always require @@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _doBootstrapUIAuth = async (makeRequest) => { + private doBootstrapUIAuth = async (makeRequest): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ type: 'm.login.password', @@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _bootstrapCrossSigning = async () => { + private bootstrapCrossSigning = async (): Promise => { this.setState({ error: null, }); @@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent { try { await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + authUploadDeviceSigningKeys: this.doBootstrapUIAuth, }); this.props.onFinished(true); } catch (e) { if (this.props.tokenLogin) { // ignore any failures, we are relying on grace period here - this.props.onFinished(); + this.props.onFinished(false); return; } @@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); } @@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {

{_t("Unable to set up keys")}

; From 68db2438ea84940086d8a05b960aeadd3e9de071 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 20:41:26 +0100 Subject: [PATCH 03/46] Migrate SetupEncryptionDialog to TypeScript --- ...ionDialog.js => SetupEncryptionDialog.tsx} | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) rename src/components/views/dialogs/security/{SetupEncryptionDialog.js => SetupEncryptionDialog.tsx} (77%) diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.js b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx similarity index 77% rename from src/components/views/dialogs/security/SetupEncryptionDialog.js rename to src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 3c15ea9f1d..01ed3e0771 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.js +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -15,14 +15,13 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody'; import BaseDialog from '../BaseDialog'; import { _t } from '../../../../languageHandler'; import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../../utils/replaceableComponent"; -function iconFromPhase(phase) { +function iconFromPhase(phase: string) { if (phase === PHASE_DONE) { return require("../../../../../res/img/e2e/verified.svg"); } else { @@ -30,32 +29,38 @@ function iconFromPhase(phase) { } } -@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") -export default class SetupEncryptionDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - constructor() { - super(); +interface IState { + icon: any; +} + +@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") +export default class SetupEncryptionDialog extends React.Component { + private store: SetupEncryptionStore; + + constructor(props) { + super(props); this.store = SetupEncryptionStore.sharedInstance(); this.state = {icon: iconFromPhase(this.store.phase)}; } - componentDidMount() { - this.store.on("update", this._onStoreUpdate); + public componentDidMount() { + this.store.on("update", this.onStoreUpdate); } - componentWillUnmount() { - this.store.removeListener("update", this._onStoreUpdate); + public componentWillUnmount() { + this.store.removeListener("update", this.onStoreUpdate); } - _onStoreUpdate = () => { + private onStoreUpdate = (): void => { this.setState({icon: iconFromPhase(this.store.phase)}); }; - render() { + public render() { return Date: Mon, 14 Jun 2021 20:58:20 +0100 Subject: [PATCH 04/46] migrate SetupEncryptionStore to TypeScript --- src/@types/global.d.ts | 2 + .../security/SetupEncryptionDialog.tsx | 8 +- ...yptionStore.js => SetupEncryptionStore.ts} | 97 +++++++++++-------- 3 files changed, 61 insertions(+), 46 deletions(-) rename src/stores/{SetupEncryptionStore.js => SetupEncryptionStore.ts} (73%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 22280b8a28..0c6b63dd33 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -44,6 +44,7 @@ import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; +import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; declare global { interface Window { @@ -84,6 +85,7 @@ declare global { mxPerformanceMonitor: PerformanceMonitor; mxPerformanceEntryNames: any; mxUIStore: UIStore; + mxSetupEncryptionStore?: SetupEncryptionStore; } interface Document { diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 01ed3e0771..b86b89cede 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -18,11 +18,11 @@ import React from 'react'; import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody'; import BaseDialog from '../BaseDialog'; import { _t } from '../../../../languageHandler'; -import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, PHASE } from '../../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../../utils/replaceableComponent"; -function iconFromPhase(phase: string) { - if (phase === PHASE_DONE) { +function iconFromPhase(phase: PHASE) { + if (phase === PHASE.DONE) { return require("../../../../../res/img/e2e/verified.svg"); } else { return require("../../../../../res/img/e2e/warning.svg"); @@ -34,7 +34,7 @@ interface IProps { } interface IState { - icon: any; + icon: PHASE; } @replaceableComponent("views.dialogs.security.SetupEncryptionDialog") diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.ts similarity index 73% rename from src/stores/SetupEncryptionStore.js rename to src/stores/SetupEncryptionStore.ts index b768ae69df..86e8b7afc3 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.ts @@ -15,29 +15,42 @@ limitations under the License. */ import EventEmitter from 'events'; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { IKeyBackupVersion } from "matrix-js-sdk/src/crypto/keybackup"; +import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from '../MatrixClientPeg'; import { accessSecretStorage, AccessCancelledError } from '../SecurityManager'; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -export const PHASE_LOADING = 0; -export const PHASE_INTRO = 1; -export const PHASE_BUSY = 2; -export const PHASE_DONE = 3; //final done stage, but still showing UX -export const PHASE_CONFIRM_SKIP = 4; -export const PHASE_FINISHED = 5; //UX can be closed +export enum PHASE { + LOADING = 0, + INTRO = 1, + BUSY = 2, + DONE = 3, // final done stage, but still showing UX + CONFIRM_SKIP = 4, + FINISHED = 5, // UX can be closed +} export class SetupEncryptionStore extends EventEmitter { - static sharedInstance() { - if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore(); - return global.mx_SetupEncryptionStore; + private started: boolean; + public phase: PHASE; + public verificationRequest: VerificationRequest; + public backupInfo: IKeyBackupVersion; + public keyId: string; + public keyInfo: ISecretStorageKeyInfo; + public hasDevicesToVerifyAgainst: boolean; + + public static sharedInstance() { + if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore(); + return window.mxSetupEncryptionStore; } - start() { - if (this._started) { + public start(): void { + if (this.started) { return; } - this._started = true; - this.phase = PHASE_LOADING; + this.started = true; + this.phase = PHASE.LOADING; this.verificationRequest = null; this.backupInfo = null; @@ -48,34 +61,34 @@ export class SetupEncryptionStore extends EventEmitter { const cli = MatrixClientPeg.get(); cli.on("crypto.verification.request", this.onVerificationRequest); - cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged); + cli.on('userTrustStatusChanged', this.onUserTrustStatusChanged); const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId()); if (requestsInProgress.length) { // If there are multiple, we take the most recent. Equally if the user sends another request from // another device after this screen has been shown, we'll switch to the new one, so this // generally doesn't support multiple requests. - this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); + this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); } this.fetchKeyInfo(); } - stop() { - if (!this._started) { + public stop(): void { + if (!this.started) { return; } - this._started = false; + this.started = false; if (this.verificationRequest) { this.verificationRequest.off("change", this.onVerificationRequestChange); } if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); - MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged); } } - async fetchKeyInfo() { + public async fetchKeyInfo(): Promise { const cli = MatrixClientPeg.get(); const keys = await cli.isSecretStored('m.cross_signing.master', false); if (keys === null || Object.keys(keys).length === 0) { @@ -97,15 +110,15 @@ export class SetupEncryptionStore extends EventEmitter { if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) { // skip before we can even render anything. - this.phase = PHASE_FINISHED; + this.phase = PHASE.FINISHED; } else { - this.phase = PHASE_INTRO; + this.phase = PHASE.INTRO; } this.emit("update"); } - async usePassPhrase() { - this.phase = PHASE_BUSY; + public async usePassPhrase(): Promise { + this.phase = PHASE.BUSY; this.emit("update"); const cli = MatrixClientPeg.get(); try { @@ -120,7 +133,7 @@ export class SetupEncryptionStore extends EventEmitter { // passphase cached for that work. This dialog itself will only wait // on the first trust check, and the key backup restore will happen // in the background. - await new Promise((resolve, reject) => { + await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => { accessSecretStorage(async () => { await cli.checkOwnCrossSigningTrust(); resolve(); @@ -134,7 +147,7 @@ export class SetupEncryptionStore extends EventEmitter { }); if (cli.getCrossSigningId()) { - this.phase = PHASE_DONE; + this.phase = PHASE.DONE; this.emit("update"); } } catch (e) { @@ -142,25 +155,25 @@ export class SetupEncryptionStore extends EventEmitter { console.log(e); } // this will throw if the user hits cancel, so ignore - this.phase = PHASE_INTRO; + this.phase = PHASE.INTRO; this.emit("update"); } } - _onUserTrustStatusChanged = (userId) => { + private onUserTrustStatusChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); if (publicKeysTrusted) { - this.phase = PHASE_DONE; + this.phase = PHASE.DONE; this.emit("update"); } } - onVerificationRequest = (request) => { - this._setActiveVerificationRequest(request); + public onVerificationRequest = (request: VerificationRequest): void => { + this.setActiveVerificationRequest(request); } - onVerificationRequestChange = () => { + public onVerificationRequestChange = (): void => { if (this.verificationRequest.cancelled) { this.verificationRequest.off("change", this.onVerificationRequestChange); this.verificationRequest = null; @@ -172,34 +185,34 @@ export class SetupEncryptionStore extends EventEmitter { // cross signing to be ready to use, so wait for the user trust status to // change (or change to DONE if it's already ready). const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); - this.phase = publicKeysTrusted ? PHASE_DONE : PHASE_BUSY; + this.phase = publicKeysTrusted ? PHASE.DONE : PHASE.BUSY; this.emit("update"); } } - skip() { - this.phase = PHASE_CONFIRM_SKIP; + public skip(): void { + this.phase = PHASE.CONFIRM_SKIP; this.emit("update"); } - skipConfirm() { - this.phase = PHASE_FINISHED; + public skipConfirm(): void { + this.phase = PHASE.FINISHED; this.emit("update"); } - returnAfterSkip() { - this.phase = PHASE_INTRO; + public returnAfterSkip(): void { + this.phase = PHASE.INTRO; this.emit("update"); } - done() { - this.phase = PHASE_FINISHED; + public done(): void { + this.phase = PHASE.FINISHED; this.emit("update"); // async - ask other clients for keys, if necessary MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests(); } - async _setActiveVerificationRequest(request) { + private async setActiveVerificationRequest(request: VerificationRequest): Promise { if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; if (this.verificationRequest) { From f2250af5657ce3a92b6a743a6c11b6da9e7e7bff Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:03:12 +0100 Subject: [PATCH 05/46] Migrate AskInviteAnywayDialog to TypeScript --- ...wayDialog.js => AskInviteAnywayDialog.tsx} | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) rename src/components/views/dialogs/{AskInviteAnywayDialog.js => AskInviteAnywayDialog.tsx} (73%) diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.tsx similarity index 73% rename from src/components/views/dialogs/AskInviteAnywayDialog.js rename to src/components/views/dialogs/AskInviteAnywayDialog.tsx index e6cd45ba6b..970883aca2 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx @@ -15,39 +15,41 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; -import {SettingLevel} from "../../../settings/SettingLevel"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { SettingLevel } from "../../../settings/SettingLevel"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + unknownProfileUsers: Array<{ + userId: string; + errorText: string; + }>; + onInviteAnyways: () => void; + onGiveUp: () => void; + onFinished: (success: boolean) => void; +} @replaceableComponent("views.dialogs.AskInviteAnywayDialog") -export default class AskInviteAnywayDialog extends React.Component { - static propTypes = { - unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] - onInviteAnyways: PropTypes.func.isRequired, - onGiveUp: PropTypes.func.isRequired, - onFinished: PropTypes.func.isRequired, - }; - - _onInviteClicked = () => { +export default class AskInviteAnywayDialog extends React.Component { + private onInviteClicked = (): void => { this.props.onInviteAnyways(); this.props.onFinished(true); }; - _onInviteNeverWarnClicked = () => { + private onInviteNeverWarnClicked = (): void => { SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false); this.props.onInviteAnyways(); this.props.onFinished(true); }; - _onGiveUpClicked = () => { + private onGiveUpClicked = (): void => { this.props.onGiveUp(); this.props.onFinished(false); }; - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const errorList = this.props.unknownProfileUsers @@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component { return (
+ {/* eslint-disable-next-line */}

{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}

    { errorList } @@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
- - -
From be922264485846e4f0a37fa23815326cea813c2a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:23:28 +0100 Subject: [PATCH 06/46] Migrate BugReportDialog to TypeScript --- ...BugReportDialog.js => BugReportDialog.tsx} | 96 ++++++++++--------- 1 file changed, 49 insertions(+), 47 deletions(-) rename src/components/views/dialogs/{BugReportDialog.js => BugReportDialog.tsx} (79%) diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.tsx similarity index 79% rename from src/components/views/dialogs/BugReportDialog.js rename to src/components/views/dialogs/BugReportDialog.tsx index cbe0130649..f938340a50 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -18,7 +18,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; @@ -27,8 +26,27 @@ import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-ragesh import AccessibleButton from "../elements/AccessibleButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; + initialText?: string; + label?: string; +} + +interface IState { + sendLogs: boolean; + busy: boolean; + err: string; + issueUrl: string; + text: string; + progress: string; + downloadBusy: boolean; + downloadProgress: string; +} + @replaceableComponent("views.dialogs.BugReportDialog") -export default class BugReportDialog extends React.Component { +export default class BugReportDialog extends React.Component { + private unmounted: boolean; + constructor(props) { super(props); this.state = { @@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component { downloadBusy: false, downloadProgress: null, }; - this._unmounted = false; - this._onSubmit = this._onSubmit.bind(this); - this._onCancel = this._onCancel.bind(this); - this._onTextChange = this._onTextChange.bind(this); - this._onIssueUrlChange = this._onIssueUrlChange.bind(this); - this._onSendLogsChange = this._onSendLogsChange.bind(this); - this._sendProgressCallback = this._sendProgressCallback.bind(this); - this._downloadProgressCallback = this._downloadProgressCallback.bind(this); + this.unmounted = false; } - componentWillUnmount() { - this._unmounted = true; + public componentWillUnmount() { + this.unmounted = true; } - _onCancel(ev) { + private onCancel = (): void => { this.props.onFinished(false); } - _onSubmit(ev) { + private onSubmit = (): void => { if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) { this.setState({ err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."), @@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component { (this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given'); this.setState({ busy: true, progress: null, err: null }); - this._sendProgressCallback(_t("Preparing to send logs")); + this.sendProgressCallback(_t("Preparing to send logs")); sendBugReport(SdkConfig.get().bug_report_endpoint_url, { userText, sendLogs: true, - progressCallback: this._sendProgressCallback, + progressCallback: this.sendProgressCallback, label: this.props.label, }).then(() => { - if (!this._unmounted) { + if (!this.unmounted) { this.props.onFinished(false); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // N.B. first param is passed to piwik and so doesn't want i18n @@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component { }); } }, (err) => { - if (!this._unmounted) { + if (!this.unmounted) { this.setState({ busy: false, progress: null, @@ -101,14 +112,14 @@ export default class BugReportDialog extends React.Component { }); } - _onDownload = async (ev) => { + private onDownload = async (): Promise => { this.setState({ downloadBusy: true }); - this._downloadProgressCallback(_t("Preparing to download logs")); + this.downloadProgressCallback(_t("Preparing to download logs")); try { await downloadBugReport({ sendLogs: true, - progressCallback: this._downloadProgressCallback, + progressCallback: this.downloadProgressCallback, label: this.props.label, }); @@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component { downloadProgress: null, }); } catch (err) { - if (!this._unmounted) { + if (!this.unmounted) { this.setState({ downloadBusy: false, downloadProgress: _t("Failed to send logs: ") + `${err.message}`, @@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component { } }; - _onTextChange(ev) { - this.setState({ text: ev.target.value }); + private onTextChange = (ev: React.FormEvent): void => { + this.setState({ text: ev.currentTarget.value }); } - _onIssueUrlChange(ev) { - this.setState({ issueUrl: ev.target.value }); + private onIssueUrlChange = (ev: React.FormEvent): void => { + this.setState({ issueUrl: ev.currentTarget.value }); } - _onSendLogsChange(ev) { - this.setState({ sendLogs: ev.target.checked }); - } - - _sendProgressCallback(progress) { - if (this._unmounted) { + private sendProgressCallback = (progress: string): void => { + if (this.unmounted) { return; } - this.setState({progress: progress}); + this.setState({ progress }); } - _downloadProgressCallback(downloadProgress) { - if (this._unmounted) { + private downloadProgressCallback = (downloadProgress: string): void => { + if (this.unmounted) { return; } this.setState({ downloadProgress }); } - render() { + public render() { const Loader = sdk.getComponent("elements.Spinner"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component { } return ( - @@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {

- + { _t("Download logs") } {this.state.downloadProgress && {this.state.downloadProgress} ...} @@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component { type="text" className="mx_BugReportDialog_field_input" label={_t("GitHub issue")} - onChange={this._onIssueUrlChange} + onChange={this.onIssueUrlChange} value={this.state.issueUrl} placeholder="https://github.com/vector-im/element-web/issues/..." /> @@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component { element="textarea" label={_t("Notes")} rows={5} - onChange={this._onTextChange} + onChange={this.onTextChange} value={this.state.text} placeholder={_t( "If there is additional context that would help in " + @@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component { {error}
); } } - -BugReportDialog.propTypes = { - onFinished: PropTypes.func.isRequired, - initialText: PropTypes.string, -}; From 1ff3aa0d746d9db291090e418c8d57db6c4cb203 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:26:15 +0100 Subject: [PATCH 07/46] Migrate ChangelogDialog to TypeScript --- ...ChangelogDialog.js => ChangelogDialog.tsx} | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) rename src/components/views/dialogs/{ChangelogDialog.js => ChangelogDialog.tsx} (88%) diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.tsx similarity index 88% rename from src/components/views/dialogs/ChangelogDialog.js rename to src/components/views/dialogs/ChangelogDialog.tsx index efbeba3977..0ded33cdcb 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.tsx @@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import request from 'browser-request'; import { _t } from '../../../languageHandler'; +interface IProps { + newVersion: string; + version: string; + onFinished: (success: boolean) => void; +} + const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk']; -export default class ChangelogDialog extends React.Component { +export default class ChangelogDialog extends React.Component { constructor(props) { super(props); this.state = {}; } - componentDidMount() { + public componentDidMount() { const version = this.props.newVersion.split('-'); const version2 = this.props.version.split('-'); if (version == null || version2 == null) return; @@ -49,7 +54,7 @@ export default class ChangelogDialog extends React.Component { } } - _elementsForCommit(commit) { + private elementsForCommit(commit): JSX.Element { return (
  • @@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component { ); } - render() { + public render() { const Spinner = sdk.getComponent('views.elements.Spinner'); const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); @@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component { msg: this.state[repo], }); } else { - content = this.state[repo].map(this._elementsForCommit); + content = this.state[repo].map(this.elementsForCommit); } return (
    @@ -99,9 +104,3 @@ export default class ChangelogDialog extends React.Component { ); } } - -ChangelogDialog.propTypes = { - version: PropTypes.string.isRequired, - newVersion: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, -}; From 6877504f2df0044d926de3ce3986d5430df96190 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:30:28 +0100 Subject: [PATCH 08/46] Migrate ConfirmAndWaitRedactDialog to TypeScript --- ...ialog.js => ConfirmAndWaitRedactDialog.tsx} | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) rename src/components/views/dialogs/{ConfirmAndWaitRedactDialog.js => ConfirmAndWaitRedactDialog.tsx} (89%) diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx similarity index 89% rename from src/components/views/dialogs/ConfirmAndWaitRedactDialog.js rename to src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx index 37d5510756..ae7b23c2c9 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx @@ -17,7 +17,17 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + redact: () => Promise; + onFinished: (success: boolean) => void; +} + +interface IState { + isRedacting: boolean; + redactionErrorCode: string | number; +} /* * A dialog for confirming a redaction. @@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; * To avoid this, we keep the dialog open as long as /redact is in progress. */ @replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog") -export default class ConfirmAndWaitRedactDialog extends React.PureComponent { +export default class ConfirmAndWaitRedactDialog extends React.PureComponent { constructor(props) { super(props); this.state = { @@ -41,7 +51,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { }; } - onParentFinished = async (proceed) => { + public onParentFinished = async (proceed: boolean): Promise => { if (proceed) { this.setState({isRedacting: true}); try { @@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { } }; - render() { + public render() { if (this.state.isRedacting) { if (this.state.redactionErrorCode) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); From 9443ef4ff9176ae4ce54925ea8cb4aa091cc4449 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:31:17 +0100 Subject: [PATCH 09/46] Migrate ConfirmRedactDialog to TypeScript --- .../{ConfirmRedactDialog.js => ConfirmRedactDialog.tsx} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename src/components/views/dialogs/{ConfirmRedactDialog.js => ConfirmRedactDialog.tsx} (95%) diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.tsx similarity index 95% rename from src/components/views/dialogs/ConfirmRedactDialog.js rename to src/components/views/dialogs/ConfirmRedactDialog.tsx index bd63d3acc1..eee05599e8 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -19,11 +19,15 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + /* * A dialog for confirming a redaction. */ @replaceableComponent("views.dialogs.ConfirmRedactDialog") -export default class ConfirmRedactDialog extends React.Component { +export default class ConfirmRedactDialog extends React.Component { render() { const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog'); return ( From c2aaba1f796c58f6be7f8d1c08237dd3918f1ef5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:36:56 +0100 Subject: [PATCH 10/46] Migrate ConfirmUserActionDialog to TypeScript --- ...nDialog.js => ConfirmUserActionDialog.tsx} | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) rename src/components/views/dialogs/{ConfirmUserActionDialog.js => ConfirmUserActionDialog.tsx} (76%) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.tsx similarity index 76% rename from src/components/views/dialogs/ConfirmUserActionDialog.js rename to src/components/views/dialogs/ConfirmUserActionDialog.tsx index 8059b9172a..13be70dbab 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -15,13 +15,31 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; +import RoomMember from "matrix-js-sdk/src/models/room-member.js"; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; + +interface IProps { + // matrix-js-sdk (room) member object. Supply either this or 'groupMember' + member: RoomMember; + // group member object. Supply either this or 'member' + groupMember: GroupMemberType; + // needed if a group member is specified + matrixClient?: MatrixClient, + action: string; // eg. 'Ban' + title: string; // eg. 'Ban this user?' + + // Whether to display a text field for a reason + // If true, the second argument to onFinished will + // be the string entered. + askReason?: boolean; + danger?: boolean; + onFinished: (success: boolean, reason?: HTMLInputElement) => void; +} /* * A dialog for confirming an operation on another user. @@ -32,24 +50,8 @@ import {mediaFromMxc} from "../../../customisations/Media"; * Also tweaks the style for 'dangerous' actions (albeit only with colour) */ @replaceableComponent("views.dialogs.ConfirmUserActionDialog") -export default class ConfirmUserActionDialog extends React.Component { - static propTypes = { - // matrix-js-sdk (room) member object. Supply either this or 'groupMember' - member: PropTypes.object, - // group member object. Supply either this or 'member' - groupMember: GroupMemberType, - // needed if a group member is specified - matrixClient: PropTypes.instanceOf(MatrixClient), - action: PropTypes.string.isRequired, // eg. 'Ban' - title: PropTypes.string.isRequired, // eg. 'Ban this user?' - - // Whether to display a text field for a reason - // If true, the second argument to onFinished will - // be the string entered. - askReason: PropTypes.bool, - danger: PropTypes.bool, - onFinished: PropTypes.func.isRequired, - }; +export default class ConfirmUserActionDialog extends React.Component { + private reasonField: React.RefObject = React.createRef(); static defaultProps = { danger: false, @@ -59,26 +61,22 @@ export default class ConfirmUserActionDialog extends React.Component { constructor(props) { super(props); - this._reasonField = null; + this.reasonField = null; } - onOk = () => { + public onOk = (): void => { let reason; - if (this._reasonField) { - reason = this._reasonField.value; + if (this.reasonField) { + reason = this.reasonField.current; } this.props.onFinished(true, reason); }; - onCancel = () => { + public onCancel = (): void => { this.props.onFinished(false); }; - _collectReasonField = e => { - this._reasonField = e; - }; - - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); @@ -92,7 +90,7 @@ export default class ConfirmUserActionDialog extends React.Component {
    From 5cbbb5110b01f38a95d76edd5add59d5890e3da9 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:38:10 +0100 Subject: [PATCH 11/46] Migrate ConfirmWipeDeviceDialog to TypeScript --- ...ceDialog.js => ConfirmWipeDeviceDialog.tsx} | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) rename src/components/views/dialogs/{ConfirmWipeDeviceDialog.js => ConfirmWipeDeviceDialog.tsx} (88%) diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx similarity index 88% rename from src/components/views/dialogs/ConfirmWipeDeviceDialog.js rename to src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 333e1522f1..6911c845fb 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -20,17 +20,17 @@ import {_t} from "../../../languageHandler"; import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog") -export default class ConfirmWipeDeviceDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - _onConfirm = () => { +@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog") +export default class ConfirmWipeDeviceDialog extends React.Component { + private onConfirm = (): void => { this.props.onFinished(true); }; - _onDecline = () => { + private onDecline = (): void => { this.props.onFinished(false); }; @@ -55,10 +55,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
    ); From de95f3bc01a6e506f9751387e0b2d83cb83c4d0d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:43:54 +0100 Subject: [PATCH 12/46] Migrate CreateGroupDialog to TypeScript --- ...teGroupDialog.js => CreateGroupDialog.tsx} | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) rename src/components/views/dialogs/{CreateGroupDialog.js => CreateGroupDialog.tsx} (82%) diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.tsx similarity index 82% rename from src/components/views/dialogs/CreateGroupDialog.js rename to src/components/views/dialogs/CreateGroupDialog.tsx index e6c7a67aca..60e4f5efb8 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.tsx @@ -15,44 +15,51 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.CreateGroupDialog") -export default class CreateGroupDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - state = { +interface IState { + groupName: string; + groupId: string; + groupIdError: string; + creating: boolean; + createError: Error; +} + +@replaceableComponent("views.dialogs.CreateGroupDialog") +export default class CreateGroupDialog extends React.Component { + public state = { groupName: '', groupId: '', - groupError: null, + groupIdError: '', creating: false, createError: null, }; - _onGroupNameChange = e => { + private onGroupNameChange = (e: React.FormEvent): void => { this.setState({ - groupName: e.target.value, + groupName: e.currentTarget.value, }); }; - _onGroupIdChange = e => { + private onGroupIdChange = (e: React.FormEvent): void => { this.setState({ - groupId: e.target.value, + groupId: e.currentTarget.value, }); }; - _onGroupIdBlur = e => { - this._checkGroupId(); + private onGroupIdBlur = (): void => { + this.checkGroupId(); }; - _checkGroupId(e) { + private checkGroupId() { let error = null; if (!this.state.groupId) { error = _t("Community IDs cannot be empty."); @@ -67,12 +74,12 @@ export default class CreateGroupDialog extends React.Component { return error; } - _onFormSubmit = e => { + private onFormSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (this._checkGroupId()) return; + if (this.checkGroupId()) return; - const profile = {}; + const profile: any = {}; if (this.state.groupName !== '') { profile.name = this.state.groupName; } @@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component { - +
    @@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
    @@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component { + From e39baf3e2301b8280d7072d2660a2ac69df843de Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:45:12 +0100 Subject: [PATCH 13/46] Migrate CryptoStoreTooNewDialog to TypeScript --- ...toStoreTooNewDialog.js => CryptoStoreTooNewDialog.tsx} | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) rename src/components/views/dialogs/{CryptoStoreTooNewDialog.js => CryptoStoreTooNewDialog.tsx} (94%) diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx similarity index 94% rename from src/components/views/dialogs/CryptoStoreTooNewDialog.js rename to src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 6336c635e4..2bdf732bc5 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; -export default (props) => { +interface IProps { + onFinished: (success: boolean) => void; +} + +export default (props: IProps) => { const brand = SdkConfig.get().brand; const _onLogoutClicked = () => { @@ -40,7 +44,7 @@ export default (props) => { onFinished: (doLogout) => { if (doLogout) { dis.dispatch({action: 'logout'}); - props.onFinished(); + props.onFinished(true); } }, }); From cf822d4d4c2964a05447347d18f972f594c01fc7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:53:44 +0100 Subject: [PATCH 14/46] Migrate DeactivateAccountDialog to TypeScript --- .../structures/auth/Registration.tsx | 2 +- ...tDialog.js => DeactivateAccountDialog.tsx} | 56 +++++++++++-------- 2 files changed, 35 insertions(+), 23 deletions(-) rename src/components/views/dialogs/{DeactivateAccountDialog.js => DeactivateAccountDialog.tsx} (87%) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 6feb1e34f7..de02b3eb8d 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -269,7 +269,7 @@ export default class Registration extends React.Component { ); } - private onUIAuthFinished = async (success, response, extra) => { + private onUIAuthFinished = async (success, response) => { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.tsx similarity index 87% rename from src/components/views/dialogs/DeactivateAccountDialog.js rename to src/components/views/dialogs/DeactivateAccountDialog.tsx index 4e52549d51..4e64a354bb 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; @@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv import StyledCheckbox from "../elements/StyledCheckbox"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + shouldErase: boolean; + errStr: string; + authData: any; // for UIA + authEnabled: boolean; // see usages for information + + // A few strings that are passed to InteractiveAuth for design or are displayed + // next to the InteractiveAuth component. + bodyText: string; + continueText: string; + continueKind: string; +} + @replaceableComponent("views.dialogs.DeactivateAccountDialog") -export default class DeactivateAccountDialog extends React.Component { +export default class DeactivateAccountDialog extends React.Component { constructor(props) { super(props); @@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component { continueKind: null, }; - this._initAuth(/* shouldErase= */false); + this.initAuth(/* shouldErase= */false); } - _onStagePhaseChange = (stage, phase) => { + private onStagePhaseChange = (stage: string, phase: string): void => { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."), @@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component { this.setState({bodyText, continueText, continueKind}); }; - _onUIAuthFinished = (success, result, extra) => { + private onUIAuthFinished = (success: boolean, result: Error) => { if (success) return; // great! makeRequest() will be called too. if (result === ERROR_USER_CANCELLED) { - this._onCancel(); + this.onCancel(); return; } - console.error("Error during UI Auth:", {result, extra}); + console.error("Error during UI Auth:", { result }); this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")}); }; - _onUIAuthComplete = (auth) => { + private onUIAuthComplete = (auth): void => { MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => { // Deactivation worked - logout & close this dialog Analytics.trackEvent('Account', 'Deactivate Account'); @@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component { }); }; - _onEraseFieldChange = (ev) => { + private onEraseFieldChange = (ev: React.FormEvent): void => { this.setState({ - shouldErase: ev.target.checked, + shouldErase: ev.currentTarget.checked, // Disable the auth form because we're going to have to reinitialize the auth // information. We do this because we can't modify the parameters in the UIA @@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component { }); // As mentioned above, set up for auth again to get updated UIA session info - this._initAuth(/* shouldErase= */ev.target.checked); + this.initAuth(/* shouldErase= */ev.currentTarget.checked); }; - _onCancel() { + private onCancel(): void { this.props.onFinished(false); } - _initAuth(shouldErase) { + private initAuth(shouldErase: boolean): void { MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => { // If we got here, oops. The server didn't require any auth. // Our application lifecycle will catch the error and do the logout bits. @@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component { }); } - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); let error = null; @@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component { @@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {

    {_t( "Please forget all messages I have sent when my account is deactivated " + @@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component { ); } } - -DeactivateAccountDialog.propTypes = { - onFinished: PropTypes.func.isRequired, -}; From a030c1270a844c40a8120cd41bd209ac4707c6ce Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 21:55:47 +0100 Subject: [PATCH 15/46] Migrate ErrorDialog to TypeScript --- .../{ErrorDialog.js => ErrorDialog.tsx} | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) rename src/components/views/dialogs/{ErrorDialog.js => ErrorDialog.tsx} (81%) diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.tsx similarity index 81% rename from src/components/views/dialogs/ErrorDialog.js rename to src/components/views/dialogs/ErrorDialog.tsx index 5197c68b5a..d50ec7bf36 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.tsx @@ -26,37 +26,37 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.ErrorDialog") -export default class ErrorDialog extends React.Component { - static propTypes = { - title: PropTypes.string, - description: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.string, - ]), - button: PropTypes.string, - focus: PropTypes.bool, - onFinished: PropTypes.func.isRequired, - headerImage: PropTypes.string, - }; +interface IProps { + onFinished: (success: boolean) => void; + title?: string; + description?: React.ReactNode; + button?: string; + focus?: boolean; + headerImage?: string; +} - static defaultProps = { +interface IState { + onFinished: (success: boolean) => void; +} + +@replaceableComponent("views.dialogs.ErrorDialog") +export default class ErrorDialog extends React.Component { + public static defaultProps = { focus: true, title: null, description: null, button: null, }; - onClick = () => { + private onClick = () => { this.props.onFinished(true); }; - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( Date: Mon, 14 Jun 2021 23:42:36 +0100 Subject: [PATCH 16/46] Migrate UserSettingsDialog to TypeScript --- src/components/structures/SpaceRoomView.tsx | 4 +- src/components/structures/UserMenu.tsx | 6 +- .../views/dialogs/BetaFeedbackDialog.tsx | 4 +- ...ttingsDialog.js => UserSettingsDialog.tsx} | 75 ++++++++++--------- src/components/views/right_panel/UserInfo.tsx | 4 +- .../tabs/user/HelpUserSettingsTab.tsx | 2 +- .../views/spaces/SpaceCreateMenu.tsx | 4 +- src/toasts/UnverifiedSessionToast.ts | 4 +- 8 files changed, 55 insertions(+), 48 deletions(-) rename src/components/views/dialogs/{UserSettingsDialog.js => UserSettingsDialog.tsx} (76%) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 276f4ae6ca..907a0e8873 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -59,7 +59,7 @@ import IconizedContextMenu, { } from "../views/context_menus/IconizedContextMenu"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import {BetaPill} from "../views/beta/BetaCard"; -import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog"; +import { USER_TAB } from "../views/dialogs/UserSettingsDialog"; import SettingsStore from "../../settings/SettingsStore"; import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; @@ -165,7 +165,7 @@ const SpaceInfo = ({ space }) => { const onBetaClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: USER_TAB.LABS, }); }; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 6a449cf1a2..d942c71c4a 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -26,7 +26,7 @@ import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "./ContextMenu"; -import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog"; +import { USER_TAB } from "../views/dialogs/UserSettingsDialog"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; @@ -408,12 +408,12 @@ export default class UserMenu extends React.Component { this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} + onClick={(e) => this.onSettingsOpen(e, USER_TAB.NOTIFICATIONS)} /> this.onSettingsOpen(e, USER_SECURITY_TAB)} + onClick={(e) => this.onSettingsOpen(e, USER_TAB.SECURITY)} /> = ({featureId, onFinished}) => { onFinished(false); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: USER_TAB.LABS, }); }}> { _t("To leave the beta, visit your settings.") } diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.tsx similarity index 76% rename from src/components/views/dialogs/UserSettingsDialog.js rename to src/components/views/dialogs/UserSettingsDialog.tsx index fe29b85aea..921aece7f4 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import TabbedView, {Tab} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; @@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab import {UIFeature} from "../../../settings/UIFeature"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; -export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; -export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; -export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; -export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; -export const USER_VOICE_TAB = "USER_VOICE_TAB"; -export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; -export const USER_LABS_TAB = "USER_LABS_TAB"; -export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; -export const USER_HELP_TAB = "USER_HELP_TAB"; +export enum USER_TAB { + GENERAL = "USER_GENERAL_TAB", + APPEARANCE = "USER_APPEARANCE_TAB", + FLAIR = "USER_FLAIR_TAB", + NOTIFICATIONS = "USER_NOTIFICATIONS_TAB", + PREFERENCES = "USER_PREFERENCES_TAB", + VOICE = "USER_VOICE_TAB", + SECURITY = "USER_SECURITY_TAB", + LABS = "USER_LABS_TAB", + MJOLNIR = "USER_MJOLNIR_TAB", + HELP = "USER_HELP_TAB", +} + +interface IProps { + onFinished: (success: boolean) => void; + initialTabId?: string; +} + +interface IState { + mjolnirEnabled: boolean; +} @replaceableComponent("views.dialogs.UserSettingsDialog") -export default class UserSettingsDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - initialTabId: PropTypes.string, - }; +export default class UserSettingsDialog extends React.Component { + private mjolnirWatcher: string; - constructor() { - super(); + constructor(props) { + super(props); this.state = { mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), }; } - componentDidMount(): void { - this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this)); + public componentDidMount(): void { + this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged); } - componentWillUnmount(): void { - SettingsStore.unwatchSetting(this._mjolnirWatcher); + public componentWillUnmount(): void { + SettingsStore.unwatchSetting(this.mjolnirWatcher); } - _mjolnirChanged(settingName, roomId, atLevel, newValue) { + private mjolnirChanged = (settingName, roomId, atLevel, newValue: boolean) => { // We can cheat because we know what levels a feature is tracked at, and how it is tracked this.setState({mjolnirEnabled: newValue}); } @@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( - USER_GENERAL_TAB, + USER_TAB.GENERAL, _td("General"), "mx_UserSettingsDialog_settingsIcon", , )); tabs.push(new Tab( - USER_APPEARANCE_TAB, + USER_TAB.APPEARANCE, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , )); if (SettingsStore.getValue(UIFeature.Flair)) { tabs.push(new Tab( - USER_FLAIR_TAB, + USER_TAB.FLAIR, _td("Flair"), "mx_UserSettingsDialog_flairIcon", , )); } tabs.push(new Tab( - USER_NOTIFICATIONS_TAB, + USER_TAB.NOTIFICATIONS, _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , )); tabs.push(new Tab( - USER_PREFERENCES_TAB, + USER_TAB.PREFERENCES, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , @@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component { if (SettingsStore.getValue(UIFeature.Voip)) { tabs.push(new Tab( - USER_VOICE_TAB, + USER_TAB.VOICE, _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , @@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component { } tabs.push(new Tab( - USER_SECURITY_TAB, + USER_TAB.SECURITY, _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , @@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component { || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) ) { tabs.push(new Tab( - USER_LABS_TAB, + USER_TAB.LABS, _td("Labs"), "mx_UserSettingsDialog_labsIcon", , @@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component { } if (this.state.mjolnirEnabled) { tabs.push(new Tab( - USER_MJOLNIR_TAB, + USER_TAB.MJOLNIR, _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , )); } tabs.push(new Tab( - USER_HELP_TAB, + USER_TAB.HELP, _td("Help & About"), "mx_UserSettingsDialog_helpIcon", - , + this.props.onFinished(true)} />, )); return tabs; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d6c97f9cf2..ce5a96c8a3 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; import { Action } from "../../../dispatcher/actions"; -import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; +import { USER_TAB } from "../dialogs/UserSettingsDialog"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import { E2EStatus } from "../../../utils/ShieldUtils"; @@ -1381,7 +1381,7 @@ const BasicUserInfo: React.FC<{ { dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_SECURITY_TAB, + initialTabId: USER_TAB.SECURITY, }); }}> { _t("Edit devices") } diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 3fa0be478c..beff033001 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -32,7 +32,7 @@ import * as ContextMenu from "../../../../structures/ContextMenu"; import { toRightOf } from "../../../../structures/ContextMenu"; interface IProps { - closeSettingsFn: () => {}; + closeSettingsFn: () => void; } interface IState { diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 0ebf511018..29be03eaa4 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -29,7 +29,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import {BetaPill} from "../beta/BetaCard"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog"; +import { USER_TAB } from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; @@ -222,7 +222,7 @@ const SpaceCreateMenu = ({ onFinished }) => { onFinished(); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: USER_TAB.LABS, }); }} /> { body } diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index c856d39d1f..8e3fa7c8a7 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -21,7 +21,7 @@ import DeviceListener from '../DeviceListener'; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import { Action } from "../dispatcher/actions"; -import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog"; +import { USER_TAB } from "../components/views/dialogs/UserSettingsDialog"; function toastKey(deviceId: string) { return "unverified_session_" + deviceId; @@ -34,7 +34,7 @@ export const showToast = async (deviceId: string) => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_SECURITY_TAB, + initialTabId: USER_TAB.SECURITY, }); }; From 75151b7a6c8bc690fe38c145ab77d27000438ad5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 23:50:41 +0100 Subject: [PATCH 17/46] Migrate TermsDialog to TypeScript --- .../{TermsDialog.js => TermsDialog.tsx} | 100 ++++++++++-------- 1 file changed, 53 insertions(+), 47 deletions(-) rename src/components/views/dialogs/{TermsDialog.js => TermsDialog.tsx} (72%) diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.tsx similarity index 72% rename from src/components/views/dialogs/TermsDialog.js rename to src/components/views/dialogs/TermsDialog.tsx index e8625ec6cb..ace5316323 100644 --- a/src/components/views/dialogs/TermsDialog.js +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -16,22 +16,21 @@ limitations under the License. import url from 'url'; import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t, pickBestLanguage } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; +import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; -class TermsCheckbox extends React.PureComponent { - static propTypes = { - onChange: PropTypes.func.isRequired, - url: PropTypes.string.isRequired, - checked: PropTypes.bool.isRequired, - } +interface ITermsCheckboxProps { + onChange: (url: string, checked: boolean) => void; + url: string; + checked: boolean; +} - onChange = (ev) => { - this.props.onChange(this.props.url, ev.target.checked); +class TermsCheckbox extends React.PureComponent { + private onChange = (ev: React.FormEvent): void => { + this.props.onChange(this.props.url, ev.currentTarget.checked); } render() { @@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent { } } +interface ITermsDialogProps { + /** + * Array of [Service, policies] pairs, where policies is the response from the + * /terms endpoint for that service + */ + policiesAndServicePairs: any[], + + /** + * urls that the user has already agreed to + */ + agreedUrls?: string[], + + /** + * Called with: + * * success {bool} True if the user accepted any douments, false if cancelled + * * agreedUrls {string[]} List of agreed URLs + */ + onFinished: (success: boolean, agreedUrls?: string[]) => void, +} + +interface IState { + agreedUrls: any; +} + @replaceableComponent("views.dialogs.TermsDialog") -export default class TermsDialog extends React.PureComponent { - static propTypes = { - /** - * Array of [Service, policies] pairs, where policies is the response from the - * /terms endpoint for that service - */ - policiesAndServicePairs: PropTypes.array.isRequired, - - /** - * urls that the user has already agreed to - */ - agreedUrls: PropTypes.arrayOf(PropTypes.string), - - /** - * Called with: - * * success {bool} True if the user accepted any douments, false if cancelled - * * agreedUrls {string[]} List of agreed URLs - */ - onFinished: PropTypes.func.isRequired, - } - +export default class TermsDialog extends React.PureComponent { constructor(props) { - super(); + super(props); this.state = { // url -> boolean agreedUrls: {}, @@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent { } } - _onCancelClick = () => { + private onCancelClick = (): void => { this.props.onFinished(false); } - _onNextClick = () => { + private onNextClick = (): void => { this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url])); } - _nameForServiceType(serviceType, host) { + private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element { switch (serviceType) { case SERVICE_TYPES.IS: return

    {_t("Identity Server")}
    ({host})
    ; @@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent { } } - _summaryForServiceType(serviceType) { + private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element { switch (serviceType) { case SERVICE_TYPES.IS: return
    @@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent { } } - _onTermsCheckboxChange = (url, checked) => { + private onTermsCheckboxChange = (url: string, checked: boolean) => { this.setState({ agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }), }); } - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent { let serviceName; let summary; if (i === 0) { - serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host); - summary = this._summaryForServiceType( + serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host); + summary = this.summaryForServiceType( policiesAndService.service.serviceType, ); } @@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent { rows.push( {serviceName} {summary} - {termDoc[termsLang].name} - - + + {termDoc[termsLang].name} + + + + ); @@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent { return ( From b2f20e052df53cd1a992ab0621a9d723f3891336 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 23:51:38 +0100 Subject: [PATCH 18/46] remove unused import --- src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 6911c845fb..d95b1fe358 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; From 07bdaeaf704b02569edfc3c6a33de75923bdbee2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 14 Jun 2021 23:55:14 +0100 Subject: [PATCH 19/46] Fix mjolnir private chat enum --- src/mjolnir/Mjolnir.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mjolnir/Mjolnir.ts b/src/mjolnir/Mjolnir.ts index 891438bbb9..71d63f650c 100644 --- a/src/mjolnir/Mjolnir.ts +++ b/src/mjolnir/Mjolnir.ts @@ -20,6 +20,7 @@ import SettingsStore from "../settings/SettingsStore"; import {_t} from "../languageHandler"; import dis from "../dispatcher/dispatcher"; import {SettingLevel} from "../settings/SettingLevel"; +import { Preset } from "../createRoom"; // TODO: Move this and related files to the js-sdk or something once finalized. @@ -86,7 +87,7 @@ export class Mjolnir { const resp = await MatrixClientPeg.get().createRoom({ name: _t("My Ban List"), topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), - preset: "private_chat", + preset: Preset.PrivateChat, }); personalRoomId = resp['room_id']; await SettingsStore.setValue( From 2e6dab0bcd93cfc6e8362423218372767572ae63 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 15 Jun 2021 00:01:05 +0100 Subject: [PATCH 20/46] change parameter to use preset enum --- src/components/views/dialogs/CreateRoomDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index cce6b6c34c..614ed4f645 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -72,7 +72,7 @@ export default class CreateRoomDialog extends React.Component { canChangeEncryption: true, }; - MatrixClientPeg.get().doesServerForceEncryptionForPreset("private") + MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat) .then(isForced => this.setState({ canChangeEncryption: !isForced })); } From b3912dc5b8b499528a2316af790b317a8d5df4ee Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 15 Jun 2021 16:16:42 +0100 Subject: [PATCH 21/46] Upgrade matrix-js-sdk to 12.0.0-rc.1 --- package.json | 4 ++-- yarn.lock | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d8c26098ca..7ac73a60e4 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "12.0.0-rc.1", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -139,12 +139,12 @@ "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/parser": "^4.14.0", + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "chokidar": "^3.5.1", "concurrently": "^5.3.0", "enzyme": "^3.11.0", - "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "eslint": "7.18.0", "eslint-config-matrix-org": "^0.2.0", "eslint-plugin-babel": "^5.3.1", diff --git a/yarn.lock b/yarn.lock index 7c232d2aa1..14cd11d769 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5711,9 +5711,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "11.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2" +matrix-js-sdk@12.0.0-rc.1: + version "12.0.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0-rc.1.tgz#b94a72f0549f3000763efb8c7b6fa1f8808e56f6" + integrity sha512-bzozc4w9dF6Dl8xXXLXMpe3FqL/ncczKdB9Y8dL1mPaujVrmLWAai+BYmC9/c4SIw+1zUap9P5W16ej3z7prig== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From cdb9d3a41be4c3252781a50a1913b6db4223bb7d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 15 Jun 2021 16:22:33 +0100 Subject: [PATCH 22/46] Prepare changelog for v3.24.0-rc.1 --- CHANGELOG.md | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c9530941..a14a0f308e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,121 @@ +Changes in [3.24.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0-rc.1) (2021-06-15) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0...v3.24.0-rc.1) + + * Upgrade to JS SDK 12.0.0-rc.1 + * Translations update from Weblate + [\#6192](https://github.com/matrix-org/matrix-react-sdk/pull/6192) + * Disable comment-on-alert for PR coming from a fork + [\#6189](https://github.com/matrix-org/matrix-react-sdk/pull/6189) + * Add JS benchmark tracking in CI + [\#6177](https://github.com/matrix-org/matrix-react-sdk/pull/6177) + * Upgrade matrix-react-test-utils for React 17 peer deps + [\#6187](https://github.com/matrix-org/matrix-react-sdk/pull/6187) + * Fix display name overlaps on the IRC layout + [\#6186](https://github.com/matrix-org/matrix-react-sdk/pull/6186) + * Small fixes to the spaces experience + [\#6184](https://github.com/matrix-org/matrix-react-sdk/pull/6184) + * Add footer and privacy note to the start dm dialog + [\#6111](https://github.com/matrix-org/matrix-react-sdk/pull/6111) + * Format mxids when disambiguation needed + [\#5880](https://github.com/matrix-org/matrix-react-sdk/pull/5880) + * Move various createRoom types to the js-sdk + [\#6183](https://github.com/matrix-org/matrix-react-sdk/pull/6183) + * Fix HTML tag for Event Tile when not rendered in a list + [\#6175](https://github.com/matrix-org/matrix-react-sdk/pull/6175) + * Remove legacy polyfills and unused dependencies + [\#6176](https://github.com/matrix-org/matrix-react-sdk/pull/6176) + * Fix buggy hovering/selecting of event tiles + [\#6173](https://github.com/matrix-org/matrix-react-sdk/pull/6173) + * Add room intro warning when e2ee is not enabled + [\#5929](https://github.com/matrix-org/matrix-react-sdk/pull/5929) + * Migrate end to end tests to GitHub actions + [\#6156](https://github.com/matrix-org/matrix-react-sdk/pull/6156) + * Fix expanding last collapsed sticky session when zoomed in + [\#6171](https://github.com/matrix-org/matrix-react-sdk/pull/6171) + * ⚛️ Upgrade to React@17 + [\#6165](https://github.com/matrix-org/matrix-react-sdk/pull/6165) + * Revert refreshStickyHeaders optimisations + [\#6168](https://github.com/matrix-org/matrix-react-sdk/pull/6168) + * Add logging for which rooms calls are in + [\#6170](https://github.com/matrix-org/matrix-react-sdk/pull/6170) + * Restore read receipt animation from event to event + [\#6169](https://github.com/matrix-org/matrix-react-sdk/pull/6169) + * Restore copy button icon when sharing permalink + [\#6166](https://github.com/matrix-org/matrix-react-sdk/pull/6166) + * Restore Page Up/Down key bindings when focusing the composer + [\#6167](https://github.com/matrix-org/matrix-react-sdk/pull/6167) + * Timeline rendering optimizations + [\#6143](https://github.com/matrix-org/matrix-react-sdk/pull/6143) + * Bump css-what from 5.0.0 to 5.0.1 + [\#6164](https://github.com/matrix-org/matrix-react-sdk/pull/6164) + * Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests + [\#6145](https://github.com/matrix-org/matrix-react-sdk/pull/6145) + * Bump trim-newlines from 3.0.0 to 3.0.1 + [\#6163](https://github.com/matrix-org/matrix-react-sdk/pull/6163) + * Fix upgrade to element home button in top left menu + [\#6162](https://github.com/matrix-org/matrix-react-sdk/pull/6162) + * Fix unpinning of pinned messages and panel empty state + [\#6140](https://github.com/matrix-org/matrix-react-sdk/pull/6140) + * Better handling for widgets that fail to load + [\#6161](https://github.com/matrix-org/matrix-react-sdk/pull/6161) + * Improved forwarding UI + [\#5999](https://github.com/matrix-org/matrix-react-sdk/pull/5999) + * Fixes for sharing room links + [\#6118](https://github.com/matrix-org/matrix-react-sdk/pull/6118) + * Fix setting watchers + [\#6160](https://github.com/matrix-org/matrix-react-sdk/pull/6160) + * Fix Stickerpicker context menu + [\#6152](https://github.com/matrix-org/matrix-react-sdk/pull/6152) + * Add warning to private space creation flow + [\#6155](https://github.com/matrix-org/matrix-react-sdk/pull/6155) + * Add prop to alwaysShowTimestamps on TimelinePanel + [\#6159](https://github.com/matrix-org/matrix-react-sdk/pull/6159) + * Fix notif panel timestamp padding + [\#6157](https://github.com/matrix-org/matrix-react-sdk/pull/6157) + * Fixes and refactoring for the ImageView + [\#6149](https://github.com/matrix-org/matrix-react-sdk/pull/6149) + * Fix timestamps + [\#6148](https://github.com/matrix-org/matrix-react-sdk/pull/6148) + * Make it easier to pan images in the lightbox + [\#6147](https://github.com/matrix-org/matrix-react-sdk/pull/6147) + * Fix scroll token for EventTile and EventListSummary node type + [\#6154](https://github.com/matrix-org/matrix-react-sdk/pull/6154) + * Convert bunch of things to Typescript + [\#6153](https://github.com/matrix-org/matrix-react-sdk/pull/6153) + * Lint the typescript tests + [\#6142](https://github.com/matrix-org/matrix-react-sdk/pull/6142) + * Fix jumping to bottom without a highlighted event + [\#6146](https://github.com/matrix-org/matrix-react-sdk/pull/6146) + * Repair event status position in timeline + [\#6141](https://github.com/matrix-org/matrix-react-sdk/pull/6141) + * Adapt for js-sdk MatrixClient conversion to TS + [\#6132](https://github.com/matrix-org/matrix-react-sdk/pull/6132) + * Improve pinned messages in Labs + [\#6096](https://github.com/matrix-org/matrix-react-sdk/pull/6096) + * Map phone number lookup results to their native rooms + [\#6136](https://github.com/matrix-org/matrix-react-sdk/pull/6136) + * Fix mx_Event containment rules and empty read avatar row + [\#6138](https://github.com/matrix-org/matrix-react-sdk/pull/6138) + * Improve switch room rendering + [\#6079](https://github.com/matrix-org/matrix-react-sdk/pull/6079) + * Add CSS containment rules for shorter reflow operations + [\#6127](https://github.com/matrix-org/matrix-react-sdk/pull/6127) + * ignore hash/fragment when de-duplicating links for url previews + [\#6135](https://github.com/matrix-org/matrix-react-sdk/pull/6135) + * Clicking jump to bottom resets room hash + [\#5823](https://github.com/matrix-org/matrix-react-sdk/pull/5823) + * Use passive option for scroll handlers + [\#6113](https://github.com/matrix-org/matrix-react-sdk/pull/6113) + * Optimise memberSort performance for large list + [\#6130](https://github.com/matrix-org/matrix-react-sdk/pull/6130) + * Tweak event border radius to match action bar + [\#6133](https://github.com/matrix-org/matrix-react-sdk/pull/6133) + * Log when we ignore a second call in a room + [\#6131](https://github.com/matrix-org/matrix-react-sdk/pull/6131) + * Performance monitoring measurements + [\#6041](https://github.com/matrix-org/matrix-react-sdk/pull/6041) + Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0) From 2eb7d35ea16ce593d7e8fd666327d5a2b2bc495c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 15 Jun 2021 16:22:34 +0100 Subject: [PATCH 23/46] v3.24.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7ac73a60e4..644793e265 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.23.0", + "version": "3.24.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -197,5 +197,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From ea46df0d4841c8c92ed876bae7f2faa98e14da99 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 7 Jun 2021 20:19:16 -0600 Subject: [PATCH 24/46] Partially restore immutable event objects at the rendering layer This is primarily to fix some extremely rare edge cases in local echo, but also restores the accuracy of some comments in the stack regarding immutable event objects (which were made mutable many years ago). This shouldn't have any impact on the daily usage of the app, only adding a measured 0ms of latency to the stack. --- src/components/views/messages/TextualBody.js | 1 + src/components/views/rooms/EventTile.tsx | 124 +++++++++++-------- 2 files changed, 76 insertions(+), 49 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 3adfea6ee6..00e7d3d301 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -262,6 +262,7 @@ export default class TextualBody extends React.Component { // exploit that events are immutable :) return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || + nextProps.mxEvent !== this.props.mxEvent || nextProps.highlights !== this.props.highlights || nextProps.replacingEventId !== this.props.replacingEventId || nextProps.highlightLink !== this.props.highlightLink || diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 85b9cac2c4..d1b596a709 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -298,6 +298,9 @@ interface IState { // The Relations model from the JS SDK for reactions to `mxEvent` reactions: Relations; + // Our snapshotted/local copy of the props.mxEvent, for local echo reasons + mxEvent: MatrixEvent; + hover: boolean; } @@ -332,6 +335,8 @@ export default class EventTile extends React.Component { // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), + mxEvent: this.mxEvent.getSnapshotCopy(), // snapshot up front to verify it all works + hover: false, }; @@ -348,6 +353,10 @@ export default class EventTile extends React.Component { this.ref = React.createRef(); } + private get mxEvent(): MatrixEvent { + return this.state?.mxEvent ?? this.props.mxEvent; + } + /** * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending' * or 'sent' receipt, for example. @@ -356,16 +365,16 @@ export default class EventTile extends React.Component { private get isEligibleForSpecialReceipt() { // First, if there are other read receipts then just short-circuit this. if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; - if (!this.props.mxEvent) return false; + if (!this.mxEvent) return false; // Sanity check (should never happen, but we shouldn't explode if it does) - const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + const room = this.context.getRoom(this.mxEvent.getRoomId()); if (!room) return false; // Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for // special read receipts. const myUserId = MatrixClientPeg.get().getUserId(); - if (this.props.mxEvent.getSender() !== myUserId) return false; + if (this.mxEvent.getSender() !== myUserId) return false; // Finally, determine if the type is relevant to the user. This notably excludes state // events and pretty much anything that can't be sent by the composer as a message. For @@ -376,7 +385,7 @@ export default class EventTile extends React.Component { EventType.RoomMessage, EventType.RoomMessageEncrypted, ]; - if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false; + if (!simpleSendableEvents.includes(this.mxEvent.getType())) return false; // Default case return true; @@ -418,7 +427,7 @@ export default class EventTile extends React.Component { // TODO: [REACT-WARNING] Move into constructor // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(this.mxEvent); } componentDidMount() { @@ -448,11 +457,21 @@ export default class EventTile extends React.Component { } shouldComponentUpdate(nextProps, nextState) { + // If the echo changed meaningfully, update. + if (!this.state.mxEvent?.isEquivalentTo(nextProps.mxEvent)) { + return true; + } + if (objectHasDiff(this.state, nextState)) { return true; } - return !this.propsEqual(this.props, nextProps); + if (!this.propsEqual(this.props, nextProps)) { + return true; + } + + // Always assume there's no significant change. + return false; } componentWillUnmount() { @@ -473,11 +492,18 @@ export default class EventTile extends React.Component { this.context.on("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = true; } + + // Update the state again if the snapshot needs updating. Note that this will fire + // a second state update to re-render child components, which ultimately calls didUpdate + // again, so we break that loop with a reference check first (faster than comparing events). + if (this.state.mxEvent === prevState.mxEvent && !this.state?.mxEvent.isEquivalentTo(this.props.mxEvent)) { + this.setState({mxEvent: this.props.mxEvent.getSnapshotCopy()}); + } } private onRoomReceipt = (ev, room) => { // ignore events for other rooms - const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const tileRoom = MatrixClientPeg.get().getRoom(this.mxEvent.getRoomId()); if (room !== tileRoom) return; if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) { @@ -501,19 +527,19 @@ export default class EventTile extends React.Component { // we need to re-verify the sending device. // (we call onHeightChanged in verifyEvent to handle the case where decryption // has caused a change in size of the event tile) - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(this.mxEvent); this.forceUpdate(); }; private onDeviceVerificationChanged = (userId, device) => { - if (userId === this.props.mxEvent.getSender()) { - this.verifyEvent(this.props.mxEvent); + if (userId === this.mxEvent.getSender()) { + this.verifyEvent(this.mxEvent); } }; private onUserVerificationChanged = (userId, _trustStatus) => { - if (userId === this.props.mxEvent.getSender()) { - this.verifyEvent(this.props.mxEvent); + if (userId === this.mxEvent.getSender()) { + this.verifyEvent(this.mxEvent); } }; @@ -620,11 +646,11 @@ export default class EventTile extends React.Component { } shouldHighlight() { - const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); + const actions = this.context.getPushActionsForEvent(this.mxEvent.replacingEvent() || this.mxEvent); if (!actions || !actions.tweaks) { return false; } // don't show self-highlights from another of our clients - if (this.props.mxEvent.getSender() === this.context.credentials.userId) { + if (this.mxEvent.getSender() === this.context.credentials.userId) { return false; } @@ -639,7 +665,7 @@ export default class EventTile extends React.Component { getReadAvatars() { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { - return ; + return ; } // return early if there are no read receipts @@ -726,7 +752,7 @@ export default class EventTile extends React.Component { } onSenderProfileClick = event => { - const mxEvent = this.props.mxEvent; + const mxEvent = this.mxEvent; dis.dispatch({ action: 'insert_mention', user_id: mxEvent.getSender(), @@ -743,7 +769,7 @@ export default class EventTile extends React.Component { // Cancel any outgoing key request for this event and resend it. If a response // is received for the request with the required keys, the event could be // decrypted successfully. - this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); + this.context.cancelAndResendEventRoomKeyRequest(this.mxEvent); }; onPermalinkClicked = e => { @@ -752,14 +778,14 @@ export default class EventTile extends React.Component { e.preventDefault(); dis.dispatch({ action: 'view_room', - event_id: this.props.mxEvent.getId(), + event_id: this.mxEvent.getId(), highlighted: true, - room_id: this.props.mxEvent.getRoomId(), + room_id: this.mxEvent.getRoomId(), }); }; private renderE2EPadlock() { - const ev = this.props.mxEvent; + const ev = this.mxEvent; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { @@ -818,7 +844,7 @@ export default class EventTile extends React.Component { ) { return null; } - const eventId = this.props.mxEvent.getId(); + const eventId = this.mxEvent.getId(); return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); }; @@ -837,13 +863,13 @@ export default class EventTile extends React.Component { const SenderProfile = sdk.getComponent('messages.SenderProfile'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - //console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + //console.info("EventTile showUrlPreview for %s is %s", this.mxEvent.getId(), this.props.showUrlPreview); - const content = this.props.mxEvent.getContent(); + const content = this.mxEvent.getContent(); const msgtype = content.msgtype; - const eventType = this.props.mxEvent.getType(); + const eventType = this.mxEvent.getType(); - let tileHandler = getHandlerTile(this.props.mxEvent); + let tileHandler = getHandlerTile(this.mxEvent); // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || @@ -860,7 +886,7 @@ export default class EventTile extends React.Component { // source tile when there's no regular tile for an event and also for // replace relations (which otherwise would display as a confusing // duplicate of the thing they are replacing). - if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) { + if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.mxEvent)) { tileHandler = "messages.ViewSourceEvent"; // Reuse info message avatar and sender profile styling isInfoMessage = true; @@ -879,8 +905,8 @@ export default class EventTile extends React.Component { const EventTileType = sdk.getComponent(tileHandler); const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); - const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; - const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); + const isRedacted = isMessageEvent(this.mxEvent) && this.props.isRedacted; + const isEncryptionFailure = this.mxEvent.isDecryptionFailure(); const isEditing = !!this.props.editState; const classes = classNames({ @@ -910,14 +936,14 @@ export default class EventTile extends React.Component { let permalink = "#"; if (this.props.permalinkCreator) { - permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); + permalink = this.props.permalinkCreator.forEvent(this.mxEvent.getId()); } // we can't use local echoes as scroll tokens, because their event IDs change. // Local echos have a send "status". - const scrollToken = this.props.mxEvent.status + const scrollToken = this.mxEvent.status ? undefined - : this.props.mxEvent.getId(); + : this.mxEvent.getId(); let avatar; let sender; @@ -947,15 +973,15 @@ export default class EventTile extends React.Component { needsSenderProfile = true; } - if (this.props.mxEvent.sender && avatarSize) { + if (this.mxEvent.sender && avatarSize) { let member; // set member to receiver (target) if it is a 3PID invite // so that the correct avatar is shown as the text is // `$target accepted the invitation for $email` - if (this.props.mxEvent.getContent().third_party_invite) { - member = this.props.mxEvent.target; + if (this.mxEvent.getContent().third_party_invite) { + member = this.mxEvent.target; } else { - member = this.props.mxEvent.sender; + member = this.mxEvent.sender; } avatar = (
    @@ -970,17 +996,17 @@ export default class EventTile extends React.Component { if (needsSenderProfile) { if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { sender = ; } else { - sender = ; + sender = ; } } const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); const actionBar = !isEditing ? { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const showTimestamp = this.props.mxEvent.getTs() && + const showTimestamp = this.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); const timestamp = showTimestamp ? - : null; + : null; const keyRequestHelpText =
    @@ -1031,7 +1057,7 @@ export default class EventTile extends React.Component { if (!isRedacted) { const ReactionsRow = sdk.getComponent('messages.ReactionsRow'); reactionsRow = ; } @@ -1039,7 +1065,7 @@ export default class EventTile extends React.Component { const linkedTimestamp = { timestamp } ; @@ -1058,7 +1084,7 @@ export default class EventTile extends React.Component { switch (this.props.tileShape) { case 'notif': { - const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + const room = this.context.getRoom(this.mxEvent.getRoomId()); return React.createElement(this.props.as || "li", { "className": classes, "aria-live": ariaLive, @@ -1080,7 +1106,7 @@ export default class EventTile extends React.Component {
    ,
    { }, [
    { let thread; if (this.props.tileShape === 'reply_preview') { thread = ReplyThread.makeThread( - this.props.mxEvent, + this.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, @@ -1148,7 +1174,7 @@ export default class EventTile extends React.Component { { groupPadlock } { thread } { } default: { const thread = ReplyThread.makeThread( - this.props.mxEvent, + this.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, @@ -1188,7 +1214,7 @@ export default class EventTile extends React.Component { { groupPadlock } { thread } Date: Tue, 15 Jun 2021 17:24:56 -0600 Subject: [PATCH 25/46] Update MSC number references for voice messages as per https://github.com/matrix-org/matrix-doc/pull/3245 --- src/components/views/messages/MVoiceOrAudioBody.tsx | 4 +++- .../views/rooms/VoiceRecordComposerTile.tsx | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx index 0cebcf3440..6d26ef3dcb 100644 --- a/src/components/views/messages/MVoiceOrAudioBody.tsx +++ b/src/components/views/messages/MVoiceOrAudioBody.tsx @@ -28,7 +28,9 @@ interface IProps { @replaceableComponent("views.messages.MVoiceOrAudioBody") export default class MVoiceOrAudioBody extends React.PureComponent { public render() { - const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']; + // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245 + const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'] + || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice']; const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages"); if (isVoiceMessage && voiceMessagesEnabled) { return ; diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 2102071bf3..20d8c9c5d4 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -77,7 +77,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), }, - "org.matrix.msc2516.voice": {}, // No content, this is a rendering hint + "org.matrix.msc3245.voice": {}, // No content, this is a rendering hint }); await this.disposeRecording(); } From 7c6161d83ad60df8e332656ca0389ce90797ec3e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 16 Jun 2021 18:00:06 +0100 Subject: [PATCH 26/46] Stop requesting null next replies from the server A recent change (47e007e08f9bedaf47cf59a63c9bd04219195d76) introduced a regression where we failed to check whether a reply thread has a next reply. This meant that we would end up sending `/context/undefined` requests to the server for every reply thread on every room view. Fixes https://github.com/vector-im/element-web/issues/17563 Regressed by https://github.com/matrix-org/matrix-react-sdk/pull/6079 --- src/components/views/elements/ReplyThread.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 81ed360b17..a9b24a306b 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -297,6 +297,7 @@ export default class ReplyThread extends React.Component { } async getEvent(eventId) { + if (!eventId) return null; const event = this.room.findEventById(eventId); if (event) return event; From 0367b5bcced808ce75e19f0a7669f3ab5b61524c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 17 Jun 2021 08:45:09 +0100 Subject: [PATCH 27/46] remove stray bullet point in reply preview --- src/components/views/elements/ReplyThread.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 81ed360b17..0f6aef37eb 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -392,6 +392,7 @@ export default class ReplyThread extends React.Component { alwaysShowTimestamps={this.props.alwaysShowTimestamps} enableFlair={SettingsStore.getValue(UIFeature.Flair)} replacingEventId={ev.replacingEventId()} + as="div" /> ; }); From 1597b2a971bb3e0194c4a8cf8bb5530923f17438 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 16 Jun 2021 10:01:23 +0100 Subject: [PATCH 28/46] Keep composer reply when scrolling away from a highlighted event --- src/components/structures/RoomView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fe90d2f873..c0ce6ba4c9 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -701,6 +701,7 @@ export default class RoomView extends React.Component { room_id: this.state.room.roomId, event_id: this.state.initialEventId, highlighted: false, + replyingToEvent: this.state.replyToEvent, }); } } From a687391b98d638e69983e5d814bcb73bfe52a381 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Jun 2021 14:21:01 -0600 Subject: [PATCH 29/46] Switch order --- src/components/views/messages/TextualBody.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 00e7d3d301..cb6a4f14b6 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -261,8 +261,8 @@ export default class TextualBody extends React.Component { //console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); // exploit that events are immutable :) - return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || - nextProps.mxEvent !== this.props.mxEvent || + return (nextProps.mxEvent !== this.props.mxEvent || + nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || nextProps.highlights !== this.props.highlights || nextProps.replacingEventId !== this.props.replacingEventId || nextProps.highlightLink !== this.props.highlightLink || From 98e0200b4a17fc80b1865ae25bdfec80de0bc161 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Jun 2021 14:21:50 -0600 Subject: [PATCH 30/46] Function name --- src/components/views/rooms/EventTile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index d1b596a709..4dd8fff636 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -335,7 +335,7 @@ export default class EventTile extends React.Component { // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), - mxEvent: this.mxEvent.getSnapshotCopy(), // snapshot up front to verify it all works + mxEvent: this.mxEvent.toSnapshot(), // snapshot up front to verify it all works hover: false, }; @@ -497,7 +497,7 @@ export default class EventTile extends React.Component { // a second state update to re-render child components, which ultimately calls didUpdate // again, so we break that loop with a reference check first (faster than comparing events). if (this.state.mxEvent === prevState.mxEvent && !this.state?.mxEvent.isEquivalentTo(this.props.mxEvent)) { - this.setState({mxEvent: this.props.mxEvent.getSnapshotCopy()}); + this.setState({mxEvent: this.props.mxEvent.toSnapshot()}); } } From d22617c422ebabc88435bb8a8421b0b347d5e7c0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 18 Jun 2021 12:44:15 +0100 Subject: [PATCH 31/46] More specific type definition and adhering to code style better --- src/components/structures/SpaceRoomView.tsx | 4 +- src/components/structures/UserMenu.tsx | 6 +-- .../structures/auth/Registration.tsx | 2 +- .../views/dialogs/BetaFeedbackDialog.tsx | 4 +- .../views/dialogs/ConfirmUserActionDialog.tsx | 6 --- .../views/dialogs/DeactivateAccountDialog.tsx | 2 +- .../views/dialogs/UserSettingsDialog.tsx | 46 +++++++++---------- .../security/CreateCrossSigningDialog.tsx | 6 +-- .../security/SetupEncryptionDialog.tsx | 10 ++-- src/components/views/right_panel/UserInfo.tsx | 4 +- .../views/spaces/SpaceCreateMenu.tsx | 4 +- src/stores/SetupEncryptionStore.ts | 40 ++++++++-------- src/toasts/UnverifiedSessionToast.ts | 4 +- 13 files changed, 66 insertions(+), 72 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 36da384e69..aad770888b 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -59,7 +59,7 @@ import IconizedContextMenu, { } from "../views/context_menus/IconizedContextMenu"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import {BetaPill} from "../views/beta/BetaCard"; -import { USER_TAB } from "../views/dialogs/UserSettingsDialog"; +import { UserTab } from "../views/dialogs/UserSettingsDialog"; import SettingsStore from "../../settings/SettingsStore"; import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; @@ -166,7 +166,7 @@ const SpaceInfo = ({ space }) => { const onBetaClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_TAB.LABS, + initialTabId: UserTab.Labs, }); }; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index d942c71c4a..3cf0dc5f84 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -26,7 +26,7 @@ import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "./ContextMenu"; -import { USER_TAB } from "../views/dialogs/UserSettingsDialog"; +import { UserTab } from "../views/dialogs/UserSettingsDialog"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; @@ -408,12 +408,12 @@ export default class UserMenu extends React.Component { this.onSettingsOpen(e, USER_TAB.NOTIFICATIONS)} + onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)} /> this.onSettingsOpen(e, USER_TAB.SECURITY)} + onClick={(e) => this.onSettingsOpen(e, UserTab.Security)} /> { ); } - private onUIAuthFinished = async (success, response) => { + private onUIAuthFinished = async (success: boolean, response: any) => { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 85fe81ef4e..1c2dab4bfc 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -29,7 +29,7 @@ import InfoDialog from "./InfoDialog"; import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import { USER_TAB } from "./UserSettingsDialog"; +import { UserTab } from "./UserSettingsDialog"; interface IProps extends IDialogProps { featureId: string; @@ -70,7 +70,7 @@ const BetaFeedbackDialog: React.FC = ({featureId, onFinished}) => { onFinished(false); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_TAB.LABS, + initialTabId: UserTab.Labs, }); }}> { _t("To leave the beta, visit your settings.") } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 13be70dbab..c91dcba95c 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -58,12 +58,6 @@ export default class ConfirmUserActionDialog extends React.Component { askReason: false, }; - constructor(props) { - super(props); - - this.reasonField = null; - } - public onOk = (): void => { let reason; if (this.reasonField) { diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index 4e64a354bb..cf88802340 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -115,7 +115,7 @@ export default class DeactivateAccountDialog extends React.Component { + private onUIAuthComplete = (auth: any): void => { MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => { // Deactivation worked - logout & close this dialog Analytics.trackEvent('Account', 'Deactivate Account'); diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 921aece7f4..1a62a4ff22 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -19,7 +19,7 @@ import React from 'react'; import TabbedView, {Tab} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; -import SettingsStore from "../../../settings/SettingsStore"; +import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; @@ -34,17 +34,17 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab import {UIFeature} from "../../../settings/UIFeature"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -export enum USER_TAB { - GENERAL = "USER_GENERAL_TAB", - APPEARANCE = "USER_APPEARANCE_TAB", - FLAIR = "USER_FLAIR_TAB", - NOTIFICATIONS = "USER_NOTIFICATIONS_TAB", - PREFERENCES = "USER_PREFERENCES_TAB", - VOICE = "USER_VOICE_TAB", - SECURITY = "USER_SECURITY_TAB", - LABS = "USER_LABS_TAB", - MJOLNIR = "USER_MJOLNIR_TAB", - HELP = "USER_HELP_TAB", +export enum UserTab { + General = "USER_GENERAL_TAB", + Appearance = "USER_APPEARANCE_TAB", + Flair = "USER_FLAIR_TAB", + Notifications = "USER_NOTIFICATIONS_TAB", + Preferences = "USER_PREFERENCES_TAB", + Voice = "USER_VOICE_TAB", + Security = "USER_SECURITY_TAB", + Labs = "USER_LABS_TAB", + Mjolnir = "USER_MJOLNIR_TAB", + Help = "USER_HELP_TAB", } interface IProps { @@ -76,7 +76,7 @@ export default class UserSettingsDialog extends React.Component SettingsStore.unwatchSetting(this.mjolnirWatcher); } - private mjolnirChanged = (settingName, roomId, atLevel, newValue: boolean) => { + private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { // We can cheat because we know what levels a feature is tracked at, and how it is tracked this.setState({mjolnirEnabled: newValue}); } @@ -85,33 +85,33 @@ export default class UserSettingsDialog extends React.Component const tabs = []; tabs.push(new Tab( - USER_TAB.GENERAL, + UserTab.General, _td("General"), "mx_UserSettingsDialog_settingsIcon", , )); tabs.push(new Tab( - USER_TAB.APPEARANCE, + UserTab.Appearance, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , )); if (SettingsStore.getValue(UIFeature.Flair)) { tabs.push(new Tab( - USER_TAB.FLAIR, + UserTab.Flair, _td("Flair"), "mx_UserSettingsDialog_flairIcon", , )); } tabs.push(new Tab( - USER_TAB.NOTIFICATIONS, + UserTab.Notifications, _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , )); tabs.push(new Tab( - USER_TAB.PREFERENCES, + UserTab.Preferences, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , @@ -119,7 +119,7 @@ export default class UserSettingsDialog extends React.Component if (SettingsStore.getValue(UIFeature.Voip)) { tabs.push(new Tab( - USER_TAB.VOICE, + UserTab.Voice, _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , @@ -127,7 +127,7 @@ export default class UserSettingsDialog extends React.Component } tabs.push(new Tab( - USER_TAB.SECURITY, + UserTab.Security, _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , @@ -137,7 +137,7 @@ export default class UserSettingsDialog extends React.Component || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) ) { tabs.push(new Tab( - USER_TAB.LABS, + UserTab.Labs, _td("Labs"), "mx_UserSettingsDialog_labsIcon", , @@ -145,14 +145,14 @@ export default class UserSettingsDialog extends React.Component } if (this.state.mjolnirEnabled) { tabs.push(new Tab( - USER_TAB.MJOLNIR, + UserTab.Mjolnir, _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , )); } tabs.push(new Tab( - USER_TAB.HELP, + UserTab.Help, _td("Help & About"), "mx_UserSettingsDialog_helpIcon", this.props.onFinished(true)} />, diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index 7770da3049..840390f6fb 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -34,7 +34,7 @@ interface IProps { interface IState { error: Error | null; - canUploadKeysWithPasswordOnly: boolean | null; + canUploadKeysWithPasswordOnly?: boolean; accountPassword: string; } @@ -45,7 +45,7 @@ interface IState { */ @replaceableComponent("views.dialogs.security.CreateCrossSigningDialog") export default class CreateCrossSigningDialog extends React.PureComponent { - constructor(props) { + constructor(props: IProps) { super(props); this.state = { @@ -90,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ type: 'm.login.password', diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index b86b89cede..19c7af01ff 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -18,11 +18,11 @@ import React from 'react'; import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody'; import BaseDialog from '../BaseDialog'; import { _t } from '../../../../languageHandler'; -import { SetupEncryptionStore, PHASE } from '../../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../../utils/replaceableComponent"; -function iconFromPhase(phase: PHASE) { - if (phase === PHASE.DONE) { +function iconFromPhase(phase: Phase) { + if (phase === Phase.Done) { return require("../../../../../res/img/e2e/verified.svg"); } else { return require("../../../../../res/img/e2e/warning.svg"); @@ -34,14 +34,14 @@ interface IProps { } interface IState { - icon: PHASE; + icon: Phase; } @replaceableComponent("views.dialogs.security.SetupEncryptionDialog") export default class SetupEncryptionDialog extends React.Component { private store: SetupEncryptionStore; - constructor(props) { + constructor(props: IProps) { super(props); this.store = SetupEncryptionStore.sharedInstance(); diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index fe77ac0377..03954bad56 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; import { Action } from "../../../dispatcher/actions"; -import { USER_TAB } from "../dialogs/UserSettingsDialog"; +import { UserTab } from "../dialogs/UserSettingsDialog"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import { E2EStatus } from "../../../utils/ShieldUtils"; @@ -1381,7 +1381,7 @@ const BasicUserInfo: React.FC<{ { dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_TAB.SECURITY, + initialTabId: UserTab.Security, }); }}> { _t("Edit devices") } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 95bbabbe53..977cd4a9aa 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -29,7 +29,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import {BetaPill} from "../beta/BetaCard"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import { USER_TAB } from "../dialogs/UserSettingsDialog"; +import { UserTab } from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; @@ -224,7 +224,7 @@ const SpaceCreateMenu = ({ onFinished }) => { onFinished(); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_TAB.LABS, + initialTabId: UserTab.Labs, }); }} /> { body } diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 86e8b7afc3..88385d0399 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -22,18 +22,18 @@ import { MatrixClientPeg } from '../MatrixClientPeg'; import { accessSecretStorage, AccessCancelledError } from '../SecurityManager'; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -export enum PHASE { - LOADING = 0, - INTRO = 1, - BUSY = 2, - DONE = 3, // final done stage, but still showing UX - CONFIRM_SKIP = 4, - FINISHED = 5, // UX can be closed +export enum Phase { + Loading = 0, + Intro = 1, + Busy = 2, + Done = 3, // final done stage, but still showing UX + ConfirmSkip = 4, + Finished = 5, // UX can be closed } export class SetupEncryptionStore extends EventEmitter { private started: boolean; - public phase: PHASE; + public phase: Phase; public verificationRequest: VerificationRequest; public backupInfo: IKeyBackupVersion; public keyId: string; @@ -50,7 +50,7 @@ export class SetupEncryptionStore extends EventEmitter { return; } this.started = true; - this.phase = PHASE.LOADING; + this.phase = Phase.Loading; this.verificationRequest = null; this.backupInfo = null; @@ -110,15 +110,15 @@ export class SetupEncryptionStore extends EventEmitter { if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) { // skip before we can even render anything. - this.phase = PHASE.FINISHED; + this.phase = Phase.Finished; } else { - this.phase = PHASE.INTRO; + this.phase = Phase.Intro; } this.emit("update"); } public async usePassPhrase(): Promise { - this.phase = PHASE.BUSY; + this.phase = Phase.Busy; this.emit("update"); const cli = MatrixClientPeg.get(); try { @@ -147,7 +147,7 @@ export class SetupEncryptionStore extends EventEmitter { }); if (cli.getCrossSigningId()) { - this.phase = PHASE.DONE; + this.phase = Phase.Done; this.emit("update"); } } catch (e) { @@ -155,7 +155,7 @@ export class SetupEncryptionStore extends EventEmitter { console.log(e); } // this will throw if the user hits cancel, so ignore - this.phase = PHASE.INTRO; + this.phase = Phase.Intro; this.emit("update"); } } @@ -164,7 +164,7 @@ export class SetupEncryptionStore extends EventEmitter { if (userId !== MatrixClientPeg.get().getUserId()) return; const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); if (publicKeysTrusted) { - this.phase = PHASE.DONE; + this.phase = Phase.Done; this.emit("update"); } } @@ -185,28 +185,28 @@ export class SetupEncryptionStore extends EventEmitter { // cross signing to be ready to use, so wait for the user trust status to // change (or change to DONE if it's already ready). const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); - this.phase = publicKeysTrusted ? PHASE.DONE : PHASE.BUSY; + this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy; this.emit("update"); } } public skip(): void { - this.phase = PHASE.CONFIRM_SKIP; + this.phase = Phase.ConfirmSkip; this.emit("update"); } public skipConfirm(): void { - this.phase = PHASE.FINISHED; + this.phase = Phase.Finished; this.emit("update"); } public returnAfterSkip(): void { - this.phase = PHASE.INTRO; + this.phase = Phase.Intro; this.emit("update"); } public done(): void { - this.phase = PHASE.FINISHED; + this.phase = Phase.Finished; this.emit("update"); // async - ask other clients for keys, if necessary MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests(); diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index 8e3fa7c8a7..05425b93c0 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -21,7 +21,7 @@ import DeviceListener from '../DeviceListener'; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import { Action } from "../dispatcher/actions"; -import { USER_TAB } from "../components/views/dialogs/UserSettingsDialog"; +import { UserTab } from "../components/views/dialogs/UserSettingsDialog"; function toastKey(deviceId: string) { return "unverified_session_" + deviceId; @@ -34,7 +34,7 @@ export const showToast = async (deviceId: string) => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_TAB.SECURITY, + initialTabId: UserTab.Security, }); }; From fcda0604e0ff1bf6531f1fd45f8c50b4145f6fa1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 18 Jun 2021 12:48:31 +0100 Subject: [PATCH 32/46] Fix RoomMember import --- src/components/views/dialogs/ConfirmUserActionDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index c91dcba95c..05f8c63ace 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; -import RoomMember from "matrix-js-sdk/src/models/room-member.js"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; From 538165d51580805170e4230ba7ded0862f1f89bd Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 18 Jun 2021 14:05:12 +0100 Subject: [PATCH 33/46] Fix phase enum usage in JS modules as well https://github.com/matrix-org/matrix-react-sdk/pull/6185 converted `SetupEncryptionStore` to TS, including moving the phase states to an enum. The calling JS modules were forgotten, so they got a bit confused. Fixes https://github.com/vector-im/element-web/issues/17689 Regressed by https://github.com/matrix-org/matrix-react-sdk/pull/6185 --- .../structures/auth/CompleteSecurity.js | 19 ++++++------------ .../structures/auth/SetupEncryptionBody.js | 20 ++++++------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index 49fcf20415..654dd9b6c8 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -18,14 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; -import { - SetupEncryptionStore, - PHASE_LOADING, - PHASE_INTRO, - PHASE_BUSY, - PHASE_DONE, - PHASE_CONFIRM_SKIP, -} from '../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import SetupEncryptionBody from "./SetupEncryptionBody"; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -61,18 +54,18 @@ export default class CompleteSecurity extends React.Component { let icon; let title; - if (phase === PHASE_LOADING) { + if (phase === Phase.Loading) { return null; - } else if (phase === PHASE_INTRO) { + } else if (phase === Phase.Intro) { icon = ; title = _t("Verify this login"); - } else if (phase === PHASE_DONE) { + } else if (phase === Phase.Done) { icon = ; title = _t("Session verified"); - } else if (phase === PHASE_CONFIRM_SKIP) { + } else if (phase === Phase.ConfirmSkip) { icon = ; title = _t("Are you sure?"); - } else if (phase === PHASE_BUSY) { + } else if (phase === Phase.Busy) { icon = ; title = _t("Verify this login"); } else { diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 803df19d00..90137e084c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -21,15 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import * as sdk from '../../../index'; -import { - SetupEncryptionStore, - PHASE_LOADING, - PHASE_INTRO, - PHASE_BUSY, - PHASE_DONE, - PHASE_CONFIRM_SKIP, - PHASE_FINISHED, -} from '../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../utils/replaceableComponent"; function keyHasPassphrase(keyInfo) { @@ -63,7 +55,7 @@ export default class SetupEncryptionBody extends React.Component { _onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); - if (store.phase === PHASE_FINISHED) { + if (store.phase === Phase.Finished) { this.props.onFinished(); return; } @@ -136,7 +128,7 @@ export default class SetupEncryptionBody extends React.Component { onClose={this.props.onFinished} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; - } else if (phase === PHASE_INTRO) { + } else if (phase === Phase.Intro) { const store = SetupEncryptionStore.sharedInstance(); let recoveryKeyPrompt; if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { @@ -174,7 +166,7 @@ export default class SetupEncryptionBody extends React.Component {
    ); - } else if (phase === PHASE_DONE) { + } else if (phase === Phase.Done) { let message; if (this.state.backupInfo) { message =

    {_t( @@ -200,7 +192,7 @@ export default class SetupEncryptionBody extends React.Component {

    ); - } else if (phase === PHASE_CONFIRM_SKIP) { + } else if (phase === Phase.ConfirmSkip) { return (

    {_t( @@ -224,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component {

    ); - } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) { + } else if (phase === Phase.Busy || phase === Phase.Loading) { const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { From 77a4d345bda4791bfec1b8fee643e2b29441a1c2 Mon Sep 17 00:00:00 2001 From: David Teller Date: Fri, 18 Jun 2021 18:09:02 +0200 Subject: [PATCH 34/46] Submitting abuse reports to moderators (#6213) This patch is part of MSC3215. It implements `feature_report_to_moderator` to let end-users send report to room moderators instead of homeserver administrators. This only works if the room has been setup for moderation, something that does not have a UX yet. Signed-off-by: David Teller --- .../views/dialogs/ReportEventDialog.js | 149 ------ .../views/dialogs/ReportEventDialog.tsx | 445 ++++++++++++++++++ src/i18n/strings/en_EN.json | 18 +- src/settings/Settings.tsx | 7 + 4 files changed, 468 insertions(+), 151 deletions(-) delete mode 100644 src/components/views/dialogs/ReportEventDialog.js create mode 100644 src/components/views/dialogs/ReportEventDialog.tsx diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js deleted file mode 100644 index 5454b97287..0000000000 --- a/src/components/views/dialogs/ReportEventDialog.js +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - -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, {PureComponent} from 'react'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import PropTypes from "prop-types"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import SdkConfig from '../../../SdkConfig'; -import Markdown from '../../../Markdown'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -/* - * A dialog for reporting an event. - */ -@replaceableComponent("views.dialogs.ReportEventDialog") -export default class ReportEventDialog extends PureComponent { - static propTypes = { - mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, - onFinished: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - reason: "", - busy: false, - err: null, - }; - } - - _onReasonChange = ({target: {value: reason}}) => { - this.setState({ reason }); - }; - - _onCancel = () => { - this.props.onFinished(false); - }; - - _onSubmit = async () => { - if (!this.state.reason || !this.state.reason.trim()) { - this.setState({ - err: _t("Please fill why you're reporting."), - }); - return; - } - - this.setState({ - busy: true, - err: null, - }); - - try { - const ev = this.props.mxEvent; - await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim()); - this.props.onFinished(true); - } catch (e) { - this.setState({ - busy: false, - err: e.message, - }); - } - }; - - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Loader = sdk.getComponent('elements.Spinner'); - const Field = sdk.getComponent('elements.Field'); - - let error = null; - if (this.state.err) { - error =
    - {this.state.err} -
    ; - } - - let progress = null; - if (this.state.busy) { - progress = ( -
    - -
    - ); - } - - const adminMessageMD = - SdkConfig.get().reportEvent && - SdkConfig.get().reportEvent.adminMessageMD; - let adminMessage; - if (adminMessageMD) { - const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true }); - adminMessage =

    ; - } - - return ( - -

    -

    - { - _t("Reporting this message will send its unique 'event ID' to the administrator of " + - "your homeserver. If messages in this room are encrypted, your homeserver " + - "administrator will not be able to read the message text or view any files or images.") - } -

    - {adminMessage} - - {progress} - {error} -
    - - - ); - } -} diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx new file mode 100644 index 0000000000..8271239f7f --- /dev/null +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -0,0 +1,445 @@ +/* +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +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 * as sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import { ensureDMExists } from "../../../createRoom"; +import { IDialogProps } from "./IDialogProps"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import SdkConfig from '../../../SdkConfig'; +import Markdown from '../../../Markdown'; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import SettingsStore from "../../../settings/SettingsStore"; +import StyledRadioButton from "../elements/StyledRadioButton"; + +interface IProps extends IDialogProps { + mxEvent: MatrixEvent; +} + +interface IState { + // A free-form text describing the abuse. + reason: string; + busy: boolean; + err?: string; + // If we know it, the nature of the abuse, as specified by MSC3215. + nature?: EXTENDED_NATURE; +} + + +const MODERATED_BY_STATE_EVENT_TYPE = [ + "org.matrix.msc3215.room.moderation.moderated_by", + /** + * Unprefixed state event. Not ready for prime time. + * + * "m.room.moderation.moderated_by" + */ +]; + +const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report"; + +// Standard abuse natures. +enum NATURE { + DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement", + TOXIC = "org.matrix.msc3215.abuse.nature.toxic", + ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal", + SPAM = "org.matrix.msc3215.abuse.nature.spam", + OTHER = "org.matrix.msc3215.abuse.nature.other", +} + +enum NON_STANDARD_NATURE { + // Non-standard abuse nature. + // It should never leave the client - we use it to fallback to + // server-wide abuse reporting. + ADMIN = "non-standard.abuse.nature.admin" +} + +type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE; + +type Moderation = { + // The id of the moderation room. + moderationRoomId: string; + // The id of the bot in charge of forwarding abuse reports to the moderation room. + moderationBotUserId: string; +} +/* + * A dialog for reporting an event. + * + * The actual content of the dialog will depend on two things: + * + * 1. Is `feature_report_to_moderators` enabled? + * 2. Does the room support moderation as per MSC3215, i.e. is there + * a well-formed state event `m.room.moderation.moderated_by` + * /`org.matrix.msc3215.room.moderation.moderated_by`? + */ +@replaceableComponent("views.dialogs.ReportEventDialog") +export default class ReportEventDialog extends React.Component { + // If the room supports moderation, the moderation information. + private moderation?: Moderation; + + constructor(props: IProps) { + super(props); + + let moderatedByRoomId = null; + let moderatedByUserId = null; + + if (SettingsStore.getValue("feature_report_to_moderators")) { + // The client supports reporting to moderators. + // Does the room support it, too? + + // Extract state events to determine whether we should display + const client = MatrixClientPeg.get(); + const room = client.getRoom(props.mxEvent.getRoomId()); + + for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) { + const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType); + if (!stateEvent) { + continue; + } + if (Array.isArray(stateEvent)) { + // Internal error. + throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` + + "should return at most one state event"); + } + const event = stateEvent.event; + if (!("content" in event) || typeof event["content"] != "object") { + // The room is improperly configured. + // Display this debug message for the sake of moderators. + console.debug("Moderation error", "state event", stateEventType, + "should have an object field `content`, got", event); + continue; + } + const content = event["content"]; + if (!("room_id" in content) || typeof content["room_id"] != "string") { + // The room is improperly configured. + // Display this debug message for the sake of moderators. + console.debug("Moderation error", "state event", stateEventType, + "should have a string field `content.room_id`, got", event); + continue; + } + if (!("user_id" in content) || typeof content["user_id"] != "string") { + // The room is improperly configured. + // Display this debug message for the sake of moderators. + console.debug("Moderation error", "state event", stateEventType, + "should have a string field `content.user_id`, got", event); + continue; + } + moderatedByRoomId = content["room_id"]; + moderatedByUserId = content["user_id"]; + } + + if (moderatedByRoomId && moderatedByUserId) { + // The room supports moderation. + this.moderation = { + moderationRoomId: moderatedByRoomId, + moderationBotUserId: moderatedByUserId, + }; + } + } + + this.state = { + // A free-form text describing the abuse. + reason: "", + busy: false, + err: null, + // If specified, the nature of the abuse, as specified by MSC3215. + nature: null, + }; + } + + // The user has written down a freeform description of the abuse. + private onReasonChange = ({target: {value: reason}}): void => { + this.setState({ reason }); + }; + + // The user has clicked on a nature. + private onNatureChosen = (e: React.FormEvent): void => { + this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE}); + }; + + // The user has clicked "cancel". + private onCancel = (): void => { + this.props.onFinished(false); + }; + + // The user has clicked "submit". + private onSubmit = async () => { + let reason = this.state.reason || ""; + reason = reason.trim(); + if (this.moderation) { + // This room supports moderation. + // We need a nature. + // If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`. + if (!this.state.nature || + ((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN) + && !reason) + ) { + this.setState({ + err: _t("Please fill why you're reporting."), + }); + return; + } + } else { + // This room does not support moderation. + // We need a `reason`. + if (!reason) { + this.setState({ + err: _t("Please fill why you're reporting."), + }); + return; + } + } + + this.setState({ + busy: true, + err: null, + }); + + try { + const client = MatrixClientPeg.get(); + const ev = this.props.mxEvent; + if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) { + const nature: NATURE = this.state.nature; + + // Report to moderators through to the dedicated bot, + // as configured in the room's state events. + const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId); + await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, { + event_id: ev.getId(), + room_id: ev.getRoomId(), + moderated_by_id: this.moderation.moderationRoomId, + nature, + reporter: client.getUserId(), + comment: this.state.reason.trim(), + }); + } else { + // Report to homeserver admin through the dedicated Matrix API. + await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim()); + } + this.props.onFinished(true); + } catch (e) { + this.setState({ + busy: false, + err: e.message, + }); + } + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const Loader = sdk.getComponent('elements.Spinner'); + const Field = sdk.getComponent('elements.Field'); + + let error = null; + if (this.state.err) { + error =
    + {this.state.err} +
    ; + } + + let progress = null; + if (this.state.busy) { + progress = ( +
    + +
    + ); + } + + const adminMessageMD = + SdkConfig.get().reportEvent && + SdkConfig.get().reportEvent.adminMessageMD; + let adminMessage; + if (adminMessageMD) { + const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true }); + adminMessage =

    ; + } + + if (this.moderation) { + // Display report-to-moderator dialog. + // We let the user pick a nature. + const client = MatrixClientPeg.get(); + const homeServerName = SdkConfig.get()["validated_server_config"].hsName; + let subtitle; + switch (this.state.nature) { + case NATURE.DISAGREEMENT: + subtitle = _t("What this user is writing is wrong.\n" + + "This will be reported to the room moderators."); + break; + case NATURE.TOXIC: + subtitle = _t("This user is displaying toxic behaviour, " + + "for instance by insulting other users or sharing " + + " adult-only content in a family-friendly room " + + " or otherwise violating the rules of this room.\n" + + "This will be reported to the room moderators."); + break; + case NATURE.ILLEGAL: + subtitle = _t("This user is displaying illegal behaviour, " + + "for instance by doxing people or threatening violence.\n" + + "This will be reported to the room moderators who may escalate this to legal authorities."); + break; + case NATURE.SPAM: + subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" + + "This will be reported to the room moderators."); + break; + case NON_STANDARD_NATURE.ADMIN: + if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) { + subtitle = _t("This room is dedicated to illegal or toxic content " + + "or the moderators fail to moderate illegal or toxic content.\n" + + "This will be reported to the administrators of %(homeserver)s. " + + "The administrators will NOT be able to read the encrypted content of this room.", + { homeserver: homeServerName }); + } else { + subtitle = _t("This room is dedicated to illegal or toxic content " + + "or the moderators fail to moderate illegal or toxic content.\n" + + " This will be reported to the administrators of %(homeserver)s.", + { homeserver: homeServerName }); + } + break; + case NATURE.OTHER: + subtitle = _t("Any other reason. Please describe the problem.\n" + + "This will be reported to the room moderators."); + break; + default: + subtitle = _t("Please pick a nature and describe what makes this message abusive."); + break; + } + + return ( + +

    + + {_t('Disagree')} + + + {_t('Toxic Behaviour')} + + + {_t('Illegal Content')} + + + {_t('Spam or propaganda')} + + + {_t('Report the entire room')} + + + {_t('Other')} + +

    + {subtitle} +

    + + {progress} + {error} +
    + + + ); + } + // Report to homeserver admin. + // Currently, the API does not support natures. + return ( + +
    +

    + { + _t("Reporting this message will send its unique 'event ID' to the administrator of " + + "your homeserver. If messages in this room are encrypted, your homeserver " + + "administrator will not be able to read the message text or view any files " + + "or images.") + } +

    + {adminMessage} + + {progress} + {error} +
    + +
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8c4262fe44..b88dc79da5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -784,6 +784,7 @@ "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Change notification settings": "Change notification settings", + "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.", "Spaces": "Spaces", "Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.", @@ -2318,9 +2319,23 @@ "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", "Email (optional)": "Email (optional)", "Please fill why you're reporting.": "Please fill why you're reporting.", + "What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.", + "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", + "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.", + "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.", + "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.", + "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.", + "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.", + "Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.", + "Report Content": "Report Content", + "Disagree": "Disagree", + "Toxic Behaviour": "Toxic Behaviour", + "Illegal Content": "Illegal Content", + "Spam or propaganda": "Spam or propaganda", + "Report the entire room": "Report the entire room", + "Send report": "Send report", "Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator", "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.", - "Send report": "Send report", "Room Settings - %(roomName)s": "Room Settings - %(roomName)s", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", @@ -2487,7 +2502,6 @@ "Share Message": "Share Message", "Source URL": "Source URL", "Collapse Reply Thread": "Collapse Reply Thread", - "Report Content": "Report Content", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 155d039572..97f1beb979 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -131,6 +131,13 @@ export interface ISetting { } export const SETTINGS: {[setting: string]: ISetting} = { + "feature_report_to_moderators": { + isFeature: true, + displayName: _td("Report to moderators prototype. " + + "In rooms that support moderation, the `report` button will let you report abuse to room moderators"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_spaces": { isFeature: true, displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " + From 4ff25c5978484289d085466d0e60f3cbb23fabd9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 19:16:39 +0100 Subject: [PATCH 35/46] Add jq to e2e tests Dockerfile --- scripts/ci/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile index 3fdd0d7bf6..1d1425c865 100644 --- a/scripts/ci/Dockerfile +++ b/scripts/ci/Dockerfile @@ -3,6 +3,6 @@ # docker push vectorim/element-web-ci-e2etests-env:latest FROM node:14-buster RUN apt-get update -RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime +RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime # dependencies for chrome (installed by puppeteer) RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget From 2c5ddea1d9687bfe7ce406741e824074be56ddfb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 15:57:07 +0100 Subject: [PATCH 36/46] Fix ConfirmUserActionDialog returning an input field rather than text --- src/components/views/dialogs/ConfirmUserActionDialog.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 05f8c63ace..5cdb4c664b 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -38,7 +38,7 @@ interface IProps { // be the string entered. askReason?: boolean; danger?: boolean; - onFinished: (success: boolean, reason?: HTMLInputElement) => void; + onFinished: (success: boolean, reason?: string) => void; } /* @@ -59,11 +59,7 @@ export default class ConfirmUserActionDialog extends React.Component { }; public onOk = (): void => { - let reason; - if (this.reasonField) { - reason = this.reasonField.current; - } - this.props.onFinished(true, reason); + this.props.onFinished(true, this.reasonField.current?.value); }; public onCancel = (): void => { From 9344adb2d2e9419fa5238e6a03cf863380c621fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 19 Jun 2021 13:38:19 -0600 Subject: [PATCH 37/46] Revert "Partially restore immutable event objects at the rendering layer" --- src/components/views/messages/TextualBody.js | 3 +- src/components/views/rooms/EventTile.tsx | 124 ++++++++----------- 2 files changed, 50 insertions(+), 77 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index c67001cc87..ebc4ce7ce8 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -263,8 +263,7 @@ export default class TextualBody extends React.Component { //console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); // exploit that events are immutable :) - return (nextProps.mxEvent !== this.props.mxEvent || - nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || + return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || nextProps.highlights !== this.props.highlights || nextProps.replacingEventId !== this.props.replacingEventId || nextProps.highlightLink !== this.props.highlightLink || diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 07c6427992..0099bf73fb 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -300,9 +300,6 @@ interface IState { // The Relations model from the JS SDK for reactions to `mxEvent` reactions: Relations; - // Our snapshotted/local copy of the props.mxEvent, for local echo reasons - mxEvent: MatrixEvent; - hover: boolean; } @@ -337,8 +334,6 @@ export default class EventTile extends React.Component { // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), - mxEvent: this.mxEvent.toSnapshot(), // snapshot up front to verify it all works - hover: false, }; @@ -355,10 +350,6 @@ export default class EventTile extends React.Component { this.ref = React.createRef(); } - private get mxEvent(): MatrixEvent { - return this.state?.mxEvent ?? this.props.mxEvent; - } - /** * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending' * or 'sent' receipt, for example. @@ -367,16 +358,16 @@ export default class EventTile extends React.Component { private get isEligibleForSpecialReceipt() { // First, if there are other read receipts then just short-circuit this. if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; - if (!this.mxEvent) return false; + if (!this.props.mxEvent) return false; // Sanity check (should never happen, but we shouldn't explode if it does) - const room = this.context.getRoom(this.mxEvent.getRoomId()); + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); if (!room) return false; // Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for // special read receipts. const myUserId = MatrixClientPeg.get().getUserId(); - if (this.mxEvent.getSender() !== myUserId) return false; + if (this.props.mxEvent.getSender() !== myUserId) return false; // Finally, determine if the type is relevant to the user. This notably excludes state // events and pretty much anything that can't be sent by the composer as a message. For @@ -387,7 +378,7 @@ export default class EventTile extends React.Component { EventType.RoomMessage, EventType.RoomMessageEncrypted, ]; - if (!simpleSendableEvents.includes(this.mxEvent.getType() as EventType)) return false; + if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false; // Default case return true; @@ -429,7 +420,7 @@ export default class EventTile extends React.Component { // TODO: [REACT-WARNING] Move into constructor // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { - this.verifyEvent(this.mxEvent); + this.verifyEvent(this.props.mxEvent); } componentDidMount() { @@ -459,21 +450,11 @@ export default class EventTile extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - // If the echo changed meaningfully, update. - if (!this.state.mxEvent?.isEquivalentTo(nextProps.mxEvent)) { - return true; - } - if (objectHasDiff(this.state, nextState)) { return true; } - if (!this.propsEqual(this.props, nextProps)) { - return true; - } - - // Always assume there's no significant change. - return false; + return !this.propsEqual(this.props, nextProps); } componentWillUnmount() { @@ -494,18 +475,11 @@ export default class EventTile extends React.Component { this.context.on("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = true; } - - // Update the state again if the snapshot needs updating. Note that this will fire - // a second state update to re-render child components, which ultimately calls didUpdate - // again, so we break that loop with a reference check first (faster than comparing events). - if (this.state.mxEvent === prevState.mxEvent && !this.state?.mxEvent.isEquivalentTo(this.props.mxEvent)) { - this.setState({mxEvent: this.props.mxEvent.toSnapshot()}); - } } private onRoomReceipt = (ev, room) => { // ignore events for other rooms - const tileRoom = MatrixClientPeg.get().getRoom(this.mxEvent.getRoomId()); + const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); if (room !== tileRoom) return; if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) { @@ -529,19 +503,19 @@ export default class EventTile extends React.Component { // we need to re-verify the sending device. // (we call onHeightChanged in verifyEvent to handle the case where decryption // has caused a change in size of the event tile) - this.verifyEvent(this.mxEvent); + this.verifyEvent(this.props.mxEvent); this.forceUpdate(); }; private onDeviceVerificationChanged = (userId, device) => { - if (userId === this.mxEvent.getSender()) { - this.verifyEvent(this.mxEvent); + if (userId === this.props.mxEvent.getSender()) { + this.verifyEvent(this.props.mxEvent); } }; private onUserVerificationChanged = (userId, _trustStatus) => { - if (userId === this.mxEvent.getSender()) { - this.verifyEvent(this.mxEvent); + if (userId === this.props.mxEvent.getSender()) { + this.verifyEvent(this.props.mxEvent); } }; @@ -648,11 +622,11 @@ export default class EventTile extends React.Component { } shouldHighlight() { - const actions = this.context.getPushActionsForEvent(this.mxEvent.replacingEvent() || this.mxEvent); + const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); if (!actions || !actions.tweaks) { return false; } // don't show self-highlights from another of our clients - if (this.mxEvent.getSender() === this.context.credentials.userId) { + if (this.props.mxEvent.getSender() === this.context.credentials.userId) { return false; } @@ -667,7 +641,7 @@ export default class EventTile extends React.Component { getReadAvatars() { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { - return ; + return ; } // return early if there are no read receipts @@ -754,7 +728,7 @@ export default class EventTile extends React.Component { } onSenderProfileClick = event => { - const mxEvent = this.mxEvent; + const mxEvent = this.props.mxEvent; dis.dispatch({ action: Action.ComposerInsert, userId: mxEvent.getSender(), @@ -771,7 +745,7 @@ export default class EventTile extends React.Component { // Cancel any outgoing key request for this event and resend it. If a response // is received for the request with the required keys, the event could be // decrypted successfully. - this.context.cancelAndResendEventRoomKeyRequest(this.mxEvent); + this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); }; onPermalinkClicked = e => { @@ -780,14 +754,14 @@ export default class EventTile extends React.Component { e.preventDefault(); dis.dispatch({ action: 'view_room', - event_id: this.mxEvent.getId(), + event_id: this.props.mxEvent.getId(), highlighted: true, - room_id: this.mxEvent.getRoomId(), + room_id: this.props.mxEvent.getRoomId(), }); }; private renderE2EPadlock() { - const ev = this.mxEvent; + const ev = this.props.mxEvent; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { @@ -846,7 +820,7 @@ export default class EventTile extends React.Component { ) { return null; } - const eventId = this.mxEvent.getId(); + const eventId = this.props.mxEvent.getId(); return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); }; @@ -865,13 +839,13 @@ export default class EventTile extends React.Component { const SenderProfile = sdk.getComponent('messages.SenderProfile'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - //console.info("EventTile showUrlPreview for %s is %s", this.mxEvent.getId(), this.props.showUrlPreview); + //console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); - const content = this.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const msgtype = content.msgtype; - const eventType = this.mxEvent.getType(); + const eventType = this.props.mxEvent.getType(); - let tileHandler = getHandlerTile(this.mxEvent); + let tileHandler = getHandlerTile(this.props.mxEvent); // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || @@ -888,7 +862,7 @@ export default class EventTile extends React.Component { // source tile when there's no regular tile for an event and also for // replace relations (which otherwise would display as a confusing // duplicate of the thing they are replacing). - if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.mxEvent)) { + if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) { tileHandler = "messages.ViewSourceEvent"; // Reuse info message avatar and sender profile styling isInfoMessage = true; @@ -907,8 +881,8 @@ export default class EventTile extends React.Component { const EventTileType = sdk.getComponent(tileHandler); const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); - const isRedacted = isMessageEvent(this.mxEvent) && this.props.isRedacted; - const isEncryptionFailure = this.mxEvent.isDecryptionFailure(); + const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; + const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); const isEditing = !!this.props.editState; const classes = classNames({ @@ -938,14 +912,14 @@ export default class EventTile extends React.Component { let permalink = "#"; if (this.props.permalinkCreator) { - permalink = this.props.permalinkCreator.forEvent(this.mxEvent.getId()); + permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } // we can't use local echoes as scroll tokens, because their event IDs change. // Local echos have a send "status". - const scrollToken = this.mxEvent.status + const scrollToken = this.props.mxEvent.status ? undefined - : this.mxEvent.getId(); + : this.props.mxEvent.getId(); let avatar; let sender; @@ -975,15 +949,15 @@ export default class EventTile extends React.Component { needsSenderProfile = true; } - if (this.mxEvent.sender && avatarSize) { + if (this.props.mxEvent.sender && avatarSize) { let member; // set member to receiver (target) if it is a 3PID invite // so that the correct avatar is shown as the text is // `$target accepted the invitation for $email` - if (this.mxEvent.getContent().third_party_invite) { - member = this.mxEvent.target; + if (this.props.mxEvent.getContent().third_party_invite) { + member = this.props.mxEvent.target; } else { - member = this.mxEvent.sender; + member = this.props.mxEvent.sender; } avatar = (
    @@ -998,17 +972,17 @@ export default class EventTile extends React.Component { if (needsSenderProfile) { if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { sender = ; } else { - sender = ; + sender = ; } } const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); const actionBar = !isEditing ? { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const showTimestamp = this.mxEvent.getTs() && + const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); const timestamp = showTimestamp ? - : null; + : null; const keyRequestHelpText =
    @@ -1059,7 +1033,7 @@ export default class EventTile extends React.Component { if (!isRedacted) { const ReactionsRow = sdk.getComponent('messages.ReactionsRow'); reactionsRow = ; } @@ -1067,7 +1041,7 @@ export default class EventTile extends React.Component { const linkedTimestamp = { timestamp } ; @@ -1086,7 +1060,7 @@ export default class EventTile extends React.Component { switch (this.props.tileShape) { case 'notif': { - const room = this.context.getRoom(this.mxEvent.getRoomId()); + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return React.createElement(this.props.as || "li", { "className": classes, "aria-live": ariaLive, @@ -1108,7 +1082,7 @@ export default class EventTile extends React.Component {
    ,
    { }, [
    { let thread; if (this.props.tileShape === 'reply_preview') { thread = ReplyThread.makeThread( - this.mxEvent, + this.props.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, @@ -1176,7 +1150,7 @@ export default class EventTile extends React.Component { { groupPadlock } { thread } { } default: { const thread = ReplyThread.makeThread( - this.mxEvent, + this.props.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, @@ -1216,7 +1190,7 @@ export default class EventTile extends React.Component { { groupPadlock } { thread } Date: Fri, 18 Jun 2021 18:32:45 +0100 Subject: [PATCH 38/46] Naive attempt at improving our end-to-end tests in Github Actions --- .github/workflows/develop.yml | 7 +++++-- ...d-tests.sh => prepare-end-to-end-tests.sh} | 11 +---------- scripts/ci/run-end-to-end-tests.sh | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 12 deletions(-) rename scripts/ci/{end-to-end-tests.sh => prepare-end-to-end-tests.sh} (65%) create mode 100755 scripts/ci/run-end-to-end-tests.sh diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 6410bd28fa..3c3807e33b 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -11,10 +11,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: End-to-End tests - run: ./scripts/ci/end-to-end-tests.sh + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh - name: Archive logs uses: actions/upload-artifact@v2 + if: ${{ always() }} with: path: | test/end-to-end-tests/logs/**/* diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/prepare-end-to-end-tests.sh similarity index 65% rename from scripts/ci/end-to-end-tests.sh rename to scripts/ci/prepare-end-to-end-tests.sh index edb8870d8e..147e1f6445 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/prepare-end-to-end-tests.sh @@ -1,8 +1,4 @@ #!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones element-web develop and runs the tests against our version of react-sdk. set -ev @@ -19,7 +15,7 @@ cd element-web element_web_dir=`pwd` CI_PACKAGE=true yarn build cd .. -# run end to end tests +# prepare end to end tests pushd test/end-to-end-tests ln -s $element_web_dir element/element-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh @@ -28,9 +24,4 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of element ./element/install-webserver.sh -rm -r logs || true -mkdir logs -echo "+++ Running end-to-end tests" -TESTS_STARTED=1 -./run.sh --no-sandbox --log-directory logs/ popd diff --git a/scripts/ci/run-end-to-end-tests.sh b/scripts/ci/run-end-to-end-tests.sh new file mode 100755 index 0000000000..3c99391fc7 --- /dev/null +++ b/scripts/ci/run-end-to-end-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ev + +handle_error() { + EXIT_CODE=$? + exit $EXIT_CODE +} + +trap 'handle_error' ERR + +# run end to end tests +pushd test/end-to-end-tests +rm -r logs || true +mkdir logs +echo "--- Running end-to-end tests" +TESTS_STARTED=1 +./run.sh --no-sandbox --log-directory logs/ +popd From e79b7d7adb7b8f5214d0b498cd555957ccc5f1ba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 15:37:48 +0100 Subject: [PATCH 39/46] Fix View Source accessing renamed private field on MatrixEvent --- src/components/structures/ViewSource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 6fe99dd464..b69a92dd61 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -55,7 +55,7 @@ export default class ViewSource extends React.Component { viewSourceContent() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const isEncrypted = mxEvent.isEncrypted(); - const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private + const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private const originalEventSource = mxEvent.event; if (isEncrypted) { From 2d9e97a3e13bb481431f812178c47f6c75e395c6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 09:47:46 +0100 Subject: [PATCH 40/46] Fix branch matching to work with GitHub Actions and BuildKite --- scripts/fetchdep.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index fe1f49c361..9844fdc9db 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -22,15 +22,18 @@ clone() { } # Try the PR author's branch in case it exists on the deps as well. -# First we check if BUILDKITE_BRANCH is defined, +# First we check if GITHUB_HEAD_REF is defined, +# Then we check if BUILDKITE_BRANCH is defined, # if it isn't we can assume this is a Netlify build -if [ -z ${BUILDKITE_BRANCH+x} ]; then +if [ -n ${GITHUB_HEAD_REF+x} ]; then + head=$GITHUB_HEAD_REF +elif [ -n ${BUILDKITE_BRANCH+x} ]; then + head=$BUILDKITE_BRANCH +else # Netlify doesn't give us info about the fork so we have to get it from GitHub API apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" apiEndpoint+=$REVIEW_ID head=$(curl $apiEndpoint | jq -r '.head.label') -else - head=$BUILDKITE_BRANCH fi # If head is set, it will contain either: @@ -39,12 +42,18 @@ fi # We can split on `:` into an array to check. BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - clone $deforg $defrepo $BUILDKITE_BRANCH + clone $deforg $defrepo $head elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} fi + # Try the target branch of the push or PR. -clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +if [ -n ${GITHUB_BASE_REF+x} ]; then + clone $deforg $defrepo $GITHUB_BASE_REF +elif [ -n ${BUILDKITE_PULL_REQUEST_BASE_BRANCH+x} ]; then + clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +fi + # Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds) clone $deforg $defrepo $HEAD # Use the default branch as the last resort. From ca5f8f97bb80d4cdeaee584b7e9ba0c83207a2b2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 16:18:13 +0100 Subject: [PATCH 41/46] Branch matching support for forked repository on GitHub actions --- scripts/fetchdep.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 9844fdc9db..02af402951 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -36,13 +36,19 @@ else head=$(curl $apiEndpoint | jq -r '.head.label') fi -# If head is set, it will contain either: +# If head is set, it will contain on BuilKite either: # * "branch" when the author's branch and target branch are in the same repo # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build # We can split on `:` into an array to check. +# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR +# to determine whether the branch is from a fork or not BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - clone $deforg $defrepo $head + if [[ "$GITHUB_REPOSITORY" = "$deforg/$defrepo" ]]; then + clone $deforg $defrepo $head + else + clone $GITHUB_ACTOR $defrepo $head + fi elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} fi From 174a43f1ef901168f69f535515eedb8e6c7f864c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:37:49 +0100 Subject: [PATCH 42/46] Upgrade matrix-js-sdk to 12.0.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 644793e265..4a4a326a89 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.0.0-rc.1", + "matrix-js-sdk": "12.0.0", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 14cd11d769..82ee9a070e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5711,10 +5711,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.0.0-rc.1: - version "12.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0-rc.1.tgz#b94a72f0549f3000763efb8c7b6fa1f8808e56f6" - integrity sha512-bzozc4w9dF6Dl8xXXLXMpe3FqL/ncczKdB9Y8dL1mPaujVrmLWAai+BYmC9/c4SIw+1zUap9P5W16ej3z7prig== +matrix-js-sdk@12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd" + integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From ddbf0fa77fd798495ce3c5940e4c4f44a607d766 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:46:20 +0100 Subject: [PATCH 43/46] Prepare changelog for v3.24.0 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a14a0f308e..0f979b4802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0) + + * Upgrade to JS SDK 12.0.0 + * [Release] Keep composer reply when scrolling away from a highlighted event + [\#6211](https://github.com/matrix-org/matrix-react-sdk/pull/6211) + * [Release] Remove stray bullet point in reply preview + [\#6210](https://github.com/matrix-org/matrix-react-sdk/pull/6210) + * [Release] Stop requesting null next replies from the server + [\#6209](https://github.com/matrix-org/matrix-react-sdk/pull/6209) + Changes in [3.24.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0-rc.1) (2021-06-15) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0...v3.24.0-rc.1) From d89710defe12a133cfbc9fee664996dcc17761e2 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:53:20 +0100 Subject: [PATCH 44/46] v3.24.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a4a326a89..d592d426a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.24.0-rc.1", + "version": "3.24.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 54c3832b5b9dde5981eb1934f599e7b75eac00e8 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:54:41 +0100 Subject: [PATCH 45/46] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d592d426a6..f232d4301b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./lib/index.js", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -197,6 +197,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From 903f898beeb1e1ddbc3de960197b1b5cc16e36d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 18:28:30 +0200 Subject: [PATCH 46/46] Remove ComposerInsertPayload as this is a JS file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/context_menus/MessageContextMenu.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 2fb2ac4d0e..5a1da1376d 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -33,7 +33,6 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import ForwardDialog from "../dialogs/ForwardDialog"; -import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; export function canCancel(eventStatus) { @@ -201,7 +200,7 @@ export default class MessageContextMenu extends React.Component { }; onQuoteClick = () => { - dis.dispatch({ + dis.dispatch({ action: Action.ComposerInsert, event: this.props.mxEvent, });