From 71d06b4d59bc084be3a5d3c0aa88346a69ebf0b0 Mon Sep 17 00:00:00 2001 From: "mikhail.aheichyk" Date: Tue, 20 Dec 2022 11:01:14 +0300 Subject: [PATCH 01/92] Widget receives updated state events if user is re-invited into the room. --- cypress/e2e/widgets/events.spec.ts | 205 ++++++++++++++++++ .../templates/default/homeserver.yaml | 4 + src/stores/widgets/StopGapWidget.ts | 8 +- 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/widgets/events.spec.ts diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts new file mode 100644 index 0000000000..aeef1d5c76 --- /dev/null +++ b/cypress/e2e/widgets/events.spec.ts @@ -0,0 +1,205 @@ +/* +Copyright 2022 Mikhail Aheichyk +Copyright 2022 Nordeck IT + Consulting GmbH. + +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 { IWidget } from "matrix-widget-api/src/interfaces/IWidget"; + +import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk/src/matrix"; +import { SynapseInstance } from "../../plugins/synapsedocker"; +import { UserCredentials } from "../../support/login"; + +const DEMO_WIDGET_ID = "demo-widget-id"; +const DEMO_WIDGET_NAME = "Demo Widget"; +const DEMO_WIDGET_TYPE = "demo"; +const ROOM_NAME = "Demo"; + +const DEMO_WIDGET_HTML = ` + + + Demo Widget + + + + + + +`; + +function waitForRoom(win: Cypress.AUTWindow, roomId: string, predicate: (room: Room) => boolean): Promise { + const matrixClient = win.mxMatrixClientPeg.get(); + + return new Promise((resolve, reject) => { + const room = matrixClient.getRoom(roomId); + + if (predicate(room)) { + resolve(); + return; + } + + function onEvent(ev: MatrixEvent) { + if (ev.getRoomId() !== roomId) return; + + if (predicate(room)) { + matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent); + resolve(); + } + } + + matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent); + }); +} + +describe("Widget Events", () => { + let synapse: SynapseInstance; + let user: UserCredentials; + let bot: MatrixClient; + let demoWidgetUrl: string; + + beforeEach(() => { + cy.startSynapse("default").then(data => { + synapse = data; + + cy.initTestUser(synapse, "Mike").then(_user => { + user = _user; + }); + cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: true }).then(_bot => { + bot = _bot; + }); + }); + cy.serveHtmlFile(DEMO_WIDGET_HTML).then(url => { + demoWidgetUrl = url; + }); + }); + + afterEach(() => { + cy.stopSynapse(synapse); + cy.stopWebServers(); + }); + + it('should be updated if user is re-invited into the room with updated state event', () => { + cy.createRoom({ + name: ROOM_NAME, + invite: [bot.getUserId()], + }).then(roomId => { + // setup widget via state event + cy.getClient().then(async matrixClient => { + const content: IWidget = { + id: DEMO_WIDGET_ID, + creatorUserId: 'somebody', + type: DEMO_WIDGET_TYPE, + name: DEMO_WIDGET_NAME, + url: demoWidgetUrl, + }; + await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, DEMO_WIDGET_ID); + }).as('widgetEventSent'); + + // set initial layout + cy.getClient().then(async matrixClient => { + const content = { + widgets: { + [DEMO_WIDGET_ID]: { + container: 'top', index: 1, width: 100, height: 0, + }, + }, + }; + await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, ""); + }).as('layoutEventSent'); + + // open the room + cy.viewRoomByName(ROOM_NAME); + + // approve capabilities + cy.contains('.mx_WidgetCapabilitiesPromptDialog button', 'Approve').click(); + + cy.all([ + cy.get("@widgetEventSent"), + cy.get("@layoutEventSent"), + ]).then(async () => { + // bot creates a new room with 'net.metadata_invite_shared' state event + const { room_id: roomNew } = await bot.createRoom({ + name: "New room", + initial_state: [ + { + type: 'net.metadata_invite_shared', + state_key: '', + content: { + value: "initial", + }, + }, + ], + }); + + await bot.invite(roomNew, user.userId); + + // widget should receive 'net.metadata_invite_shared' event after invite + cy.window().then(async win => { + await waitForRoom(win, roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some(e => e.getType() === 'net.widget_echo' + && e.getContent().type === 'net.metadata_invite_shared' + && e.getContent().content.value === 'initial'); + }); + }); + + await bot.sendStateEvent(roomNew, 'net.metadata_invite_shared', { + value: "new_value", + }, ''); + + await bot.invite(roomNew, user.userId, 'something changed in the room'); + + // widget should receive updated 'net.metadata_invite_shared' event after re-invite + cy.window().then(async win => { + await waitForRoom(win, roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some(e => e.getType() === 'net.widget_echo' + && e.getContent().type === 'net.metadata_invite_shared' + && e.getContent().content.value === 'new_value'); + }); + }); + }); + }); + }); +}); diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml index aaad3420b9..e282e790e9 100644 --- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml +++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml @@ -74,3 +74,7 @@ suppress_key_server_warning: true ui_auth: session_timeout: "300s" + +room_prejoin_state: + additional_event_types: + - net.metadata_invite_shared diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 9230dda12e..f7586e86ec 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -520,7 +520,13 @@ export class StopGapWidget extends EventEmitter { } } - this.readUpToMap[ev.getRoomId()] = ev.getId(); + // Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from + // invitation room will assign it and new state events will be not forwarded to the widget + // because of empty timeline for invitation room and assigned marker. + const room = this.client.getRoom(ev.getRoomId()); + if (room && room.getMyMembership() !== 'invite') { + this.readUpToMap[ev.getRoomId()] = ev.getId(); + } const raw = ev.getEffectiveEvent(); this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId).catch((e) => { From e0b6e52d9c0d1faca8a96ee709c508c6cc158367 Mon Sep 17 00:00:00 2001 From: "mikhail.aheichyk" Date: Tue, 20 Dec 2022 12:20:40 +0300 Subject: [PATCH 02/92] prettier --- cypress/e2e/widgets/events.spec.ts | 105 ++++++++++-------- .../templates/default/homeserver.yaml | 4 +- src/stores/widgets/StopGapWidget.ts | 2 +- 3 files changed, 63 insertions(+), 48 deletions(-) diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts index aeef1d5c76..f5ebf0397e 100644 --- a/cypress/e2e/widgets/events.spec.ts +++ b/cypress/e2e/widgets/events.spec.ts @@ -19,7 +19,7 @@ limitations under the License. import { IWidget } from "matrix-widget-api/src/interfaces/IWidget"; -import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk/src/matrix"; +import type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { SynapseInstance } from "../../plugins/synapsedocker"; import { UserCredentials } from "../../support/login"; @@ -99,17 +99,17 @@ describe("Widget Events", () => { let demoWidgetUrl: string; beforeEach(() => { - cy.startSynapse("default").then(data => { + cy.startSynapse("default").then((data) => { synapse = data; - cy.initTestUser(synapse, "Mike").then(_user => { + cy.initTestUser(synapse, "Mike").then((_user) => { user = _user; }); - cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: true }).then(_bot => { + cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: true }).then((_bot) => { bot = _bot; }); }); - cy.serveHtmlFile(DEMO_WIDGET_HTML).then(url => { + cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => { demoWidgetUrl = url; }); }); @@ -119,52 +119,56 @@ describe("Widget Events", () => { cy.stopWebServers(); }); - it('should be updated if user is re-invited into the room with updated state event', () => { + it("should be updated if user is re-invited into the room with updated state event", () => { cy.createRoom({ name: ROOM_NAME, invite: [bot.getUserId()], - }).then(roomId => { + }).then((roomId) => { // setup widget via state event - cy.getClient().then(async matrixClient => { - const content: IWidget = { - id: DEMO_WIDGET_ID, - creatorUserId: 'somebody', - type: DEMO_WIDGET_TYPE, - name: DEMO_WIDGET_NAME, - url: demoWidgetUrl, - }; - await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, DEMO_WIDGET_ID); - }).as('widgetEventSent'); + cy.getClient() + .then(async (matrixClient) => { + const content: IWidget = { + id: DEMO_WIDGET_ID, + creatorUserId: "somebody", + type: DEMO_WIDGET_TYPE, + name: DEMO_WIDGET_NAME, + url: demoWidgetUrl, + }; + await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID); + }) + .as("widgetEventSent"); // set initial layout - cy.getClient().then(async matrixClient => { - const content = { - widgets: { - [DEMO_WIDGET_ID]: { - container: 'top', index: 1, width: 100, height: 0, + cy.getClient() + .then(async (matrixClient) => { + const content = { + widgets: { + [DEMO_WIDGET_ID]: { + container: "top", + index: 1, + width: 100, + height: 0, + }, }, - }, - }; - await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, ""); - }).as('layoutEventSent'); + }; + await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, ""); + }) + .as("layoutEventSent"); // open the room cy.viewRoomByName(ROOM_NAME); // approve capabilities - cy.contains('.mx_WidgetCapabilitiesPromptDialog button', 'Approve').click(); + cy.contains(".mx_WidgetCapabilitiesPromptDialog button", "Approve").click(); - cy.all([ - cy.get("@widgetEventSent"), - cy.get("@layoutEventSent"), - ]).then(async () => { + cy.all([cy.get("@widgetEventSent"), cy.get("@layoutEventSent")]).then(async () => { // bot creates a new room with 'net.metadata_invite_shared' state event const { room_id: roomNew } = await bot.createRoom({ name: "New room", initial_state: [ { - type: 'net.metadata_invite_shared', - state_key: '', + type: "net.metadata_invite_shared", + state_key: "", content: { value: "initial", }, @@ -175,28 +179,39 @@ describe("Widget Events", () => { await bot.invite(roomNew, user.userId); // widget should receive 'net.metadata_invite_shared' event after invite - cy.window().then(async win => { + cy.window().then(async (win) => { await waitForRoom(win, roomId, (room) => { const events = room.getLiveTimeline().getEvents(); - return events.some(e => e.getType() === 'net.widget_echo' - && e.getContent().type === 'net.metadata_invite_shared' - && e.getContent().content.value === 'initial'); + return events.some( + (e) => + e.getType() === "net.widget_echo" && + e.getContent().type === "net.metadata_invite_shared" && + e.getContent().content.value === "initial", + ); }); }); - await bot.sendStateEvent(roomNew, 'net.metadata_invite_shared', { - value: "new_value", - }, ''); + await bot.sendStateEvent( + roomNew, + "net.metadata_invite_shared", + { + value: "new_value", + }, + "", + ); - await bot.invite(roomNew, user.userId, 'something changed in the room'); + await bot.invite(roomNew, user.userId, "something changed in the room"); // widget should receive updated 'net.metadata_invite_shared' event after re-invite - cy.window().then(async win => { + cy.window().then(async (win) => { await waitForRoom(win, roomId, (room) => { const events = room.getLiveTimeline().getEvents(); - return events.some(e => e.getType() === 'net.widget_echo' - && e.getContent().type === 'net.metadata_invite_shared' - && e.getContent().content.value === 'new_value'); + return events.some( + (e) => + e.getType() === "net.widget_echo" && + e.getContent().type === "net.metadata_invite_shared" && + e.getContent().content.value === "new_value", + ); }); }); }); diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml index e282e790e9..f35f5a55e6 100644 --- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml +++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml @@ -76,5 +76,5 @@ ui_auth: session_timeout: "300s" room_prejoin_state: - additional_event_types: - - net.metadata_invite_shared + additional_event_types: + - net.metadata_invite_shared diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index f7586e86ec..9efa14829c 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -524,7 +524,7 @@ export class StopGapWidget extends EventEmitter { // invitation room will assign it and new state events will be not forwarded to the widget // because of empty timeline for invitation room and assigned marker. const room = this.client.getRoom(ev.getRoomId()); - if (room && room.getMyMembership() !== 'invite') { + if (room && room.getMyMembership() !== "invite") { this.readUpToMap[ev.getRoomId()] = ev.getId(); } From 7f0621e84da1965076a8ad14fe76777abc8b5ce9 Mon Sep 17 00:00:00 2001 From: "mikhail.aheichyk" Date: Tue, 20 Dec 2022 12:52:31 +0300 Subject: [PATCH 03/92] ts error fix --- src/stores/widgets/StopGapWidget.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 9efa14829c..dfacefa1cd 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -523,9 +523,11 @@ export class StopGapWidget extends EventEmitter { // Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from // invitation room will assign it and new state events will be not forwarded to the widget // because of empty timeline for invitation room and assigned marker. - const room = this.client.getRoom(ev.getRoomId()); - if (room && room.getMyMembership() !== "invite") { - this.readUpToMap[ev.getRoomId()] = ev.getId(); + if (ev.getRoomId()) { + const room = this.client.getRoom(ev.getRoomId()); + if (room && room.getMyMembership() !== "invite") { + this.readUpToMap[ev.getRoomId()] = ev.getId(); + } } const raw = ev.getEffectiveEvent(); From f1f61cb40d3570f1cddc649d28541c3f7fe5e52f Mon Sep 17 00:00:00 2001 From: "mikhail.aheichyk" Date: Tue, 20 Dec 2022 13:41:13 +0300 Subject: [PATCH 04/92] ts error fix --- src/stores/widgets/StopGapWidget.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index dfacefa1cd..433d681b02 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -523,10 +523,12 @@ export class StopGapWidget extends EventEmitter { // Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from // invitation room will assign it and new state events will be not forwarded to the widget // because of empty timeline for invitation room and assigned marker. - if (ev.getRoomId()) { - const room = this.client.getRoom(ev.getRoomId()); + const evRoomId = ev.getRoomId(); + const evId = ev.getId(); + if (evRoomId && evId) { + const room = this.client.getRoom(evRoomId); if (room && room.getMyMembership() !== "invite") { - this.readUpToMap[ev.getRoomId()] = ev.getId(); + this.readUpToMap[evRoomId] = evId; } } From 7ca1b385d95045c850a6f0f66d7b5456cc945104 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 9 Jan 2023 12:38:19 +0000 Subject: [PATCH 05/92] Force enable threads labs flag --- cypress/e2e/polls/polls.spec.ts | 2 +- cypress/e2e/threads/threads.spec.ts | 2 +- src/MatrixClientPeg.ts | 2 +- src/Unread.ts | 2 +- src/components/structures/MessagePanel.tsx | 2 +- src/components/structures/RoomSearchView.tsx | 2 +- src/components/structures/RoomView.tsx | 2 +- src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 2 +- src/components/views/context_menus/MessageContextMenu.tsx | 4 ++-- src/components/views/messages/MessageActionBar.tsx | 8 ++++---- src/components/views/right_panel/RoomHeaderButtons.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 6 +++--- src/components/views/rooms/SearchResultTile.tsx | 2 +- src/components/views/rooms/SendMessageComposer.tsx | 2 +- .../views/rooms/wysiwyg_composer/utils/message.ts | 2 +- src/settings/Settings.tsx | 4 ++-- src/stores/TypingStore.ts | 2 +- src/stores/right-panel/RightPanelStore.ts | 4 ++-- src/stores/widgets/StopGapWidgetDriver.ts | 2 +- src/utils/Reply.ts | 4 ++-- src/utils/exportUtils/HtmlExport.tsx | 2 +- test/components/structures/TimelinePanel-test.tsx | 8 ++++---- test/components/views/messages/MessageActionBar-test.tsx | 4 ++-- .../views/right_panel/RoomHeaderButtons-test.tsx | 2 +- test/components/views/rooms/EventTile-test.tsx | 2 +- test/components/views/settings/Notifications-test.tsx | 3 ++- 27 files changed, 41 insertions(+), 40 deletions(-) diff --git a/cypress/e2e/polls/polls.spec.ts b/cypress/e2e/polls/polls.spec.ts index c092d4f647..4a7b32eb04 100644 --- a/cypress/e2e/polls/polls.spec.ts +++ b/cypress/e2e/polls/polls.spec.ts @@ -77,7 +77,7 @@ describe("Polls", () => { }; beforeEach(() => { - cy.enableLabsFeature("feature_threadstable"); + cy.enableLabsFeature("feature_threadenabled"); cy.window().then((win) => { win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests }); diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index 7b616bd13f..3a87ab18dc 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -29,7 +29,7 @@ describe("Threads", () => { beforeEach(() => { // Default threads to ON for this spec - cy.enableLabsFeature("feature_threadstable"); + cy.enableLabsFeature("feature_threadenabled"); cy.window().then((win) => { win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests }); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index a18b4e9a6f..f674892bf7 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -218,7 +218,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.lazyLoadMembers = true; opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours - opts.experimentalThreadSupport = SettingsStore.getValue("feature_threadstable"); + opts.experimentalThreadSupport = SettingsStore.getValue("feature_threadenabled"); if (SettingsStore.getValue("feature_sliding_sync")) { const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); diff --git a/src/Unread.ts b/src/Unread.ts index 17fe76f03f..d604a50d79 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -66,7 +66,7 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { // despite the name of the method :(( const readUpToId = room.getEventReadUpTo(myUserId!); - if (!SettingsStore.getValue("feature_threadstable")) { + if (!SettingsStore.getValue("feature_threadenabled")) { // as we don't send RRs for our own messages, make sure we special case that // if *we* sent the last message into the room, we consider it not unread! // Should fix: https://github.com/vector-im/element-web/issues/3263 diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 8942f0abd4..cd3f322369 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -287,7 +287,7 @@ export default class MessagePanel extends React.Component { // and we check this in a hot code path. This is also cached in our // RoomContext, however we still need a fallback for roomless MessagePanels. this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); - this.threadsEnabled = SettingsStore.getValue("feature_threadstable"); + this.threadsEnabled = SettingsStore.getValue("feature_threadenabled"); this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( "showTypingNotifications", diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 269980c6a3..d7a995b5c0 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -100,7 +100,7 @@ export const RoomSearchView = forwardRef( return b.length - a.length; }); - if (SettingsStore.getValue("feature_threadstable")) { + if (SettingsStore.getValue("feature_threadenabled")) { // Process all thread roots returned in this batch of search results // XXX: This won't work for results coming from Seshat which won't include the bundled relationship for (const result of results.results) { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 3de552d1d0..eb034fd2b7 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1182,7 +1182,7 @@ export class RoomView extends React.Component { CHAT_EFFECTS.forEach((effect) => { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { // For initial threads launch, chat effects are disabled see #19731 - if (!SettingsStore.getValue("feature_threadstable") || !ev.isRelation(THREAD_RELATION_TYPE.name)) { + if (!SettingsStore.getValue("feature_threadenabled") || !ev.isRelation(THREAD_RELATION_TYPE.name)) { dis.dispatch({ action: `effects.${effect.command}` }); } } diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 51990a739d..b313a0948e 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -249,7 +249,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => const openFeedback = shouldShowFeedback() ? () => { Modal.createDialog(BetaFeedbackDialog, { - featureId: "feature_threadstable", + featureId: "feature_threadenabled", }); } : null; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index c1fc13bbb1..fedada7c3f 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1688,7 +1688,7 @@ class TimelinePanel extends React.Component { is very tied to the main room timeline, we are forcing the timeline to send read receipts for threaded events */ const isThreadTimeline = this.context.timelineRenderingType === TimelineRenderingType.Thread; - if (SettingsStore.getValue("feature_threadstable") && isThreadTimeline) { + if (SettingsStore.getValue("feature_threadenabled") && isThreadTimeline) { return 0; } const index = this.state.events.findIndex((ev) => ev.getId() === evId); diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index ed4c2ab4cb..295a4452cd 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -71,7 +71,7 @@ const ReplyInThreadButton = ({ mxEvent, closeMenu }: IReplyInThreadButton) => { if (Boolean(relationType) && relationType !== RelationType.Thread) return null; const onClick = (): void => { - if (!SettingsStore.getValue("feature_threadstable")) { + if (!SettingsStore.getValue("feature_threadenabled")) { dis.dispatch({ action: Action.ViewUserSettings, initialTabId: UserTab.Labs, @@ -640,7 +640,7 @@ export default class MessageContextMenu extends React.Component rightClick && contentActionable && canSendMessages && - SettingsStore.getValue("feature_threadstable") && + SettingsStore.getValue("feature_threadenabled") && Thread.hasServerSideSupport && timelineRenderingType !== TimelineRenderingType.Thread ) { diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 1ec7fae751..5b78a2614f 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -204,7 +204,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => { const relationType = mxEvent?.getRelation()?.rel_type; const hasARelation = !!relationType && relationType !== RelationType.Thread; - const threadsEnabled = SettingsStore.getValue("feature_threadstable"); + const threadsEnabled = SettingsStore.getValue("feature_threadenabled"); if (!threadsEnabled && !Thread.hasServerSideSupport) { // hide the prompt if the user would only have degraded mode @@ -216,7 +216,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => { e.preventDefault(); e.stopPropagation(); - if (!SettingsStore.getValue("feature_threadstable")) { + if (!SettingsStore.getValue("feature_threadenabled")) { dis.dispatch({ action: Action.ViewUserSettings, initialTabId: UserTab.Labs, @@ -252,7 +252,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => { {!hasARelation && (
- {SettingsStore.getValue("feature_threadstable") + {SettingsStore.getValue("feature_threadenabled") ? _t("Beta feature") : _t("Beta feature. Click to learn more.")}
@@ -548,7 +548,7 @@ export default class MessageActionBar extends React.PureComponent { ); rightPanelPhaseButtons.set( RightPanelPhases.ThreadPanel, - SettingsStore.getValue("feature_threadstable") ? ( + SettingsStore.getValue("feature_threadenabled") ? ( } } - if (SettingsStore.getValue("feature_threadstable")) { + if (SettingsStore.getValue("feature_threadenabled")) { this.props.mxEvent.on(ThreadEvent.Update, this.updateThread); if (this.thread && !this.supportsThreadNotifications) { @@ -469,7 +469,7 @@ export class UnwrappedEventTile extends React.Component if (this.props.showReactions) { this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated); } - if (SettingsStore.getValue("feature_threadstable")) { + if (SettingsStore.getValue("feature_threadenabled")) { this.props.mxEvent.off(ThreadEvent.Update, this.updateThread); } this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate); @@ -500,7 +500,7 @@ export class UnwrappedEventTile extends React.Component }; private get thread(): Thread | null { - if (!SettingsStore.getValue("feature_threadstable")) { + if (!SettingsStore.getValue("feature_threadenabled")) { return null; } diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 269a35d8a2..f88e71e02b 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -68,7 +68,7 @@ export default class SearchResultTile extends React.Component { const layout = SettingsStore.getValue("layout"); const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); - const threadsEnabled = SettingsStore.getValue("feature_threadstable"); + const threadsEnabled = SettingsStore.getValue("feature_threadenabled"); for (let j = 0; j < timeline.length; j++) { const mxEv = timeline[j]; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 692591fb0b..60ce5d09b4 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -436,7 +436,7 @@ export class SendMessageComposer extends React.Component ( diff --git a/src/stores/TypingStore.ts b/src/stores/TypingStore.ts index 4a572e74d9..8296ebfe5f 100644 --- a/src/stores/TypingStore.ts +++ b/src/stores/TypingStore.ts @@ -65,7 +65,7 @@ export default class TypingStore { if (SettingsStore.getValue("lowBandwidth")) return; // Disable typing notification for threads for the initial launch // before we figure out a better user experience for them - if (SettingsStore.getValue("feature_threadstable") && threadId) return; + if (SettingsStore.getValue("feature_threadenabled") && threadId) return; let currentTyping = this.typingStates[roomId]; if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) { diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index b9e218b369..3aea9a4746 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -278,10 +278,10 @@ export default class RightPanelStore extends ReadyWatchingStore { // (A nicer fix could be to indicate, that the right panel is loading if there is missing state data and re-emit if the data is available) switch (card.phase) { case RightPanelPhases.ThreadPanel: - if (!SettingsStore.getValue("feature_threadstable")) return false; + if (!SettingsStore.getValue("feature_threadenabled")) return false; break; case RightPanelPhases.ThreadView: - if (!SettingsStore.getValue("feature_threadstable")) return false; + if (!SettingsStore.getValue("feature_threadenabled")) return false; if (!card.state.threadHeadEvent) { logger.warn("removed card from right panel because of missing threadHeadEvent in card state"); } diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 42fdf0dea4..3caac0fbca 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -236,7 +236,7 @@ export class StopGapWidgetDriver extends WidgetDriver { // For initial threads launch, chat effects are disabled // see #19731 const isNotThread = content["m.relates_to"].rel_type !== THREAD_RELATION_TYPE.name; - if (!SettingsStore.getValue("feature_threadstable") || isNotThread) { + if (!SettingsStore.getValue("feature_threadenabled") || isNotThread) { dis.dispatch({ action: `effects.${effect.command}` }); } } diff --git a/src/utils/Reply.ts b/src/utils/Reply.ts index b6ee476bf6..7bafd9f66a 100644 --- a/src/utils/Reply.ts +++ b/src/utils/Reply.ts @@ -176,7 +176,7 @@ export function makeReplyMixIn(ev?: MatrixEvent): IEventRelation { }; if (ev.threadRootId) { - if (SettingsStore.getValue("feature_threadstable")) { + if (SettingsStore.getValue("feature_threadenabled")) { mixin.is_falling_back = false; } else { // Clients that do not offer a threading UI should behave as follows when replying, for best interaction @@ -203,7 +203,7 @@ export function shouldDisplayReply(event: MatrixEvent): boolean { const relation = event.getRelation(); if ( - SettingsStore.getValue("feature_threadstable") && + SettingsStore.getValue("feature_threadenabled") && relation?.rel_type === THREAD_RELATION_TYPE.name && relation?.is_falling_back ) { diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 95a2fdbd60..57eb54ce8f 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -62,7 +62,7 @@ export default class HTMLExporter extends Exporter { this.mediaOmitText = !this.exportOptions.attachmentsIncluded ? _t("Media omitted") : _t("Media omitted - file size limit exceeded"); - this.threadsEnabled = SettingsStore.getValue("feature_threadstable"); + this.threadsEnabled = SettingsStore.getValue("feature_threadenabled"); } protected async getRoomAvatar() { diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index d7ff659c9d..1b4bc4d5bc 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -172,7 +172,7 @@ describe("TimelinePanel", () => { const getValueCopy = SettingsStore.getValue; SettingsStore.getValue = jest.fn().mockImplementation((name: string) => { if (name === "sendReadReceipts") return true; - if (name === "feature_threadstable") return false; + if (name === "feature_threadenabled") return false; return getValueCopy(name); }); @@ -186,7 +186,7 @@ describe("TimelinePanel", () => { const getValueCopy = SettingsStore.getValue; SettingsStore.getValue = jest.fn().mockImplementation((name: string) => { if (name === "sendReadReceipts") return false; - if (name === "feature_threadstable") return false; + if (name === "feature_threadenabled") return false; return getValueCopy(name); }); @@ -363,7 +363,7 @@ describe("TimelinePanel", () => { client.supportsExperimentalThreads = () => true; const getValueCopy = SettingsStore.getValue; SettingsStore.getValue = jest.fn().mockImplementation((name: string) => { - if (name === "feature_threadstable") return true; + if (name === "feature_threadenabled") return true; return getValueCopy(name); }); @@ -518,7 +518,7 @@ describe("TimelinePanel", () => { }); it("renders when the last message is an undecryptable thread root", async () => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_threadstable"); + jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_threadenabled"); const client = MatrixClientPeg.get(); client.isRoomEncrypted = () => true; diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx index 8b64f205f0..c5f6c03b78 100644 --- a/test/components/views/messages/MessageActionBar-test.tsx +++ b/test/components/views/messages/MessageActionBar-test.tsx @@ -389,7 +389,7 @@ describe("", () => { describe("when threads feature is not enabled", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation( - (setting) => setting !== "feature_threadstable", + (setting) => setting !== "feature_threadenabled", ); }); @@ -435,7 +435,7 @@ describe("", () => { describe("when threads feature is enabled", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation( - (setting) => setting === "feature_threadstable", + (setting) => setting === "feature_threadenabled", ); }); diff --git a/test/components/views/right_panel/RoomHeaderButtons-test.tsx b/test/components/views/right_panel/RoomHeaderButtons-test.tsx index f9a3572aa8..20416c8f7c 100644 --- a/test/components/views/right_panel/RoomHeaderButtons-test.tsx +++ b/test/components/views/right_panel/RoomHeaderButtons-test.tsx @@ -40,7 +40,7 @@ describe("RoomHeaderButtons-test.tsx", function () { }); jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { - if (name === "feature_threadstable") return true; + if (name === "feature_threadenabled") return true; }); }); diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx index f425bc5aa5..78600ba97c 100644 --- a/test/components/views/rooms/EventTile-test.tsx +++ b/test/components/views/rooms/EventTile-test.tsx @@ -80,7 +80,7 @@ describe("EventTile", () => { jest.spyOn(client, "getRoom").mockReturnValue(room); jest.spyOn(client, "decryptEventIfNeeded").mockResolvedValue(); - jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_threadstable"); + jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_threadenabled"); mxEvent = mkMessage({ room: room.roomId, diff --git a/test/components/views/settings/Notifications-test.tsx b/test/components/views/settings/Notifications-test.tsx index f3bb4abc30..b33f72838a 100644 --- a/test/components/views/settings/Notifications-test.tsx +++ b/test/components/views/settings/Notifications-test.tsx @@ -30,7 +30,7 @@ import { fireEvent, getByTestId, render, screen, waitFor } from "@testing-librar import Notifications from "../../../../src/components/views/settings/Notifications"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { StandardActions } from "../../../../src/notifications/StandardActions"; -import { getMockClientWithEventEmitter, mkMessage } from "../../../test-utils"; +import { getMockClientWithEventEmitter, mkMessage, mockClientMethodsUser } from "../../../test-utils"; // don't pollute test output with error logs from mock rejections jest.mock("matrix-js-sdk/src/logger"); @@ -205,6 +205,7 @@ describe("", () => { }; const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), getPushRules: jest.fn(), getPushers: jest.fn(), getThreePids: jest.fn(), From cce506b1c142bebdaf4c548fb50e11fa3e00b861 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 11 Jan 2023 16:39:44 +0100 Subject: [PATCH 06/92] Fix misaligned timestamps for thread roots which are emotes (#9875) * Fix misaligned timestamps for thread roots which are emotes * Use cross-browser variant instead of webkit-only variant for fill-available --- res/css/views/rooms/_EventTile.pcss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 1169f51388..c7e857fc9c 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -970,6 +970,7 @@ $left-gutter: 64px; font-size: $font-12px; max-width: var(--MessageTimestamp-max-width); position: initial; + margin-left: auto; /* to ensure it's end-aligned even if it's the only element of its parent */ } &:hover { @@ -1297,7 +1298,7 @@ $left-gutter: 64px; .mx_EventTile_details { display: flex; - width: -webkit-fill-available; + width: fill-available; align-items: center; justify-content: space-between; gap: $spacing-8; From c18a2ef452194ec6d94cfee0d789c9fa742e00c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:39:23 +0000 Subject: [PATCH 07/92] Update dependency @types/jest to v29.2.5 (#9891) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index ee1874ea43..a82fc2ba13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2110,9 +2110,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@*", "@types/jest@^29.2.1": - version "29.2.4" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.4.tgz#9c155c4b81c9570dbd183eb8604aa0ae80ba5a5b" - integrity sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A== + version "29.2.5" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.5.tgz#c27f41a9d6253f288d1910d3c5f09484a56b73c0" + integrity sha512-H2cSxkKgVmqNHXP7TC2L/WUorrZu8ZigyRywfVzv6EyBlxj39n4C00hjXYQWsbwqgElaj/CiAeSRmk5GoaKTgw== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -2171,9 +2171,9 @@ integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA== "@types/node@*": - version "18.11.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.15.tgz#de0e1fbd2b22b962d45971431e2ae696643d3f5d" - integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== "@types/node@^14.14.31": version "14.18.34" @@ -2334,9 +2334,9 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.17" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.17.tgz#5672e5621f8e0fca13f433a8017aae4b7a2a03e7" - integrity sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g== + version "17.0.19" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.19.tgz#8dbecdc9ab48bee0cb74f6e3327de3fa0d0c98ae" + integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ== dependencies: "@types/yargs-parser" "*" @@ -3116,7 +3116,12 @@ chokidar@^3.4.0, chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.2" -ci-info@^3.2.0, ci-info@^3.6.1: +ci-info@^3.2.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" + integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + +ci-info@^3.6.1: version "3.7.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== From 07ae84370931f5f34657df4c66270a4236c67d5f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:45:28 +0000 Subject: [PATCH 08/92] Update babel monorepo (#9893) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 187 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 134 insertions(+), 53 deletions(-) diff --git a/yarn.lock b/yarn.lock index a82fc2ba13..28c15fa27f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,9 +41,9 @@ "@jridgewell/trace-mapping" "^0.3.9" "@babel/cli@^7.12.10": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.19.3.tgz#55914ed388e658e0b924b3a95da1296267e278e2" - integrity sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.20.7.tgz#8fc12e85c744a1a617680eacb488fab1fcd35b7c" + integrity sha512-WylgcELHB66WwQqItxNILsMlaTd8/SO6SgTTjMp4uCI7P4QyH1r3nqgFmO3BfM4AtfniHgFMH3EpYFj/zynBkQ== dependencies: "@jridgewell/trace-mapping" "^0.3.8" commander "^4.0.1" @@ -68,25 +68,30 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== +"@babel/compat-data@^7.20.5": + version "7.20.10" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec" + integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg== + "@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" - integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== + version "7.20.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" + integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.5" - "@babel/parser" "^7.20.5" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" + "@babel/generator" "^7.20.7" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helpers" "^7.20.7" + "@babel/parser" "^7.20.7" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.12" + "@babel/types" "^7.20.7" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.1" + json5 "^2.2.2" semver "^6.3.0" "@babel/eslint-parser@^7.12.10": @@ -105,7 +110,16 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.20.5", "@babel/generator@^7.7.2": +"@babel/generator@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.7.tgz#f8ef57c8242665c5929fe2e8d82ba75460187b4a" + integrity sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw== + dependencies: + "@babel/types" "^7.20.7" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/generator@^7.7.2": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== @@ -139,6 +153,17 @@ browserslist "^4.21.3" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" + integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.2", "@babel/helper-create-class-features-plugin@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz#327154eedfb12e977baa4ecc72e5806720a85a06" @@ -213,7 +238,7 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.20.2": +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== @@ -227,6 +252,20 @@ "@babel/traverse" "^7.20.1" "@babel/types" "^7.20.2" +"@babel/helper-module-transforms@^7.20.11": + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" + integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.10" + "@babel/types" "^7.20.7" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -306,14 +345,14 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" -"@babel/helpers@^7.20.5": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" - integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== +"@babel/helpers@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.7.tgz#04502ff0feecc9f20ecfaad120a18f011a8e6dce" + integrity sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA== dependencies: - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.7" + "@babel/types" "^7.20.7" "@babel/highlight@^7.18.6": version "7.18.6" @@ -324,10 +363,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.5", "@babel/parser@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" - integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.5", "@babel/parser@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b" + integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -429,15 +468,15 @@ "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz#a556f59d555f06961df1e572bb5eca864c84022d" - integrity sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== dependencies: - "@babel/compat-data" "^7.20.1" - "@babel/helper-compilation-targets" "^7.20.0" + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.1" + "@babel/plugin-transform-parameters" "^7.20.7" "@babel/plugin-proposal-optional-catch-binding@^7.18.6": version "7.18.6" @@ -799,6 +838,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" +"@babel/plugin-transform-parameters@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz#0ee349e9d1bc96e78e3b37a7af423a4078a7083f" + integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-transform-property-literals@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" @@ -1059,13 +1105,22 @@ regenerator-runtime "^0.13.11" "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.18.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" - integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" + integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== dependencies: regenerator-runtime "^0.13.11" -"@babel/template@^7.18.10", "@babel/template@^7.3.3": +"@babel/template@^7.18.10", "@babel/template@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/template@^7.3.3": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== @@ -1074,23 +1129,23 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.12.12", "@babel/traverse@^7.18.5", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.7.2": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" - integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== +"@babel/traverse@^7.12.12", "@babel/traverse@^7.18.5", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.7.2": + version "7.20.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.12.tgz#7f0f787b3a67ca4475adef1f56cb94f6abd4a4b5" + integrity sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" + "@babel/generator" "^7.20.7" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.5" - "@babel/types" "^7.20.5" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== @@ -1099,6 +1154,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" + integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3027,9 +3091,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001400: - version "1.0.30001435" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz#502c93dbd2f493bee73a408fe98e98fb1dad10b2" - integrity sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA== + version "1.0.30001442" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz#40337f1cf3be7c637b061e2f78582dc1daec0614" + integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow== caseless@~0.12.0: version "0.12.0" @@ -6055,11 +6119,16 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2, json5@^2.2.1: +json5@^2.1.2: version "2.2.1" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -6309,6 +6378,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -6657,9 +6733,9 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" - integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" + integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== normalize-package-data@^2.5.0: version "2.5.0" @@ -8808,6 +8884,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 837115ece3eef8b457ad5fb8abc8bf4fe7e17f47 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 11 Jan 2023 18:56:01 +0100 Subject: [PATCH 09/92] Fix unexpected composer growing (#9889) * Stop the enter event propagation when a message is sent to avoid the composer to grow. * Update @matrix-org/matrix-wysiwyg to 0.16.0 --- package.json | 2 +- .../hooks/useInputEventProcessor.ts | 14 +++++++++++--- yarn.lock | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index cf00952f23..5fa2970973 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.3.0", - "@matrix-org/matrix-wysiwyg": "^0.14.0", + "@matrix-org/matrix-wysiwyg": "^0.16.0", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index d84c6420b7..a94a8db9e9 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { WysiwygInputEvent } from "@matrix-org/matrix-wysiwyg"; +import { WysiwygEvent } from "@matrix-org/matrix-wysiwyg"; import { useCallback } from "react"; import { useSettingValue } from "../../../../../hooks/useSettings"; @@ -22,12 +22,20 @@ import { useSettingValue } from "../../../../../hooks/useSettings"; export function useInputEventProcessor(onSend: () => void) { const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend"); return useCallback( - (event: WysiwygInputEvent) => { + (event: WysiwygEvent) => { if (event instanceof ClipboardEvent) { return event; } - if ((event.inputType === "insertParagraph" && !isCtrlEnter) || event.inputType === "sendMessage") { + const isKeyboardEvent = event instanceof KeyboardEvent; + const isEnterPress = + !isCtrlEnter && (isKeyboardEvent ? event.key === "Enter" : event.inputType === "insertParagraph"); + // sendMessage is sent when ctrl+enter is pressed + const isSendMessage = !isKeyboardEvent && event.inputType === "sendMessage"; + + if (isEnterPress || isSendMessage) { + event.stopPropagation?.(); + event.preventDefault?.(); onSend(); return null; } diff --git a/yarn.lock b/yarn.lock index 28c15fa27f..aba8ea03eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1589,10 +1589,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== -"@matrix-org/matrix-wysiwyg@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.14.0.tgz#359fabf5af403b3f128fe6ede3bff9754a9e18c4" - integrity sha512-iSwIR7kS/zwAzy/8S5cUMv2aceoJl/vIGhqmY9hSU0gVyzmsyaVnx00uNMvVDBUFiiPT2gonN8R3+dxg58TPaQ== +"@matrix-org/matrix-wysiwyg@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.16.0.tgz#2eb81899cedc740f522bd7c2839bd9151d67a28e" + integrity sha512-w+/bUQ5x4lVRncrYSmdxy5ww4kkgXeSg4aFfby9c7c6o/+o4gfV6/XBdoJ71nhStyIYIweKAz8i3zA3rKonyvw== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" From 575508ae15d65cd5149a79bd7aff76a3c803294f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 11 Jan 2023 18:12:38 +0000 Subject: [PATCH 10/92] Rewrite `cy.all` implementation (#9892) The previous implementation was indecipherable, and didn't actually work. --- cypress/support/util.ts | 60 +++++++++++++---------------------------- 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/cypress/support/util.ts b/cypress/support/util.ts index b86bbc27d5..6855379bda 100644 --- a/cypress/support/util.ts +++ b/cypress/support/util.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022-2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,12 +16,6 @@ limitations under the License. /// -// @see https://github.com/cypress-io/cypress/issues/915#issuecomment-475862672 -// Modified due to changes to `cy.queue` https://github.com/cypress-io/cypress/pull/17448 -// Note: this DOES NOT run Promises in parallel like `Promise.all` due to the nature -// of Cypress promise-like objects and command queue. This only makes it convenient to use the same -// API but runs the commands sequentially. - declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { @@ -31,51 +25,35 @@ declare global { all( commands: T, ): Cypress.Chainable<{ [P in keyof T]: ChainableValue }>; - queue: any; - } - - interface Chainable { - chainerId: string; } } } -const chainStart = Symbol("chainStart"); - /** * @description Returns a single Chainable that resolves when all of the Chainables pass. * @param {Cypress.Chainable[]} commands - List of Cypress.Chainable to resolve. * @returns {Cypress.Chainable} Cypress when all Chainables are resolved. */ cy.all = function all(commands): Cypress.Chainable { - const chain = cy.wrap(null, { log: false }); - const stopCommand = Cypress._.find(cy.queue.get(), { - attributes: { chainerId: chain.chainerId }, + const resultArray = []; + + // as each command completes, store the result in the corresponding location of resultArray. + for (let i = 0; i < commands.length; i++) { + commands[i].then((val) => { + resultArray[i] = val; + }); + } + + // add an entry to the log which, when clicked, will write the results to the console. + Cypress.log({ + name: "all", + consoleProps: () => ({ Results: resultArray }), }); - const startCommand = Cypress._.find(cy.queue.get(), { - attributes: { chainerId: commands[0].chainerId }, - }); - const p = chain.then(() => { - return cy.wrap( - // @see https://lodash.com/docs/4.17.15#lodash - Cypress._(commands) - .map((cmd) => { - return cmd[chainStart] - ? cmd[chainStart].attributes - : Cypress._.find(cy.queue.get(), { - attributes: { chainerId: cmd.chainerId }, - }).attributes; - }) - .concat(stopCommand.attributes) - .slice(1) - .map((cmd) => { - return cmd.prev.get("subject"); - }) - .value(), - ); - }); - p[chainStart] = startCommand; - return p; + + // return a chainable which wraps the resultArray. Although this doesn't have a direct dependency on the input + // commands, cypress won't process it until the commands that precede it on the command queue (which must include + // the input commands) have passed. + return cy.wrap(resultArray, { log: false }); }; // Needed to make this file a module From f58d62d339f534416cf1df2283bd6c7c20e90810 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jan 2023 18:21:11 +0000 Subject: [PATCH 11/92] Fix accessing room prop which is optional (#9523) Co-authored-by: Michael Weimann --- src/components/views/rooms/RoomPreviewBar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx index b455388196..e4d677eda2 100644 --- a/src/components/views/rooms/RoomPreviewBar.tsx +++ b/src/components/views/rooms/RoomPreviewBar.tsx @@ -498,9 +498,10 @@ export default class RoomPreviewBar extends React.Component { } const myUserId = MatrixClientPeg.get().getUserId(); - const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.getContent(); + const member = this.props.room?.currentState.getMember(myUserId); + const memberEventContent = member?.events.member?.getContent(); - if (memberEventContent.reason) { + if (memberEventContent?.reason) { reasonElement = ( Date: Wed, 11 Jan 2023 22:36:05 +0000 Subject: [PATCH 12/92] Update typescript-eslint monorepo to v5.48.0 (#9897) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 104 +++++++++++++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/yarn.lock b/yarn.lock index aba8ea03eb..b981915764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2417,13 +2417,13 @@ integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w== "@typescript-eslint/eslint-plugin@^5.35.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz#098abb4c9354e19f460d57ab18bff1f676a6cff0" - integrity sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA== + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz#deee67e399f2cb6b4608c935777110e509d8018c" + integrity sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ== dependencies: - "@typescript-eslint/scope-manager" "5.46.1" - "@typescript-eslint/type-utils" "5.46.1" - "@typescript-eslint/utils" "5.46.1" + "@typescript-eslint/scope-manager" "5.48.1" + "@typescript-eslint/type-utils" "5.48.1" + "@typescript-eslint/utils" "5.48.1" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -2432,71 +2432,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.46.1.tgz#1fc8e7102c1141eb64276c3b89d70da8c0ba5699" - integrity sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg== + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.1.tgz#d0125792dab7e232035434ab8ef0658154db2f10" + integrity sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA== dependencies: - "@typescript-eslint/scope-manager" "5.46.1" - "@typescript-eslint/types" "5.46.1" - "@typescript-eslint/typescript-estree" "5.46.1" + "@typescript-eslint/scope-manager" "5.48.1" + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/typescript-estree" "5.48.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz#70af8425c79bbc1178b5a63fb51102ddf48e104a" - integrity sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA== +"@typescript-eslint/scope-manager@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz#39c71e4de639f5fe08b988005beaaf6d79f9d64d" + integrity sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ== dependencies: - "@typescript-eslint/types" "5.46.1" - "@typescript-eslint/visitor-keys" "5.46.1" + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/visitor-keys" "5.48.1" -"@typescript-eslint/type-utils@5.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz#195033e4b30b51b870dfcf2828e88d57b04a11cc" - integrity sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng== +"@typescript-eslint/type-utils@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz#5d94ac0c269a81a91ad77c03407cea2caf481412" + integrity sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ== dependencies: - "@typescript-eslint/typescript-estree" "5.46.1" - "@typescript-eslint/utils" "5.46.1" + "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/utils" "5.48.1" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.1.tgz#4e9db2107b9a88441c4d5ecacde3bb7a5ebbd47e" - integrity sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w== +"@typescript-eslint/types@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.1.tgz#efd1913a9aaf67caf8a6e6779fd53e14e8587e14" + integrity sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg== -"@typescript-eslint/typescript-estree@5.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz#5358088f98a8f9939355e0996f9c8f41c25eced2" - integrity sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg== +"@typescript-eslint/typescript-estree@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz#9efa8ee2aa471c6ab62e649f6e64d8d121bc2056" + integrity sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA== dependencies: - "@typescript-eslint/types" "5.46.1" - "@typescript-eslint/visitor-keys" "5.46.1" + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/visitor-keys" "5.48.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.1.tgz#7da3c934d9fd0eb4002a6bb3429f33298b469b4a" - integrity sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA== +"@typescript-eslint/utils@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.1.tgz#20f2f4e88e9e2a0961cbebcb47a1f0f7da7ba7f9" + integrity sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.46.1" - "@typescript-eslint/types" "5.46.1" - "@typescript-eslint/typescript-estree" "5.46.1" + "@typescript-eslint/scope-manager" "5.48.1" + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/typescript-estree" "5.48.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz#126cc6fe3c0f83608b2b125c5d9daced61394242" - integrity sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg== +"@typescript-eslint/visitor-keys@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz#79fd4fb9996023ef86849bf6f904f33eb6c8fccb" + integrity sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA== dependencies: - "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/types" "5.48.1" eslint-visitor-keys "^3.3.0" "@wojtekmaj/enzyme-adapter-react-17@^0.8.0": @@ -4549,9 +4549,9 @@ fastest-levenshtein@^1.0.16: integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.14.0.tgz#107f69d7295b11e0fccc264e1fc6389f623731ce" - integrity sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg== + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== dependencies: reusify "^1.0.4" @@ -5160,9 +5160,9 @@ ieee754@^1.1.12, ieee754@^1.1.13: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" - integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== image-size@^1.0.0: version "1.0.2" From 0d7152a8d9e5ab0c25b22766b5e87f150a76356a Mon Sep 17 00:00:00 2001 From: Mikhail Aheichyk Date: Thu, 12 Jan 2023 12:49:24 +0300 Subject: [PATCH 13/92] update the test after merge --- cypress/e2e/widgets/events.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts index f5ebf0397e..daf8e449ba 100644 --- a/cypress/e2e/widgets/events.spec.ts +++ b/cypress/e2e/widgets/events.spec.ts @@ -20,7 +20,7 @@ limitations under the License. import { IWidget } from "matrix-widget-api/src/interfaces/IWidget"; import type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; -import { SynapseInstance } from "../../plugins/synapsedocker"; +import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { UserCredentials } from "../../support/login"; const DEMO_WIDGET_ID = "demo-widget-id"; @@ -93,19 +93,19 @@ function waitForRoom(win: Cypress.AUTWindow, roomId: string, predicate: (room: R } describe("Widget Events", () => { - let synapse: SynapseInstance; + let homeserver: HomeserverInstance; let user: UserCredentials; let bot: MatrixClient; let demoWidgetUrl: string; beforeEach(() => { - cy.startSynapse("default").then((data) => { - synapse = data; + cy.startHomeserver("default").then((data) => { + homeserver = data; - cy.initTestUser(synapse, "Mike").then((_user) => { + cy.initTestUser(homeserver, "Mike").then((_user) => { user = _user; }); - cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: true }).then((_bot) => { + cy.getBot(homeserver, { displayName: "Bot", autoAcceptInvites: true }).then((_bot) => { bot = _bot; }); }); @@ -115,7 +115,7 @@ describe("Widget Events", () => { }); afterEach(() => { - cy.stopSynapse(synapse); + cy.stopHomeserver(homeserver); cy.stopWebServers(); }); From 1b5f06b16f9b1f39c72bbcee9b3f140c1b16393c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 12 Jan 2023 10:38:41 +0000 Subject: [PATCH 14/92] Add a Cypress test for displaying edited events (#9886) MSC3925 is changing this, so let's make sure it keeps working. --- cypress/e2e/editing/editing.spec.ts | 90 ++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/editing/editing.spec.ts b/cypress/e2e/editing/editing.spec.ts index f8d7dd1e3f..ae334ecf92 100644 --- a/cypress/e2e/editing/editing.spec.ts +++ b/cypress/e2e/editing/editing.spec.ts @@ -16,9 +16,9 @@ limitations under the License. /// -import type { MsgType } from "matrix-js-sdk/src/@types/event"; +import type { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests"; -import type { EventType } from "matrix-js-sdk/src/@types/event"; +import type { IContent } from "matrix-js-sdk/src/models/event"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import Chainable = Cypress.Chainable; @@ -29,6 +29,16 @@ const sendEvent = (roomId: string): Chainable => { }); }; +/** generate a message event which will take up some room on the page. */ +function mkPadding(n: number): IContent { + return { + msgtype: "m.text" as MsgType, + body: `padding ${n}`, + format: "org.matrix.custom.html", + formatted_body: `

Test event ${n}

\n`.repeat(10), + }; +} + describe("Editing", () => { let homeserver: HomeserverInstance; @@ -37,7 +47,6 @@ describe("Editing", () => { homeserver = data; cy.initTestUser(homeserver, "Edith").then(() => { cy.injectAxe(); - return cy.createRoom({ name: "Test room" }).as("roomId"); }); }); }); @@ -47,6 +56,8 @@ describe("Editing", () => { }); it("should close the composer when clicking save after making a change and undoing it", () => { + cy.createRoom({ name: "Test room" }).as("roomId"); + cy.get("@roomId").then((roomId) => { sendEvent(roomId); cy.visit("/#/room/" + roomId); @@ -64,4 +75,77 @@ describe("Editing", () => { // Assert that the edit composer has gone away cy.get(".mx_EditMessageComposer").should("not.exist"); }); + + it("should correctly display events which are edited, where we lack the edit event", () => { + // This tests the behaviour when a message has been edited some time after it has been sent, and we + // jump back in room history to view the event, but do not have the actual edit event. + // + // In that scenario, we rely on the server to replace the content (pre-MSC3925), or do it ourselves based on + // the bundled edit event (post-MSC3925). + // + // To test it, we need to have a room with lots of events in, so we can jump around the timeline without + // paginating in the event itself. Hence, we create a bot user which creates the room and populates it before + // we join. + + let testRoomId: string; + let originalEventId: string; + let editEventId: string; + + // create a second user + const bobChainable = cy.getBot(homeserver, { displayName: "Bob", userIdPrefix: "bob_" }); + + cy.all([cy.window({ log: false }), bobChainable]).then(async ([win, bob]) => { + // "bob" now creates the room, and sends a load of events in it. Note that all of this happens via calls on + // the js-sdk rather than Cypress commands, so uses regular async/await. + + const room = await bob.createRoom({ name: "TestRoom", visibility: win.matrixcs.Visibility.Public }); + testRoomId = room.room_id; + cy.log(`Bot user created room ${room.room_id}`); + + originalEventId = (await bob.sendMessage(room.room_id, { body: "original", msgtype: "m.text" })).event_id; + cy.log(`Bot user sent original event ${originalEventId}`); + + // send a load of padding events. We make them large, so that they fill the whole screen + // and the client doesn't end up paginating into the event we want. + let i = 0; + while (i < 20) { + await bob.sendMessage(room.room_id, mkPadding(i++)); + } + + // ... then the edit ... + editEventId = ( + await bob.sendMessage(room.room_id, { + "m.new_content": { body: "Edited body", msgtype: "m.text" }, + "m.relates_to": { + rel_type: "m.replace", + event_id: originalEventId, + }, + "body": "* edited", + "msgtype": "m.text", + }) + ).event_id; + cy.log(`Bot user sent edit event ${editEventId}`); + + // ... then a load more padding ... + while (i < 40) { + await bob.sendMessage(room.room_id, mkPadding(i++)); + } + }); + + cy.getClient().then((cli) => { + // now have the cypress user join the room, jump to the original event, and wait for the event to be + // visible + cy.joinRoom(testRoomId); + cy.viewRoomByName("TestRoom"); + cy.visit(`#/room/${testRoomId}/${originalEventId}`); + cy.get(`[data-event-id="${originalEventId}"]`).should((messageTile) => { + // at this point, the edit event should still be unknown + expect(cli.getRoom(testRoomId).getTimelineForEvent(editEventId)).to.be.null; + + // nevertheless, the event should be updated + expect(messageTile.find(".mx_EventTile_body").text()).to.eq("Edited body"); + expect(messageTile.find(".mx_EventTile_edited")).to.exist; + }); + }); + }); }); From 7a36ba0fde3b40da6d7265beb390ef1935c71380 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 12 Jan 2023 11:54:03 +0100 Subject: [PATCH 15/92] Fix issue where thread dropdown would not show up correctly (#9872) * Fix issue where thread dropdown would not correctly * Write additional test for both issues - Thread dropdown should be shown if there is any thread, even if not participated - Thread list correctly updates after every change of the dropdown immediately --- src/components/structures/ThreadPanel.tsx | 26 +-- .../structures/ThreadPanel-test.tsx | 202 +++++++++++++++++- 2 files changed, 203 insertions(+), 25 deletions(-) diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index b313a0948e..2827ba76be 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Optional } from "matrix-events-sdk"; import React, { useContext, useEffect, useRef, useState } from "react"; import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; import { Thread } from "matrix-js-sdk/src/models/thread"; @@ -215,31 +216,22 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => const [filterOption, setFilterOption] = useState(ThreadFilterType.All); const [room, setRoom] = useState(null); - const [timelineSet, setTimelineSet] = useState(null); const [narrow, setNarrow] = useState(false); + const timelineSet: Optional = + filterOption === ThreadFilterType.My ? room?.threadsTimelineSets[1] : room?.threadsTimelineSets[0]; + const hasThreads = Boolean(room?.threadsTimelineSets?.[0]?.getLiveTimeline()?.getEvents()?.length); + useEffect(() => { const room = mxClient.getRoom(roomId); - room.createThreadsTimelineSets() - .then(() => { - return room.fetchRoomThreads(); - }) + room?.createThreadsTimelineSets() + .then(() => room.fetchRoomThreads()) .then(() => { setFilterOption(ThreadFilterType.All); setRoom(room); }); }, [mxClient, roomId]); - useEffect(() => { - if (room) { - if (filterOption === ThreadFilterType.My) { - setTimelineSet(room.threadsTimelineSets[1]); - } else { - setTimelineSet(room.threadsTimelineSets[0]); - } - } - }, [room, filterOption]); - useEffect(() => { if (timelineSet && !Thread.hasServerSideSupport) { timelinePanel.current.refreshTimeline(); @@ -268,7 +260,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => } footer={ @@ -315,7 +307,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => showUrlPreview={false} // No URL previews at the threads list level empty={ 0} + hasThreads={hasThreads} filterOption={filterOption} showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)} /> diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx index 483e8a9b38..b868549da4 100644 --- a/test/components/structures/ThreadPanel-test.tsx +++ b/test/components/structures/ThreadPanel-test.tsx @@ -14,18 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { mocked } from "jest-mock"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import "focus-visible"; // to fix context menus +import { mocked } from "jest-mock"; +import { MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread"; +import React from "react"; import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../src/components/structures/ThreadPanel"; -import { _t } from "../../../src/languageHandler"; -import ResizeNotifier from "../../../src/utils/ResizeNotifier"; -import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks"; -import { createTestClient, mkStubRoom } from "../../test-utils"; -import { shouldShowFeedback } from "../../../src/utils/Feedback"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../src/contexts/RoomContext"; +import { _t } from "../../../src/languageHandler"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import { shouldShowFeedback } from "../../../src/utils/Feedback"; +import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks"; +import ResizeNotifier from "../../../src/utils/ResizeNotifier"; +import { createTestClient, getRoomContext, mkStubRoom, mockPlatformPeg, stubClient } from "../../test-utils"; +import { mkThread } from "../../test-utils/threads"; jest.mock("../../../src/utils/Feedback"); @@ -122,4 +127,185 @@ describe("ThreadPanel", () => { expect(foundButton).toMatchSnapshot(); }); }); + + describe("Filtering", () => { + const ROOM_ID = "!roomId:example.org"; + const SENDER = "@alice:example.org"; + + let mockClient: MatrixClient; + let room: Room; + + const TestThreadPanel = () => ( + + + + + + ); + + beforeEach(async () => { + jest.clearAllMocks(); + + stubClient(); + mockPlatformPeg(); + mockClient = mocked(MatrixClientPeg.get()); + Thread.setServerSideSupport(FeatureSupport.Stable); + Thread.setServerSideListSupport(FeatureSupport.Stable); + Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); + jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + jest.spyOn(room, "fetchRoomThreads").mockReturnValue(Promise.resolve()); + jest.spyOn(mockClient, "getRoom").mockReturnValue(room); + await room.createThreadsTimelineSets(); + const [allThreads, myThreads] = room.threadsTimelineSets; + jest.spyOn(room, "createThreadsTimelineSets").mockReturnValue(Promise.resolve([allThreads, myThreads])); + }); + + function toggleThreadFilter(container: HTMLElement, newFilter: ThreadFilterType) { + fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!); + const found = screen.queryAllByRole("menuitemradio"); + expect(found).toHaveLength(2); + + const allThreadsContent = `${_t("All threads")}${_t("Shows all threads from current room")}`; + const myThreadsContent = `${_t("My threads")}${_t("Shows all threads you've participated in")}`; + + const allThreadsOption = found.find((it) => it.textContent === allThreadsContent); + const myThreadsOption = found.find((it) => it.textContent === myThreadsContent); + expect(allThreadsOption).toBeTruthy(); + expect(myThreadsOption).toBeTruthy(); + + const toSelect = newFilter === ThreadFilterType.My ? myThreadsOption : allThreadsOption; + fireEvent.click(toSelect!); + } + + type EventData = { sender: string | null; content: string | null }; + + function findEvents(container: HTMLElement): EventData[] { + return Array.from(container.querySelectorAll(".mx_EventTile")).map((el) => { + const sender = el.querySelector(".mx_DisambiguatedProfile_displayName")?.textContent ?? null; + const content = el.querySelector(".mx_EventTile_body")?.textContent ?? null; + return { sender, content }; + }); + } + + function toEventData(event: MatrixEvent): EventData { + return { sender: event.event.sender ?? null, content: event.event.content?.body ?? null }; + } + + it("correctly filters Thread List with multiple threads", async () => { + const otherThread = mkThread({ + room, + client: mockClient, + authorId: SENDER, + participantUserIds: [mockClient.getUserId()!], + }); + + const mixedThread = mkThread({ + room, + client: mockClient, + authorId: SENDER, + participantUserIds: [SENDER, mockClient.getUserId()!], + }); + + const ownThread = mkThread({ + room, + client: mockClient, + authorId: mockClient.getUserId()!, + participantUserIds: [mockClient.getUserId()!], + }); + + const threadRoots = [otherThread.rootEvent, mixedThread.rootEvent, ownThread.rootEvent]; + jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation((_, eventId) => { + const event = threadRoots.find((it) => it.getId() === eventId)?.event; + return event ? Promise.resolve(event) : Promise.reject(); + }); + const [allThreads, myThreads] = room.threadsTimelineSets; + allThreads.addLiveEvent(otherThread.rootEvent); + allThreads.addLiveEvent(mixedThread.rootEvent); + allThreads.addLiveEvent(ownThread.rootEvent); + myThreads.addLiveEvent(mixedThread.rootEvent); + myThreads.addLiveEvent(ownThread.rootEvent); + + let events: EventData[] = []; + const renderResult = render(); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(3); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + expect(events[1]).toEqual(toEventData(mixedThread.rootEvent)); + expect(events[2]).toEqual(toEventData(ownThread.rootEvent)); + await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy()); + toggleThreadFilter(renderResult.container, ThreadFilterType.My); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(2); + }); + expect(events[0]).toEqual(toEventData(mixedThread.rootEvent)); + expect(events[1]).toEqual(toEventData(ownThread.rootEvent)); + toggleThreadFilter(renderResult.container, ThreadFilterType.All); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(3); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + expect(events[1]).toEqual(toEventData(mixedThread.rootEvent)); + expect(events[2]).toEqual(toEventData(ownThread.rootEvent)); + }); + + it("correctly filters Thread List with a single, unparticipated thread", async () => { + const otherThread = mkThread({ + room, + client: mockClient, + authorId: SENDER, + participantUserIds: [mockClient.getUserId()!], + }); + + const threadRoots = [otherThread.rootEvent]; + jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation((_, eventId) => { + const event = threadRoots.find((it) => it.getId() === eventId)?.event; + return event ? Promise.resolve(event) : Promise.reject(); + }); + const [allThreads] = room.threadsTimelineSets; + allThreads.addLiveEvent(otherThread.rootEvent); + + let events: EventData[] = []; + const renderResult = render(); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(1); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy()); + toggleThreadFilter(renderResult.container, ThreadFilterType.My); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(0); + }); + toggleThreadFilter(renderResult.container, ThreadFilterType.All); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(1); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + }); + }); }); From 4f68742076795def0fc55129f0edb4864efe3253 Mon Sep 17 00:00:00 2001 From: Mikhail Aheichyk Date: Thu, 12 Jan 2023 14:37:23 +0300 Subject: [PATCH 16/92] excluded 'leave' membership to fix the issue that events are not received by widget after re-invite --- src/stores/widgets/StopGapWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 4e12e6f8cf..9fab6e66ee 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -526,7 +526,7 @@ export class StopGapWidget extends EventEmitter { const evId = ev.getId(); if (evRoomId && evId) { const room = this.client.getRoom(evRoomId); - if (room && room.getMyMembership() !== "invite") { + if (room && room.getMyMembership() === "join") { this.readUpToMap[evRoomId] = evId; } } From 53a5a578d8d921b647ecb21d0c95518fb9363247 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 12 Jan 2023 13:18:09 +0000 Subject: [PATCH 17/92] Bump analytics-events to 0.4.0 (allowing FavouriteMessages view) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5fa2970973..0ddf1182bf 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.3.0", + "@matrix-org/analytics-events": "^0.4.0", "@matrix-org/matrix-wysiwyg": "^0.16.0", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", From 030b7e90bf8ba09648f539dd378c0909f8773f86 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Jan 2023 13:25:14 +0000 Subject: [PATCH 18/92] Enable `@typescript-eslint/explicit-function-return-type` in /src (#9788) * Enable `@typescript-eslint/explicit-member-accessibility` on /src * Prettier * Enable `@typescript-eslint/explicit-function-return-type` in /src * Fix types * tsc strict fixes * Delint * Fix test * Fix bad merge --- .eslintrc.js | 8 +- src/@types/diff-dom.d.ts | 8 +- src/@types/polyfill.ts | 2 +- src/AsyncWrapper.tsx | 8 +- src/Avatar.ts | 7 +- src/BasePlatform.ts | 4 +- src/BlurhashEncoder.ts | 2 +- src/ContentMessages.ts | 30 ++-- src/DateUtils.ts | 4 +- src/DeviceListener.ts | 43 +++--- src/Editing.ts | 5 +- src/HtmlUtils.tsx | 6 +- src/ImageUtils.ts | 2 +- src/Keyboard.ts | 2 +- src/LegacyCallHandler.tsx | 12 +- src/Lifecycle.ts | 4 +- src/Livestream.ts | 6 +- src/Login.ts | 2 +- src/Markdown.ts | 4 +- src/Modal.tsx | 28 ++-- src/PlatformPeg.ts | 6 +- src/PosthogAnalytics.ts | 8 +- src/PosthogTrackers.ts | 8 +- src/Presence.ts | 10 +- src/ScalarAuthClient.ts | 4 +- src/ScalarMessaging.ts | 6 +- src/SdkConfig.ts | 8 +- src/SecurityManager.ts | 16 +- src/SendHistoryManager.ts | 2 +- src/SlashCommands.tsx | 30 ++-- src/SlidingSyncManager.ts | 2 +- src/Terms.ts | 4 +- src/TextForEvent.tsx | 2 +- src/UserActivity.ts | 24 +-- src/accessibility/RovingTabIndex.tsx | 2 +- src/accessibility/Toolbar.tsx | 2 +- .../context_menu/StyledMenuItemCheckbox.tsx | 4 +- .../context_menu/StyledMenuItemRadio.tsx | 4 +- src/actions/actionCreators.ts | 4 +- .../handlers/viewUserDeviceSettings.ts | 2 +- .../eventindex/ManageEventIndexDialog.tsx | 8 +- .../security/CreateKeyBackupDialog.tsx | 2 +- .../security/CreateSecretStorageDialog.tsx | 10 +- .../dialogs/security/ExportE2eKeysDialog.tsx | 2 +- .../dialogs/security/ImportE2eKeysDialog.tsx | 2 +- src/audio/ManagedPlayback.ts | 2 +- src/audio/Playback.ts | 22 +-- src/audio/PlaybackClock.ts | 14 +- src/audio/PlaybackManager.ts | 4 +- src/audio/PlaybackQueue.ts | 10 +- src/audio/RecorderWorklet.ts | 2 +- src/audio/VoiceMessageRecording.ts | 4 +- src/audio/VoiceRecording.ts | 12 +- src/autocomplete/AutocompleteProvider.tsx | 8 +- src/autocomplete/Autocompleter.ts | 4 +- src/autocomplete/CommandProvider.tsx | 2 +- src/autocomplete/EmojiProvider.tsx | 4 +- src/autocomplete/NotifProvider.tsx | 2 +- src/autocomplete/QueryMatcher.ts | 2 +- src/autocomplete/RoomProvider.tsx | 31 ++-- src/autocomplete/SpaceProvider.tsx | 5 +- src/autocomplete/UserProvider.tsx | 10 +- .../structures/AutoHideScrollbar.tsx | 8 +- .../structures/AutocompleteInput.tsx | 12 +- src/components/structures/ContextMenu.tsx | 49 +++--- src/components/structures/EmbeddedPage.tsx | 2 +- src/components/structures/FileDropTarget.tsx | 8 +- src/components/structures/FilePanel.tsx | 2 +- .../structures/GenericErrorPage.tsx | 2 +- src/components/structures/HomePage.tsx | 15 +- .../structures/HostSignupAction.tsx | 2 +- src/components/structures/InteractiveAuth.tsx | 6 +- src/components/structures/LeftPanel.tsx | 26 ++-- .../structures/LegacyCallEventGrouper.ts | 12 +- src/components/structures/LoggedInView.tsx | 34 ++--- src/components/structures/MatrixChat.tsx | 100 ++++++------- src/components/structures/MessagePanel.tsx | 10 +- .../structures/NonUrgentToastContainer.tsx | 6 +- .../structures/NotificationPanel.tsx | 2 +- .../structures/PictureInPictureDragger.tsx | 22 +-- src/components/structures/PipContainer.tsx | 14 +- src/components/structures/RightPanel.tsx | 6 +- src/components/structures/RoomSearch.tsx | 6 +- src/components/structures/RoomSearchView.tsx | 6 +- src/components/structures/RoomStatusBar.tsx | 2 +- src/components/structures/RoomView.tsx | 140 +++++++++--------- src/components/structures/ScrollPanel.tsx | 10 +- src/components/structures/SpaceHierarchy.tsx | 24 +-- src/components/structures/SpaceRoomView.tsx | 66 ++++++--- src/components/structures/SplashPage.tsx | 2 +- src/components/structures/TabbedView.tsx | 6 +- src/components/structures/ThreadPanel.tsx | 23 +-- src/components/structures/ThreadView.tsx | 12 +- src/components/structures/TimelinePanel.tsx | 35 +++-- src/components/structures/ToastContainer.tsx | 6 +- src/components/structures/UploadBar.tsx | 10 +- src/components/structures/UserMenu.tsx | 40 ++--- .../structures/auth/CompleteSecurity.tsx | 2 +- src/components/structures/auth/E2eSetup.tsx | 2 +- .../structures/auth/ForgotPassword.tsx | 16 +- src/components/structures/auth/Login.tsx | 32 ++-- .../structures/auth/Registration.tsx | 49 +++--- .../structures/auth/SetupEncryptionBody.tsx | 26 ++-- src/components/structures/auth/SoftLogout.tsx | 16 +- .../auth/forgot-password/EnterEmail.tsx | 2 +- .../auth/header/AuthHeaderDisplay.tsx | 2 +- .../auth/header/AuthHeaderModifier.tsx | 2 +- .../auth/header/AuthHeaderProvider.tsx | 2 +- .../views/audio_messages/AudioPlayerBase.tsx | 4 +- src/components/views/audio_messages/Clock.tsx | 2 +- .../views/audio_messages/DurationClock.tsx | 4 +- .../audio_messages/LiveRecordingClock.tsx | 6 +- .../audio_messages/LiveRecordingWaveform.tsx | 6 +- .../views/audio_messages/PlayPauseButton.tsx | 4 +- .../views/audio_messages/PlaybackClock.tsx | 6 +- .../views/audio_messages/PlaybackWaveform.tsx | 8 +- .../views/audio_messages/SeekBar.tsx | 10 +- .../views/audio_messages/Waveform.tsx | 2 +- src/components/views/auth/AuthBody.tsx | 2 +- src/components/views/auth/CaptchaForm.tsx | 12 +- src/components/views/auth/EmailField.tsx | 4 +- .../auth/InteractiveAuthEntryComponents.tsx | 62 ++++---- src/components/views/auth/LoginWithQR.tsx | 12 +- src/components/views/auth/LoginWithQRFlow.tsx | 8 +- .../views/auth/PassphraseConfirmField.tsx | 4 +- src/components/views/auth/PassphraseField.tsx | 8 +- src/components/views/auth/PasswordLogin.tsx | 62 ++++---- .../views/auth/RegistrationForm.tsx | 56 +++---- src/components/views/avatars/BaseAvatar.tsx | 2 +- .../views/avatars/DecoratedRoomAvatar.tsx | 8 +- src/components/views/avatars/MemberAvatar.tsx | 2 +- src/components/views/avatars/RoomAvatar.tsx | 10 +- .../views/beacon/BeaconViewDialog.tsx | 2 +- .../beacon/LeftPanelLiveShareWarning.tsx | 2 +- .../views/beacon/LiveTimeRemaining.tsx | 2 +- .../views/beacon/RoomLiveShareWarning.tsx | 14 +- src/components/views/beta/BetaCard.tsx | 8 +- .../views/context_menus/DeviceContextMenu.tsx | 2 +- .../context_menus/DialpadContextMenu.tsx | 10 +- .../views/context_menus/KebabContextMenu.tsx | 4 +- .../context_menus/LegacyCallContextMenu.tsx | 8 +- .../context_menus/MessageContextMenu.tsx | 9 +- .../views/context_menus/RoomContextMenu.tsx | 12 +- .../context_menus/RoomGeneralContextMenu.tsx | 6 +- .../RoomNotificationContextMenu.tsx | 2 +- .../views/context_menus/SpaceContextMenu.tsx | 24 +-- .../context_menus/ThreadListContextMenu.tsx | 6 +- .../views/context_menus/WidgetContextMenu.tsx | 14 +- .../dialogs/AddExistingToSpaceDialog.tsx | 21 ++- .../dialogs/AnalyticsLearnMoreDialog.tsx | 4 +- .../views/dialogs/AskInviteAnywayDialog.tsx | 2 +- .../views/dialogs/BugReportDialog.tsx | 4 +- .../views/dialogs/BulkRedactDialog.tsx | 4 +- .../views/dialogs/ChangelogDialog.tsx | 4 +- .../dialogs/ConfirmAndWaitRedactDialog.tsx | 2 +- .../views/dialogs/ConfirmRedactDialog.tsx | 7 +- .../views/dialogs/ConfirmUserActionDialog.tsx | 4 +- .../views/dialogs/ConfirmWipeDeviceDialog.tsx | 2 +- .../views/dialogs/CreateRoomDialog.tsx | 30 ++-- .../views/dialogs/CreateSubspaceDialog.tsx | 2 +- .../views/dialogs/CryptoStoreTooNewDialog.tsx | 2 +- .../views/dialogs/DeactivateAccountDialog.tsx | 2 +- .../views/dialogs/DevtoolsDialog.tsx | 4 +- .../views/dialogs/EndPollDialog.tsx | 4 +- src/components/views/dialogs/ErrorDialog.tsx | 4 +- src/components/views/dialogs/ExportDialog.tsx | 10 +- .../views/dialogs/ForwardDialog.tsx | 6 +- .../dialogs/GenericFeatureFeedbackDialog.tsx | 2 +- .../views/dialogs/HostSignupDialog.tsx | 24 +-- src/components/views/dialogs/InfoDialog.tsx | 4 +- src/components/views/dialogs/InviteDialog.tsx | 92 ++++++------ .../KeySignatureUploadFailedDialog.tsx | 2 +- src/components/views/dialogs/LogoutDialog.tsx | 4 +- .../ManageRestrictedJoinRuleDialog.tsx | 6 +- .../views/dialogs/ModalWidgetDialog.tsx | 16 +- .../views/dialogs/ModuleUiDialog.tsx | 2 +- .../dialogs/RegistrationEmailPromptDialog.tsx | 2 +- .../views/dialogs/ReportEventDialog.tsx | 4 +- .../views/dialogs/RoomSettingsDialog.tsx | 6 +- .../views/dialogs/RoomUpgradeDialog.tsx | 4 +- .../dialogs/RoomUpgradeWarningDialog.tsx | 10 +- .../views/dialogs/ScrollableBaseModal.tsx | 4 +- .../views/dialogs/ServerOfflineDialog.tsx | 8 +- .../views/dialogs/ServerPickerDialog.tsx | 18 +-- .../views/dialogs/SeshatResetDialog.tsx | 2 +- src/components/views/dialogs/ShareDialog.tsx | 8 +- .../views/dialogs/SpacePreferencesDialog.tsx | 2 +- src/components/views/dialogs/TermsDialog.tsx | 6 +- .../views/dialogs/UploadConfirmDialog.tsx | 10 +- .../views/dialogs/UserSettingsDialog.tsx | 4 +- .../dialogs/VerificationRequestDialog.tsx | 2 +- .../WidgetCapabilitiesPromptDialog.tsx | 12 +- .../views/dialogs/devtools/AccountData.tsx | 24 +-- .../views/dialogs/devtools/BaseTool.tsx | 4 +- .../views/dialogs/devtools/Event.tsx | 14 +- .../views/dialogs/devtools/FilteredList.tsx | 6 +- .../views/dialogs/devtools/RoomState.tsx | 18 +-- .../views/dialogs/devtools/ServerInfo.tsx | 4 +- .../views/dialogs/devtools/ServersInRoom.tsx | 2 +- .../dialogs/devtools/SettingExplorer.tsx | 22 +-- .../dialogs/devtools/VerificationExplorer.tsx | 5 +- .../views/dialogs/devtools/WidgetExplorer.tsx | 4 +- .../security/AccessSecretStorageDialog.tsx | 30 ++-- .../ConfirmDestroyCrossSigningDialog.tsx | 2 +- .../security/CreateCrossSigningDialog.tsx | 2 +- .../security/RestoreKeyBackupDialog.tsx | 4 +- .../security/SetupEncryptionDialog.tsx | 8 +- .../spotlight/RoomResultContextMenus.tsx | 2 +- .../dialogs/spotlight/SpotlightDialog.tsx | 10 +- .../views/directory/NetworkDropdown.tsx | 10 +- .../views/elements/AccessibleButton.tsx | 2 +- .../elements/AccessibleTooltipButton.tsx | 10 +- .../views/elements/AppPermission.tsx | 2 +- src/components/views/elements/AppTile.tsx | 12 +- .../views/elements/CopyableText.tsx | 4 +- .../elements/DesktopCapturerSourcePicker.tsx | 10 +- .../views/elements/DialPadBackspaceButton.tsx | 2 +- src/components/views/elements/Draggable.tsx | 2 +- src/components/views/elements/Dropdown.tsx | 34 ++--- .../views/elements/EditableItemList.tsx | 18 +-- .../views/elements/EffectsOverlay.tsx | 4 +- .../views/elements/ErrorBoundary.tsx | 4 +- .../views/elements/EventListSummary.tsx | 16 +- .../views/elements/EventTilePreview.tsx | 4 +- src/components/views/elements/Field.tsx | 14 +- .../elements/IRCTimelineProfileResizer.tsx | 8 +- src/components/views/elements/ImageView.tsx | 44 +++--- src/components/views/elements/InfoTooltip.tsx | 2 +- .../views/elements/InlineSpinner.tsx | 2 +- .../views/elements/InteractiveTooltip.tsx | 14 +- .../views/elements/InviteReason.tsx | 4 +- .../views/elements/JoinRuleDropdown.tsx | 4 +- .../views/elements/LabelledToggleSwitch.tsx | 2 +- .../views/elements/LanguageDropdown.tsx | 6 +- src/components/views/elements/LearnMore.tsx | 2 +- src/components/views/elements/Measured.tsx | 10 +- .../views/elements/MiniAvatarUploader.tsx | 2 +- .../views/elements/PersistedElement.tsx | 4 +- src/components/views/elements/Pill.tsx | 2 +- .../views/elements/PollCreateDialog.tsx | 12 +- .../views/elements/PowerSelector.tsx | 4 +- src/components/views/elements/ReplyChain.tsx | 10 +- .../views/elements/RoomAliasField.tsx | 35 +++-- .../views/elements/RoomFacePile.tsx | 2 +- src/components/views/elements/RoomTopic.tsx | 2 +- src/components/views/elements/SSOButtons.tsx | 4 +- .../views/elements/SearchWarning.tsx | 2 +- .../views/elements/ServerPicker.tsx | 8 +- .../views/elements/SettingsFlag.tsx | 8 +- src/components/views/elements/Slider.tsx | 2 +- .../elements/SpellCheckLanguagesDropdown.tsx | 23 ++- src/components/views/elements/Spinner.tsx | 2 +- .../views/elements/StyledCheckbox.tsx | 2 +- .../views/elements/StyledRadioButton.tsx | 2 +- .../views/elements/StyledRadioGroup.tsx | 4 +- src/components/views/elements/Tag.tsx | 2 +- src/components/views/elements/TagComposer.tsx | 8 +- .../views/elements/ToggleSwitch.tsx | 4 +- src/components/views/elements/Tooltip.tsx | 8 +- .../views/elements/TooltipButton.tsx | 2 +- .../views/elements/TruncatedList.tsx | 2 +- .../views/elements/UseCaseSelection.tsx | 2 +- .../views/elements/UseCaseSelectionButton.tsx | 2 +- src/components/views/emojipicker/Category.tsx | 4 +- src/components/views/emojipicker/Emoji.tsx | 2 +- .../views/emojipicker/EmojiPicker.tsx | 20 +-- src/components/views/emojipicker/Header.tsx | 10 +- src/components/views/emojipicker/Preview.tsx | 2 +- .../views/emojipicker/QuickReactions.tsx | 6 +- .../views/emojipicker/ReactionPicker.tsx | 12 +- src/components/views/emojipicker/Search.tsx | 6 +- .../views/host_signup/HostSignupContainer.tsx | 2 +- .../views/location/LiveDurationDropdown.tsx | 4 +- .../views/location/LocationButton.tsx | 6 +- .../views/location/LocationPicker.tsx | 20 +-- .../views/location/LocationShareMenu.tsx | 6 +- .../views/location/LocationViewDialog.tsx | 6 +- src/components/views/location/Map.tsx | 19 ++- src/components/views/location/Marker.tsx | 6 +- src/components/views/location/ShareType.tsx | 2 +- src/components/views/location/ZoomButtons.tsx | 4 +- .../views/location/shareLocation.ts | 4 +- .../views/messages/DateSeparator.tsx | 4 +- .../views/messages/DisambiguatedProfile.tsx | 2 +- .../views/messages/DownloadActionButton.tsx | 6 +- .../views/messages/EditHistoryMessage.tsx | 4 +- .../views/messages/JumpToDatePicker.tsx | 2 +- .../views/messages/LegacyCallEvent.tsx | 8 +- src/components/views/messages/MAudioBody.tsx | 6 +- src/components/views/messages/MBeaconBody.tsx | 2 +- src/components/views/messages/MFileBody.tsx | 12 +- src/components/views/messages/MImageBody.tsx | 14 +- .../views/messages/MImageReplyBody.tsx | 2 +- .../views/messages/MJitsiWidgetEvent.tsx | 2 +- .../messages/MKeyVerificationRequest.tsx | 18 +-- .../views/messages/MLocationBody.tsx | 6 +- src/components/views/messages/MPollBody.tsx | 28 ++-- .../views/messages/MStickerBody.tsx | 4 +- src/components/views/messages/MVideoBody.tsx | 12 +- .../views/messages/MVoiceMessageBody.tsx | 2 +- .../views/messages/MVoiceOrAudioBody.tsx | 2 +- .../views/messages/MessageActionBar.tsx | 4 +- .../views/messages/MessageEvent.tsx | 14 +- .../views/messages/MessageTimestamp.tsx | 2 +- .../views/messages/ReactionsRow.tsx | 18 +-- .../views/messages/ReactionsRowButton.tsx | 8 +- .../messages/ReactionsRowButtonTooltip.tsx | 2 +- .../views/messages/SenderProfile.tsx | 2 +- src/components/views/messages/TextualBody.tsx | 21 +-- .../views/messages/TextualEvent.tsx | 2 +- .../views/messages/TileErrorBoundary.tsx | 4 +- .../views/messages/ViewSourceEvent.tsx | 2 +- src/components/views/right_panel/BaseCard.tsx | 2 +- .../views/right_panel/EncryptionInfo.tsx | 2 +- .../views/right_panel/EncryptionPanel.tsx | 4 +- .../views/right_panel/HeaderButton.tsx | 2 +- .../views/right_panel/HeaderButtons.tsx | 10 +- .../views/right_panel/PinnedMessagesCard.tsx | 6 +- .../views/right_panel/RoomHeaderButtons.tsx | 20 +-- .../views/right_panel/RoomSummaryCard.tsx | 18 +-- .../views/right_panel/TimelineCard.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 87 +++++++---- .../views/right_panel/VerificationPanel.tsx | 31 ++-- .../views/room_settings/AliasSettings.tsx | 36 ++--- .../room_settings/RoomProfileSettings.tsx | 2 +- .../room_settings/RoomPublishSetting.tsx | 6 +- src/components/views/rooms/Autocomplete.tsx | 12 +- src/components/views/rooms/AuxPanel.tsx | 14 +- .../views/rooms/BasicMessageComposer.tsx | 10 +- .../views/rooms/CollapsibleButton.tsx | 8 +- src/components/views/rooms/E2EIcon.tsx | 4 +- .../views/rooms/EditMessageComposer.tsx | 6 +- src/components/views/rooms/EmojiButton.tsx | 6 +- src/components/views/rooms/EntityTile.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 44 +++--- .../EventTile/EventTileThreadToolbar.tsx | 2 +- src/components/views/rooms/ExtraTile.tsx | 4 +- src/components/views/rooms/HistoryTile.tsx | 2 +- .../views/rooms/LinkPreviewGroup.tsx | 2 +- .../views/rooms/LinkPreviewWidget.tsx | 8 +- src/components/views/rooms/MemberList.tsx | 6 +- src/components/views/rooms/MemberTile.tsx | 6 +- .../views/rooms/MessageComposer.tsx | 42 +++--- .../views/rooms/MessageComposerButtons.tsx | 18 +-- .../views/rooms/MessageComposerFormatBar.tsx | 4 +- src/components/views/rooms/NewRoomIntro.tsx | 8 +- .../views/rooms/NotificationBadge.tsx | 12 +- .../StatelessNotificationBadge.tsx | 2 +- .../UnreadNotificationBadge.tsx | 2 +- .../views/rooms/PinnedEventTile.tsx | 8 +- src/components/views/rooms/PresenceLabel.tsx | 2 +- .../views/rooms/ReadReceiptGroup.tsx | 12 +- .../views/rooms/RecentlyViewedButton.tsx | 2 +- src/components/views/rooms/ReplyPreview.tsx | 2 +- src/components/views/rooms/ReplyTile.tsx | 6 +- .../views/rooms/RoomBreadcrumbs.tsx | 8 +- .../views/rooms/RoomContextDetails.tsx | 2 +- src/components/views/rooms/RoomHeader.tsx | 34 ++--- src/components/views/rooms/RoomInfoLine.tsx | 5 +- src/components/views/rooms/RoomList.tsx | 24 +-- src/components/views/rooms/RoomListHeader.tsx | 10 +- src/components/views/rooms/RoomPreviewBar.tsx | 12 +- .../views/rooms/RoomPreviewCard.tsx | 2 +- src/components/views/rooms/RoomSublist.tsx | 56 +++---- src/components/views/rooms/RoomTile.tsx | 42 +++--- src/components/views/rooms/SearchBar.tsx | 8 +- .../views/rooms/SearchResultTile.tsx | 2 +- .../views/rooms/SendMessageComposer.tsx | 6 +- src/components/views/rooms/Stickerpicker.tsx | 2 +- .../views/rooms/ThirdPartyMemberInfo.tsx | 8 +- src/components/views/rooms/ThreadSummary.tsx | 6 +- .../views/rooms/VoiceRecordComposerTile.tsx | 18 +-- .../views/rooms/WhoIsTypingTile.tsx | 8 +- .../rooms/wysiwyg_composer/ComposerContext.ts | 2 +- .../DynamicImportWysiwygComposer.tsx | 16 +- .../wysiwyg_composer/EditWysiwygComposer.tsx | 6 +- .../wysiwyg_composer/SendWysiwygComposer.tsx | 6 +- .../components/EditionButtons.tsx | 6 +- .../wysiwyg_composer/components/Emoji.tsx | 6 +- .../components/FormattingButtons.tsx | 7 +- .../wysiwyg_composer/components/LinkModal.tsx | 12 +- .../components/PlainTextComposer.tsx | 2 +- .../hooks/useComposerFunctions.ts | 8 +- .../wysiwyg_composer/hooks/useEditing.ts | 11 +- .../hooks/useInitialContent.ts | 2 +- .../hooks/useInputEventProcessor.ts | 2 +- .../wysiwyg_composer/hooks/useIsExpanded.ts | 2 +- .../wysiwyg_composer/hooks/useIsFocused.ts | 5 +- .../hooks/usePlainTextInitialization.ts | 2 +- .../hooks/usePlainTextListeners.ts | 13 +- .../wysiwyg_composer/hooks/useSelection.ts | 8 +- .../hooks/useSetCursorPosition.ts | 2 +- .../hooks/useWysiwygEditActionHandler.ts | 2 +- .../hooks/useWysiwygSendActionHandler.ts | 2 +- .../rooms/wysiwyg_composer/hooks/utils.ts | 4 +- .../rooms/wysiwyg_composer/utils/editing.ts | 4 +- .../rooms/wysiwyg_composer/utils/message.ts | 7 +- .../rooms/wysiwyg_composer/utils/selection.ts | 4 +- .../views/settings/AddPrivilegedUsers.tsx | 10 +- src/components/views/settings/BridgeTile.tsx | 2 +- .../views/settings/CrossSigningPanel.tsx | 12 +- .../views/settings/DevicesPanel.tsx | 2 +- .../views/settings/DevicesPanelEntry.tsx | 6 +- .../views/settings/E2eAdvancedPanel.tsx | 2 +- .../views/settings/EventIndexPanel.tsx | 14 +- .../views/settings/FontScalingPanel.tsx | 6 +- .../views/settings/JoinRuleSettings.tsx | 15 +- .../views/settings/Notifications.tsx | 44 +++--- .../views/settings/SecureBackupPanel.tsx | 10 +- src/components/views/settings/SetIdServer.tsx | 41 +++-- .../views/settings/SpellCheckSettings.tsx | 12 +- .../views/settings/ThemeChoicePanel.tsx | 2 +- .../views/settings/UpdateCheckButton.tsx | 10 +- .../settings/devices/DeviceDetailHeading.tsx | 2 +- .../settings/devices/FilteredDeviceList.tsx | 14 +- .../views/settings/devices/deleteDevices.tsx | 2 +- .../views/settings/devices/filter.ts | 2 +- .../views/settings/devices/useOwnDevices.ts | 4 +- .../settings/discovery/EmailAddresses.tsx | 2 +- .../views/settings/discovery/PhoneNumbers.tsx | 2 +- .../tabs/room/AdvancedRoomSettingsTab.tsx | 6 +- .../settings/tabs/room/BridgeSettingsTab.tsx | 4 +- .../tabs/room/NotificationSettingsTab.tsx | 4 +- .../tabs/room/RolesRoomSettingsTab.tsx | 45 +++--- .../tabs/room/SecurityRoomSettingsTab.tsx | 30 ++-- .../tabs/user/AppearanceUserSettingsTab.tsx | 8 +- .../tabs/user/HelpUserSettingsTab.tsx | 12 +- .../tabs/user/MjolnirUserSettingsTab.tsx | 22 +-- .../tabs/user/NotificationUserSettingsTab.tsx | 2 +- .../tabs/user/PreferencesUserSettingsTab.tsx | 8 +- .../tabs/user/SecurityUserSettingsTab.tsx | 6 +- .../settings/tabs/user/SessionManagerTab.tsx | 14 +- .../tabs/user/SidebarUserSettingsTab.tsx | 2 +- .../tabs/user/VoiceUserSettingsTab.tsx | 10 +- .../views/spaces/QuickSettingsButton.tsx | 4 +- .../views/spaces/QuickThemeSwitcher.tsx | 2 +- .../views/spaces/SpaceBasicSettings.tsx | 8 +- .../views/spaces/SpaceChildrenPicker.tsx | 11 +- .../views/spaces/SpaceCreateMenu.tsx | 23 ++- src/components/views/spaces/SpacePanel.tsx | 20 +-- .../views/spaces/SpacePublicShare.tsx | 4 +- .../views/spaces/SpaceSettingsGeneralTab.tsx | 8 +- .../spaces/SpaceSettingsVisibilityTab.tsx | 8 +- .../views/spaces/SpaceTreeLevel.tsx | 23 +-- .../views/toasts/GenericExpiringToast.tsx | 2 +- .../toasts/NonUrgentEchoFailureToast.tsx | 4 +- .../views/toasts/VerificationRequestToast.tsx | 12 +- .../user-onboarding/UserOnboardingButton.tsx | 4 +- .../UserOnboardingFeedback.tsx | 2 +- .../user-onboarding/UserOnboardingHeader.tsx | 4 +- .../user-onboarding/UserOnboardingList.tsx | 10 +- .../user-onboarding/UserOnboardingPage.tsx | 2 +- .../user-onboarding/UserOnboardingTask.tsx | 2 +- .../verification/VerificationShowSas.tsx | 4 +- src/components/views/voip/AudioFeed.tsx | 14 +- .../voip/AudioFeedArrayForLegacyCall.tsx | 8 +- src/components/views/voip/CallView.tsx | 12 +- src/components/views/voip/DialPad.tsx | 6 +- src/components/views/voip/DialPadModal.tsx | 14 +- src/components/views/voip/LegacyCallView.tsx | 6 +- .../LegacyCallView/LegacyCallViewButtons.tsx | 6 +- .../views/voip/LegacyCallViewForRoom.tsx | 14 +- .../views/voip/LegacyCallViewSidebar.tsx | 2 +- src/components/views/voip/VideoFeed.tsx | 22 +-- src/contexts/MatrixClientContext.tsx | 19 ++- src/contexts/RoomContext.ts | 2 +- src/contexts/SDKContext.ts | 2 +- src/createRoom.ts | 8 +- src/dispatcher/dispatch-actions/threads.ts | 2 +- src/dispatcher/dispatcher.ts | 4 +- src/editor/caret.ts | 33 ++++- src/editor/deserialize.ts | 6 +- src/editor/dom.ts | 35 ++++- src/editor/history.ts | 4 +- src/editor/operations.ts | 8 +- src/editor/parts.ts | 4 +- src/editor/range.ts | 2 +- src/effects/confetti/index.ts | 6 +- src/effects/fireworks/index.ts | 14 +- src/effects/hearts/index.ts | 6 +- src/effects/rainfall/index.ts | 4 +- src/effects/snowfall/index.ts | 6 +- src/effects/spaceinvaders/index.ts | 6 +- src/emoji.ts | 4 +- src/emojipicker/recent.ts | 6 +- src/hooks/spotlight/useDebouncedCallback.ts | 8 +- src/hooks/useAccountData.ts | 6 +- src/hooks/useAnimation.ts | 4 +- src/hooks/useAudioDeviceSelection.ts | 11 +- src/hooks/useDispatcher.ts | 5 +- src/hooks/useEventEmitter.ts | 2 +- src/hooks/useFavouriteMessages.ts | 7 +- src/hooks/useLocalEcho.ts | 2 +- src/hooks/useProfileInfo.ts | 7 +- src/hooks/usePublicRoomDirectory.ts | 14 +- src/hooks/useSettings.ts | 2 +- src/hooks/useSlidingSyncRoomSearch.ts | 6 +- src/hooks/useSmoothAnimation.ts | 2 +- src/hooks/useSpaceResults.ts | 2 +- src/hooks/useStateCallback.ts | 2 +- src/hooks/useStateToggle.ts | 2 +- src/hooks/useTimeout.ts | 6 +- src/hooks/useTimeoutToggle.ts | 10 +- src/hooks/useUserDirectory.ts | 7 +- src/hooks/useUserOnboardingContext.ts | 2 +- src/hooks/useUserOnboardingTasks.ts | 4 +- src/indexing/EventIndex.ts | 84 ++++++----- src/indexing/EventIndexPeg.ts | 14 +- src/integrations/IntegrationManagers.ts | 10 +- src/languageHandler.tsx | 30 ++-- src/linkify-matrix.ts | 6 +- src/mjolnir/BanList.ts | 2 +- src/mjolnir/Mjolnir.ts | 18 +-- src/models/Call.ts | 54 +++---- src/models/RoomUpload.ts | 2 +- src/modules/ModuleRunner.ts | 4 +- src/notifications/ContentRules.ts | 4 +- src/notifications/PushRuleVectorState.ts | 4 +- src/performance/index.ts | 4 +- src/phonenumber.ts | 4 +- src/rageshake/rageshake.ts | 6 +- src/rageshake/submit-rageshake.ts | 16 +- src/resizer/distributors/collapse.ts | 6 +- src/resizer/distributors/fixed.ts | 10 +- src/resizer/distributors/percentage.ts | 6 +- src/resizer/item.ts | 28 ++-- src/resizer/resizer.ts | 14 +- src/resizer/sizer.ts | 12 +- src/sendTimePerformanceMetrics.ts | 6 +- src/settings/SettingsStore.ts | 16 +- src/settings/WatchManager.ts | 11 +- .../controllers/FontSizeController.ts | 2 +- .../controllers/NotificationControllers.ts | 2 +- .../controllers/OrderedMultiController.ts | 2 +- .../PushToMatrixClientController.ts | 2 +- .../controllers/ReloadOnChangeController.ts | 2 +- .../controllers/SystemFontController.ts | 2 +- .../controllers/ThreadBetaController.tsx | 2 +- .../controllers/UseSystemFontController.ts | 2 +- .../AbstractLocalStorageSettingsHandler.ts | 4 +- .../handlers/AccountSettingsHandler.ts | 8 +- .../handlers/DefaultSettingsHandler.ts | 2 +- .../handlers/DeviceSettingsHandler.ts | 6 +- .../handlers/PlatformSettingsHandler.ts | 2 +- .../handlers/RoomAccountSettingsHandler.ts | 6 +- src/settings/handlers/RoomSettingsHandler.ts | 6 +- src/settings/watchers/FontWatcher.ts | 15 +- src/settings/watchers/ThemeWatcher.ts | 14 +- src/stores/AsyncStore.ts | 6 +- src/stores/AsyncStoreWithClient.ts | 6 +- src/stores/AutoRageshakeStore.ts | 8 +- src/stores/BreadcrumbsStore.ts | 14 +- src/stores/CallStore.ts | 12 +- src/stores/HostSignupStore.ts | 4 +- src/stores/LifecycleStore.ts | 6 +- src/stores/ModalWidgetStore.ts | 6 +- src/stores/NonUrgentToastStore.ts | 2 +- src/stores/OwnBeaconStore.ts | 20 +-- src/stores/OwnProfileStore.ts | 10 +- src/stores/ReadyWatchingStore.ts | 12 +- src/stores/RoomViewStore.tsx | 4 +- src/stores/SetupEncryptionStore.ts | 10 +- src/stores/ThreepidInviteStore.ts | 2 +- src/stores/ToastStore.ts | 12 +- src/stores/TypingStore.ts | 2 +- src/stores/UIStore.ts | 2 +- src/stores/WidgetEchoStore.ts | 4 +- src/stores/WidgetStore.ts | 12 +- src/stores/local-echo/EchoContext.ts | 6 +- src/stores/local-echo/EchoStore.ts | 2 +- src/stores/local-echo/EchoTransaction.ts | 6 +- src/stores/local-echo/GenericEchoChamber.ts | 12 +- src/stores/local-echo/RoomEchoChamber.ts | 8 +- .../notifications/ListNotificationState.ts | 10 +- src/stores/notifications/NotificationState.ts | 2 +- .../notifications/RoomNotificationState.ts | 18 +-- .../RoomNotificationStateStore.ts | 4 +- .../notifications/SpaceNotificationState.ts | 10 +- .../SummarizedNotificationState.ts | 2 +- .../notifications/ThreadNotificationState.ts | 4 +- src/stores/right-panel/RightPanelStore.ts | 30 ++-- .../right-panel/RightPanelStorePhases.ts | 2 +- src/stores/room-list/ListLayout.ts | 4 +- src/stores/room-list/MessagePreviewStore.ts | 4 +- src/stores/room-list/RoomListLayoutStore.ts | 4 +- src/stores/room-list/RoomListStore.ts | 38 ++--- src/stores/room-list/SlidingRoomListStore.ts | 26 ++-- src/stores/room-list/SpaceWatcher.ts | 6 +- src/stores/room-list/algorithms/Algorithm.ts | 22 +-- .../list-ordering/ImportanceAlgorithm.ts | 6 +- .../list-ordering/OrderingAlgorithm.ts | 2 +- .../algorithms/tag-sorting/ManualAlgorithm.ts | 2 +- .../algorithms/tag-sorting/RecentAlgorithm.ts | 4 +- .../room-list/filters/SpaceFilterCondition.ts | 2 +- .../room-list/filters/VisibilityProvider.ts | 2 +- src/stores/spaces/SpaceStore.ts | 51 +++---- .../spaces/SpaceTreeLevelLayoutStore.ts | 2 +- src/stores/widgets/StopGapWidget.ts | 12 +- src/stores/widgets/StopGapWidgetDriver.ts | 12 +- src/stores/widgets/WidgetLayoutStore.ts | 32 ++-- src/stores/widgets/WidgetMessagingStore.ts | 6 +- src/stores/widgets/WidgetPermissionStore.ts | 2 +- src/theme.ts | 14 +- src/toasts/AnalyticsToast.tsx | 12 +- src/toasts/BulkUnverifiedSessionsToast.ts | 8 +- src/toasts/DesktopNotificationsToast.ts | 8 +- src/toasts/IncomingCallToast.tsx | 6 +- src/toasts/IncomingLegacyCallToast.tsx | 4 +- src/toasts/MobileGuideToast.ts | 8 +- src/toasts/ServerLimitToast.tsx | 9 +- src/toasts/SetupEncryptionToast.ts | 16 +- src/toasts/UnverifiedSessionToast.ts | 10 +- src/toasts/UpdateToast.tsx | 10 +- src/utils/AnimationUtils.ts | 2 +- src/utils/DMRoomMap.ts | 12 +- src/utils/DialogOpener.ts | 4 +- src/utils/EditorStateTransfer.ts | 2 +- src/utils/ErrorUtils.tsx | 2 +- src/utils/FileDownloader.ts | 2 +- src/utils/FixedRollingArray.ts | 2 +- src/utils/MarkedExecution.ts | 6 +- src/utils/MediaEventHelper.ts | 10 +- src/utils/MessageDiffUtils.tsx | 37 +++-- src/utils/MultiInviter.ts | 8 +- src/utils/PasswordScorer.ts | 2 +- src/utils/ResizeNotifier.ts | 18 +-- src/utils/RoomUpgrade.ts | 2 +- src/utils/Singleflight.ts | 6 +- src/utils/SnakedObject.ts | 2 +- src/utils/StorageManager.ts | 34 +++-- src/utils/Timer.ts | 16 +- src/utils/TypeUtils.ts | 2 +- src/utils/ValidatedServerConfig.ts | 2 +- src/utils/Whenable.ts | 4 +- src/utils/WidgetUtils.ts | 26 ++-- src/utils/beacon/geolocation.ts | 4 +- src/utils/beacon/useOwnLiveBeacons.ts | 4 +- src/utils/createVoiceMessageContent.ts | 4 +- .../snoozeBulkUnverifiedDeviceReminder.ts | 4 +- src/utils/exportUtils/Exporter.ts | 2 +- src/utils/exportUtils/HtmlExport.tsx | 24 +-- src/utils/exportUtils/JSONExport.ts | 8 +- src/utils/exportUtils/PlainTextExport.ts | 8 +- src/utils/leave-behaviour.ts | 8 +- src/utils/local-room.ts | 4 +- src/utils/membership.ts | 14 +- .../permalinks/ElementPermalinkConstructor.ts | 2 +- .../MatrixSchemePermalinkConstructor.ts | 2 +- .../MatrixToPermalinkConstructor.ts | 2 +- src/utils/permalinks/Permalinks.ts | 27 ++-- src/utils/pillify.tsx | 4 +- src/utils/presence.ts | 2 +- src/utils/space.tsx | 11 +- src/utils/strings.ts | 2 +- src/utils/tooltipify.tsx | 4 +- src/utils/useTooltip.tsx | 4 +- src/utils/validate/numberInRange.ts | 8 +- src/utils/video-rooms.ts | 2 +- src/verification.ts | 15 +- .../components/VoiceBroadcastBody.tsx | 2 +- .../components/atoms/VoiceBroadcastHeader.tsx | 2 +- .../atoms/VoiceBroadcastRoomSubtitle.tsx | 2 +- .../molecules/VoiceBroadcastPlaybackBody.tsx | 4 +- .../VoiceBroadcastPreRecordingPip.tsx | 6 +- .../molecules/VoiceBroadcastRecordingPip.tsx | 7 +- .../hooks/useCurrentVoiceBroadcastPlayback.ts | 6 +- .../useCurrentVoiceBroadcastPreRecording.ts | 5 +- .../useCurrentVoiceBroadcastRecording.ts | 8 +- .../hooks/useHasRoomLiveVoiceBroadcast.ts | 2 +- .../hooks/useVoiceBroadcastPlayback.ts | 26 +++- .../hooks/useVoiceBroadcastRecording.tsx | 16 +- .../models/VoiceBroadcastPlayback.ts | 4 +- .../models/VoiceBroadcastRecording.ts | 6 +- .../stores/VoiceBroadcastRecordingsStore.ts | 2 +- .../utils/VoiceBroadcastResumer.ts | 2 +- .../checkVoiceBroadcastPreConditions.tsx | 6 +- ...rCurrentVoiceBroadcastPlaybackIfStopped.ts | 2 +- .../shouldDisplayAsVoiceBroadcastTile.ts | 2 +- .../utils/showCantStartACallDialog.tsx | 2 +- .../utils/startNewVoiceBroadcastRecording.ts | 2 +- .../textForVoiceBroadcastStoppedEvent.tsx | 2 +- src/widgets/Jitsi.ts | 2 +- src/widgets/ManagedHybrid.ts | 2 +- 683 files changed, 3459 insertions(+), 3013 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 30e01f86b5..a65f20893b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -100,8 +100,12 @@ module.exports = { files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "cypress/**/*.ts"], extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"], rules: { - // temporary disabled - "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-function-return-type": [ + "error", + { + allowExpressions: true, + }, + ], // Things we do that break the ideal style "prefer-promise-reject-errors": "off", diff --git a/src/@types/diff-dom.d.ts b/src/@types/diff-dom.d.ts index 5998b0e404..bf9150a697 100644 --- a/src/@types/diff-dom.d.ts +++ b/src/@types/diff-dom.d.ts @@ -20,10 +20,10 @@ declare module "diff-dom" { name: string; text?: string; route: number[]; - value: string; - element: unknown; - oldValue: string; - newValue: string; + value: HTMLElement | string; + element: HTMLElement | string; + oldValue: HTMLElement | string; + newValue: HTMLElement | string; } interface IOpts {} diff --git a/src/@types/polyfill.ts b/src/@types/polyfill.ts index d24d2f4463..6434512e75 100644 --- a/src/@types/polyfill.ts +++ b/src/@types/polyfill.ts @@ -15,7 +15,7 @@ limitations under the License. */ // This is intended to fix re-resizer because of its unguarded `instanceof TouchEvent` checks. -export function polyfillTouchEvent() { +export function polyfillTouchEvent(): void { // Firefox doesn't have touch events without touch devices being present, so create a fake // one we can rely on lying about. if (!window.TouchEvent) { diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index f6f7edd2c2..226f5b692b 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -47,7 +47,7 @@ export default class AsyncWrapper extends React.Component { error: null, }; - public componentDidMount() { + public componentDidMount(): void { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/element-web/issues/3148 logger.log("Starting load of AsyncWrapper for modal"); @@ -69,15 +69,15 @@ export default class AsyncWrapper extends React.Component { }); } - public componentWillUnmount() { + public componentWillUnmount(): void { this.unmounted = true; } - private onWrapperCancelClick = () => { + private onWrapperCancelClick = (): void => { this.props.onFinished(false); }; - public render() { + public render(): JSX.Element { if (this.state.component) { const Component = this.state.component; return ; diff --git a/src/Avatar.ts b/src/Avatar.ts index 30a53e74a0..8a3f10a22c 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -137,7 +137,12 @@ export function getInitialLetter(name: string): string { return split(name, "", 1)[0].toUpperCase(); } -export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) { +export function avatarUrlForRoom( + room: Room, + width: number, + height: number, + resizeMethod?: ResizeMethod, +): string | null { if (!room) return null; // null-guard if (room.getMxcAvatarUrl()) { diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 46a406271c..22d274ffb1 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -272,7 +272,7 @@ export default abstract class BasePlatform { return null; } - public setLanguage(preferredLangs: string[]) {} + public setLanguage(preferredLangs: string[]): void {} public setSpellCheckEnabled(enabled: boolean): void {} @@ -280,7 +280,7 @@ export default abstract class BasePlatform { return null; } - public setSpellCheckLanguages(preferredLangs: string[]) {} + public setSpellCheckLanguages(preferredLangs: string[]): void {} public getSpellCheckLanguages(): Promise | null { return null; diff --git a/src/BlurhashEncoder.ts b/src/BlurhashEncoder.ts index 56e137cc01..01f84421b6 100644 --- a/src/BlurhashEncoder.ts +++ b/src/BlurhashEncoder.ts @@ -40,7 +40,7 @@ export class BlurhashEncoder { this.worker.onmessage = this.onMessage; } - private onMessage = (ev: MessageEvent) => { + private onMessage = (ev: MessageEvent): void => { const { seq, blurhash } = ev.data; const deferred = this.pendingDeferredMap.get(seq); if (deferred) { diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index a2fe27d0a9..1381e9431e 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -68,16 +68,20 @@ interface IMediaConfig { * @param {File} imageFile The file to load in an image element. * @return {Promise} A promise that resolves with the html image element. */ -async function loadImageElement(imageFile: File) { +async function loadImageElement(imageFile: File): Promise<{ + width: number; + height: number; + img: HTMLImageElement; +}> { // Load the file into an html element const img = new Image(); const objectUrl = URL.createObjectURL(imageFile); const imgPromise = new Promise((resolve, reject) => { - img.onload = function () { + img.onload = function (): void { URL.revokeObjectURL(objectUrl); resolve(img); }; - img.onerror = function (e) { + img.onerror = function (e): void { reject(e); }; }); @@ -185,13 +189,13 @@ function loadVideoElement(videoFile: File): Promise { const reader = new FileReader(); - reader.onload = function (ev) { + reader.onload = function (ev): void { // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = async function () { + video.onloadeddata = async function (): Promise { resolve(video); video.pause(); }; - video.onerror = function (e) { + video.onerror = function (e): void { reject(e); }; @@ -206,7 +210,7 @@ function loadVideoElement(videoFile: File): Promise { video.load(); video.play(); }; - reader.onerror = function (e) { + reader.onerror = function (e): void { reject(e); }; reader.readAsDataURL(videoFile); @@ -253,10 +257,10 @@ function infoForVideoFile( function readFileAsArrayBuffer(file: File | Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = function (e) { + reader.onload = function (e): void { resolve(e.target.result as ArrayBuffer); }; - reader.onerror = function (e) { + reader.onerror = function (e): void { reject(e); }; reader.readAsArrayBuffer(file); @@ -461,7 +465,7 @@ export default class ContentMessages { matrixClient: MatrixClient, replyToEvent: MatrixEvent | undefined, promBefore?: Promise, - ) { + ): Promise { const fileName = file.name || _t("Attachment"); const content: Omit & { info: Partial } = { body: fileName, @@ -491,7 +495,7 @@ export default class ContentMessages { this.inprogress.push(upload); dis.dispatch({ action: Action.UploadStarted, upload }); - function onProgress(progress: UploadProgress) { + function onProgress(progress: UploadProgress): void { upload.onProgress(progress); dis.dispatch({ action: Action.UploadProgress, upload }); } @@ -568,7 +572,7 @@ export default class ContentMessages { } } - private isFileSizeAcceptable(file: File) { + private isFileSizeAcceptable(file: File): boolean { if ( this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined && @@ -599,7 +603,7 @@ export default class ContentMessages { }); } - public static sharedInstance() { + public static sharedInstance(): ContentMessages { if (window.mxContentMessages === undefined) { window.mxContentMessages = new ContentMessages(); } diff --git a/src/DateUtils.ts b/src/DateUtils.ts index 1dab03121e..5973a7c5f2 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -188,7 +188,7 @@ export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): bo return prevEventDate.getDay() !== nextEventDate.getDay(); } -export function formatFullDateNoDay(date: Date) { +export function formatFullDateNoDay(date: Date): string { return _t("%(date)s at %(time)s", { date: date.toLocaleDateString().replace(/\//g, "-"), time: date.toLocaleTimeString().replace(/:/g, "-"), @@ -205,7 +205,7 @@ export function formatFullDateNoDayISO(date: Date): string { return date.toISOString(); } -export function formatFullDateNoDayNoTime(date: Date) { +export function formatFullDateNoDayNoTime(date: Date): string { return date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate()); } diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 3d33ff0fda..be48717415 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -19,6 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { ClientEvent, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { SyncState } from "matrix-js-sdk/src/sync"; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { MatrixClientPeg } from "./MatrixClientPeg"; import dis from "./dispatcher/dispatcher"; @@ -56,7 +57,7 @@ export default class DeviceListener { // has the user dismissed any of the various nag toasts to setup encryption on this device? private dismissedThisDeviceToast = false; // cache of the key backup info - private keyBackupInfo: object = null; + private keyBackupInfo: IKeyBackupInfo | null = null; private keyBackupFetchedAt: number = null; private keyBackupStatusChecked = false; // We keep a list of our own device IDs so we can batch ones that were already @@ -70,12 +71,12 @@ export default class DeviceListener { private enableBulkUnverifiedSessionsReminder = true; private deviceClientInformationSettingWatcherRef: string | undefined; - public static sharedInstance() { + public static sharedInstance(): DeviceListener { if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener(); return window.mxDeviceListener; } - public start() { + public start(): void { this.running = true; MatrixClientPeg.get().on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); MatrixClientPeg.get().on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); @@ -98,7 +99,7 @@ export default class DeviceListener { this.updateClientInformation(); } - public stop() { + public stop(): void { this.running = false; if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); @@ -134,7 +135,7 @@ export default class DeviceListener { * * @param {String[]} deviceIds List of device IDs to dismiss notifications for */ - public async dismissUnverifiedSessions(deviceIds: Iterable) { + public async dismissUnverifiedSessions(deviceIds: Iterable): Promise { logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(",")); for (const d of deviceIds) { this.dismissed.add(d); @@ -143,19 +144,19 @@ export default class DeviceListener { this.recheck(); } - public dismissEncryptionSetup() { + public dismissEncryptionSetup(): void { this.dismissedThisDeviceToast = true; this.recheck(); } - private ensureDeviceIdsAtStartPopulated() { + private ensureDeviceIdsAtStartPopulated(): void { if (this.ourDeviceIdsAtStart === null) { const cli = MatrixClientPeg.get(); this.ourDeviceIdsAtStart = new Set(cli.getStoredDevicesForUser(cli.getUserId()).map((d) => d.deviceId)); } } - private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => { + private onWillUpdateDevices = async (users: string[], initialFetch?: boolean): Promise => { // If we didn't know about *any* devices before (ie. it's fresh login), // then they are all pre-existing devices, so ignore this and set the // devicesAtStart list to the devices that we see after the fetch. @@ -168,26 +169,26 @@ export default class DeviceListener { // before we download any new ones. }; - private onDevicesUpdated = (users: string[]) => { + private onDevicesUpdated = (users: string[]): void => { if (!users.includes(MatrixClientPeg.get().getUserId())) return; this.recheck(); }; - private onDeviceVerificationChanged = (userId: string) => { + private onDeviceVerificationChanged = (userId: string): void => { if (userId !== MatrixClientPeg.get().getUserId()) return; this.recheck(); }; - private onUserTrustStatusChanged = (userId: string) => { + private onUserTrustStatusChanged = (userId: string): void => { if (userId !== MatrixClientPeg.get().getUserId()) return; this.recheck(); }; - private onCrossSingingKeysChanged = () => { + private onCrossSingingKeysChanged = (): void => { this.recheck(); }; - private onAccountData = (ev: MatrixEvent) => { + private onAccountData = (ev: MatrixEvent): void => { // User may have: // * migrated SSSS to symmetric // * uploaded keys to secret storage @@ -202,13 +203,13 @@ export default class DeviceListener { } }; - private onSync = (state: SyncState, prevState?: SyncState) => { + private onSync = (state: SyncState, prevState?: SyncState): void => { if (state === "PREPARED" && prevState === null) { this.recheck(); } }; - private onRoomStateEvents = (ev: MatrixEvent) => { + private onRoomStateEvents = (ev: MatrixEvent): void => { if (ev.getType() !== EventType.RoomEncryption) return; // If a room changes to encrypted, re-check as it may be our first @@ -216,7 +217,7 @@ export default class DeviceListener { this.recheck(); }; - private onAction = ({ action }: ActionPayload) => { + private onAction = ({ action }: ActionPayload): void => { if (action !== Action.OnLoggedIn) return; this.recheck(); this.updateClientInformation(); @@ -224,7 +225,7 @@ export default class DeviceListener { // The server doesn't tell us when key backup is set up, so we poll // & cache the result - private async getKeyBackupInfo() { + private async getKeyBackupInfo(): Promise { const now = new Date().getTime(); if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); @@ -233,7 +234,7 @@ export default class DeviceListener { return this.keyBackupInfo; } - private shouldShowSetupEncryptionToast() { + private shouldShowSetupEncryptionToast(): boolean { // If we're in the middle of a secret storage operation, we're likely // modifying the state involved here, so don't add new toasts to setup. if (isSecretStorageBeingAccessed()) return false; @@ -242,7 +243,7 @@ export default class DeviceListener { return cli && cli.getRooms().some((r) => cli.isRoomEncrypted(r.roomId)); } - private async recheck() { + private async recheck(): Promise { if (!this.running) return; // we have been stopped const cli = MatrixClientPeg.get(); @@ -359,7 +360,7 @@ export default class DeviceListener { this.displayingToastsForDeviceIds = newUnverifiedDeviceIds; } - private checkKeyBackupStatus = async () => { + private checkKeyBackupStatus = async (): Promise => { if (this.keyBackupStatusChecked) { return; } @@ -388,7 +389,7 @@ export default class DeviceListener { } }; - private updateClientInformation = async () => { + private updateClientInformation = async (): Promise => { try { if (this.shouldRecordClientInformation) { await recordClientInformation(MatrixClientPeg.get(), SdkConfig.get(), PlatformPeg.get()); diff --git a/src/Editing.ts b/src/Editing.ts index 57e58cc2a7..e331a3dca1 100644 --- a/src/Editing.ts +++ b/src/Editing.ts @@ -16,5 +16,6 @@ limitations under the License. import { TimelineRenderingType } from "./contexts/RoomContext"; -export const editorRoomKey = (roomId: string, context: TimelineRenderingType) => `mx_edit_room_${roomId}_${context}`; -export const editorStateKey = (eventId: string) => `mx_edit_state_${eventId}`; +export const editorRoomKey = (roomId: string, context: TimelineRenderingType): string => + `mx_edit_room_${roomId}_${context}`; +export const editorStateKey = (eventId: string): string => `mx_edit_state_${eventId}`; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 19da52f45b..3e67e42256 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -449,9 +449,9 @@ export interface IOptsReturnString extends IOpts { returnString: true; } -const emojiToHtmlSpan = (emoji: string) => +const emojiToHtmlSpan = (emoji: string): string => `${emoji}`; -const emojiToJsxSpan = (emoji: string, key: number) => ( +const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => ( {emoji} @@ -505,7 +505,7 @@ function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | s */ export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnString): string; export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnNode): ReactNode; -export function bodyToHtml(content: IContent, highlights: Optional, opts: IOpts = {}) { +export function bodyToHtml(content: IContent, highlights: Optional, opts: IOpts = {}): ReactNode | string { const isFormattedBody = content.format === "org.matrix.custom.html" && !!content.formatted_body; let bodyHasEmoji = false; let isHtmlMessage = false; diff --git a/src/ImageUtils.ts b/src/ImageUtils.ts index 1618aa8f4c..42db71ebab 100644 --- a/src/ImageUtils.ts +++ b/src/ImageUtils.ts @@ -28,7 +28,7 @@ limitations under the License. * consume in the timeline, when performing scroll offset calculations * (e.g. scroll locking) */ -export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) { +export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number): number { if (!fullWidth || !fullHeight) { // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even // log this because it's spammy diff --git a/src/Keyboard.ts b/src/Keyboard.ts index 3a425cef31..9d4d3f6152 100644 --- a/src/Keyboard.ts +++ b/src/Keyboard.ts @@ -76,7 +76,7 @@ export const Key = { export const IS_MAC = navigator.platform.toUpperCase().includes("MAC"); -export function isOnlyCtrlOrCmdKeyEvent(ev) { +export function isOnlyCtrlOrCmdKeyEvent(ev: KeyboardEvent): boolean { if (IS_MAC) { return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; } else { diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index d81bd73661..82e5cac996 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -169,7 +169,7 @@ export default class LegacyCallHandler extends EventEmitter { private silencedCalls = new Set(); // callIds - public static get instance() { + public static get instance(): LegacyCallHandler { if (!window.mxLegacyCallHandler) { window.mxLegacyCallHandler = new LegacyCallHandler(); } @@ -456,7 +456,7 @@ export default class LegacyCallHandler extends EventEmitter { return callsNotInThatRoom; } - public getAllActiveCallsForPip(roomId: string) { + public getAllActiveCallsForPip(roomId: string): MatrixCall[] { const room = MatrixClientPeg.get().getRoom(roomId); if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) { // This checks if there is space for the call view in the aux panel @@ -478,7 +478,7 @@ export default class LegacyCallHandler extends EventEmitter { const audio = document.getElementById(audioId) as HTMLMediaElement; if (audio) { this.addEventListenersForAudioElement(audio); - const playAudio = async () => { + const playAudio = async (): Promise => { try { if (audio.muted) { logger.error( @@ -524,7 +524,7 @@ export default class LegacyCallHandler extends EventEmitter { // TODO: Attach an invisible element for this instead // which listens? const audio = document.getElementById(audioId) as HTMLMediaElement; - const pauseAudio = () => { + const pauseAudio = (): void => { logger.debug(`${logPrefix} pausing audio`); // pause doesn't return a promise, so just do it audio.pause(); @@ -600,7 +600,7 @@ export default class LegacyCallHandler extends EventEmitter { this.setCallListeners(newCall); this.setCallState(newCall, newCall.state); }); - call.on(CallEvent.AssertedIdentityChanged, async () => { + call.on(CallEvent.AssertedIdentityChanged, async (): Promise => { if (!this.matchesCallForThisRoom(call)) return; logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); @@ -808,7 +808,7 @@ export default class LegacyCallHandler extends EventEmitter { private showICEFallbackPrompt(): void { const cli = MatrixClientPeg.get(); - const code = (sub) => {sub}; + const code = (sub: string): JSX.Element => {sub}; Modal.createDialog( QuestionDialog, { diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index db6d15c188..30aab429fb 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -219,7 +219,7 @@ export function attemptTokenLogin( }) .then(function (creds) { logger.log("Logged in with token"); - return clearStorage().then(async () => { + return clearStorage().then(async (): Promise => { await persistCredentials(creds); // remember that we just logged in sessionStorage.setItem("mx_fresh_login", String(true)); @@ -406,7 +406,7 @@ async function pickleKeyToAesKey(pickleKey: string): Promise { ); } -async function abortLogin() { +async function abortLogin(): Promise { const signOut = await showStorageEvictedDialog(); if (signOut) { await clearStorage(); diff --git a/src/Livestream.ts b/src/Livestream.ts index d339045c94..563136983b 100644 --- a/src/Livestream.ts +++ b/src/Livestream.ts @@ -20,14 +20,14 @@ import { MatrixClientPeg } from "./MatrixClientPeg"; import SdkConfig from "./SdkConfig"; import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; -export function getConfigLivestreamUrl() { +export function getConfigLivestreamUrl(): string | undefined { return SdkConfig.get("audio_stream_url"); } // Dummy rtmp URL used to signal that we want a special audio-only stream const AUDIOSTREAM_DUMMY_URL = "rtmp://audiostream.dummy/"; -async function createLiveStream(roomId: string) { +async function createLiveStream(roomId: string): Promise { const openIdToken = await MatrixClientPeg.get().getOpenIdToken(); const url = getConfigLivestreamUrl() + "/createStream"; @@ -47,7 +47,7 @@ async function createLiveStream(roomId: string) { return respBody["stream_id"]; } -export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) { +export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string): Promise { const streamId = await createLiveStream(roomId); await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, { diff --git a/src/Login.ts b/src/Login.ts index ec769e8cb3..90f8f5d0eb 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -122,7 +122,7 @@ export default class Login { initial_device_display_name: this.defaultDeviceDisplayName, }; - const tryFallbackHs = (originalError) => { + const tryFallbackHs = (originalError: Error): Promise => { return sendLoginRequest(this.fallbackHsUrl, this.isUrl, "m.login.password", loginParams).catch( (fallbackError) => { logger.log("fallback HS login failed", fallbackError); diff --git a/src/Markdown.ts b/src/Markdown.ts index 404da0ca8d..a32126117d 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -56,7 +56,7 @@ function isMultiLine(node: commonmark.Node): boolean { return par.firstChild != par.lastChild; } -function getTextUntilEndOrLinebreak(node: commonmark.Node) { +function getTextUntilEndOrLinebreak(node: commonmark.Node): string { let currentNode = node; let text = ""; while (currentNode !== null && currentNode.type !== "softbreak" && currentNode.type !== "linebreak") { @@ -137,7 +137,7 @@ export default class Markdown { * See: https://github.com/vector-im/element-web/issues/4674 * @param parsed */ - private repairLinks(parsed: commonmark.Node) { + private repairLinks(parsed: commonmark.Node): commonmark.Node { const walker = parsed.walker(); let event: commonmark.NodeWalkingStep = null; let text = ""; diff --git a/src/Modal.tsx b/src/Modal.tsx index 290c082344..1b21f74b5e 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -77,7 +77,7 @@ export class ModalManager extends TypedEventEmitter[] = []; - private static getOrCreateContainer() { + private static getOrCreateContainer(): HTMLElement { let container = document.getElementById(DIALOG_CONTAINER_ID); if (!container) { @@ -89,7 +89,7 @@ export class ModalManager extends TypedEventEmitter 0; + public hasDialogs(): boolean { + return !!this.priorityModal || !!this.staticModal || this.modals.length > 0; } public createDialog( Element: React.ComponentType, ...rest: ParametersWithoutFirst - ) { + ): IHandle { return this.createDialogAsync(Promise.resolve(Element), ...rest); } public appendDialog( Element: React.ComponentType, ...rest: ParametersWithoutFirst - ) { + ): IHandle { return this.appendDialogAsync(Promise.resolve(Element), ...rest); } - public closeCurrentModal(reason: string) { + public closeCurrentModal(reason: string): void { const modal = this.getCurrentModal(); if (!modal) { return; @@ -139,7 +139,11 @@ export class ModalManager extends TypedEventEmitter, className?: string, options?: IOptions, - ) { + ): { + modal: IModal; + closeDialog: IHandle["close"]; + onFinishedProm: IHandle["finished"]; + } { const modal: IModal = { onFinished: props ? props.onFinished : null, onBeforeClose: options.onBeforeClose, @@ -173,7 +177,7 @@ export class ModalManager extends TypedEventEmitter["close"], IHandle["finished"]] { const deferred = defer(); return [ - async (...args: T) => { + async (...args: T): Promise => { if (modal.beforeClosePromise) { await modal.beforeClosePromise; } else if (modal.onBeforeClose) { @@ -302,7 +306,7 @@ export class ModalManager extends TypedEventEmitter { + private onBackgroundClick = (): void => { const modal = this.getCurrentModal(); if (!modal) { return; @@ -320,7 +324,7 @@ export class ModalManager extends TypedEventEmitter { // await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around await sleep(0); diff --git a/src/PlatformPeg.ts b/src/PlatformPeg.ts index ab45f1ab1a..cc7bb8dc17 100644 --- a/src/PlatformPeg.ts +++ b/src/PlatformPeg.ts @@ -32,13 +32,13 @@ import { PlatformSetPayload } from "./dispatcher/payloads/PlatformSetPayload"; * object. */ export class PlatformPeg { - private platform: BasePlatform = null; + private platform: BasePlatform | null = null; /** * Returns the current Platform object for the application. * This should be an instance of a class extending BasePlatform. */ - public get() { + public get(): BasePlatform | null { return this.platform; } @@ -46,7 +46,7 @@ export class PlatformPeg { * Sets the current platform handler object to use for the application. * @param {BasePlatform} platform an instance of a class extending BasePlatform. */ - public set(platform: BasePlatform) { + public set(platform: BasePlatform): void { this.platform = platform; defaultDispatcher.dispatch({ action: Action.PlatformSet, diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 3e1773be29..c8a99ab426 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -175,7 +175,7 @@ export class PosthogAnalytics { this.onLayoutUpdated(); } - private onLayoutUpdated = () => { + private onLayoutUpdated = (): void => { let layout: UserProperties["WebLayout"]; switch (SettingsStore.getValue("layout")) { @@ -195,7 +195,7 @@ export class PosthogAnalytics { this.setProperty("WebLayout", layout); }; - private onAction = (payload: ActionPayload) => { + private onAction = (payload: ActionPayload): void => { if (payload.action !== Action.SettingUpdated) return; const settingsPayload = payload as SettingUpdatedPayload; if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) { @@ -232,7 +232,7 @@ export class PosthogAnalytics { return properties; }; - private registerSuperProperties(properties: Properties) { + private registerSuperProperties(properties: Properties): void { if (this.enabled) { this.posthog.register(properties); } @@ -255,7 +255,7 @@ export class PosthogAnalytics { } // eslint-disable-nextline no-unused-varsx - private capture(eventName: string, properties: Properties, options?: IPostHogEventOptions) { + private capture(eventName: string, properties: Properties, options?: IPostHogEventOptions): void { if (!this.enabled) { return; } diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts index 09c7225a3d..8a8b02965c 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -107,20 +107,20 @@ export default class PosthogTrackers { } export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName }> { - public componentDidMount() { + public componentDidMount(): void { PosthogTrackers.instance.trackOverride(this.props.screenName); } - public componentDidUpdate() { + public componentDidUpdate(): void { // We do not clear the old override here so that we do not send the non-override screen as a transition PosthogTrackers.instance.trackOverride(this.props.screenName); } - public componentWillUnmount() { + public componentWillUnmount(): void { PosthogTrackers.instance.clearOverride(this.props.screenName); } - public render() { + public render(): JSX.Element { return null; // no need to render anything, we just need to hook into the React lifecycle } } diff --git a/src/Presence.ts b/src/Presence.ts index 3684d6f779..c13cc32b60 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -41,7 +41,7 @@ class Presence { * Start listening the user activity to evaluate his presence state. * Any state change will be sent to the homeserver. */ - public async start() { + public async start(): Promise { this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); // the user_activity_start action starts the timer this.dispatcherRef = dis.register(this.onAction); @@ -58,7 +58,7 @@ class Presence { /** * Stop tracking user activity */ - public stop() { + public stop(): void { if (this.dispatcherRef) { dis.unregister(this.dispatcherRef); this.dispatcherRef = null; @@ -73,11 +73,11 @@ class Presence { * Get the current presence state. * @returns {string} the presence state (see PRESENCE enum) */ - public getState() { + public getState(): State { return this.state; } - private onAction = (payload: ActionPayload) => { + private onAction = (payload: ActionPayload): void => { if (payload.action === "user_activity") { this.setState(State.Online); this.unavailableTimer.restart(); @@ -89,7 +89,7 @@ class Presence { * If the state has changed, the homeserver will be notified. * @param {string} newState the new presence state (see PRESENCE enum) */ - private async setState(newState: State) { + private async setState(newState: State): Promise { if (newState === this.state) { return; } diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index baa6e6f632..a775eb3d4e 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -49,7 +49,7 @@ export default class ScalarAuthClient { this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; } - private writeTokenToStore() { + private writeTokenToStore(): void { window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); if (this.isDefaultManager) { // We remove the old token from storage to migrate upwards. This is safe @@ -72,7 +72,7 @@ export default class ScalarAuthClient { return this.readTokenFromStore(); } - public setTermsInteractionCallback(callback) { + public setTermsInteractionCallback(callback: TermsInteractionCallback): void { this.termsInteractionCallback = callback; } diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts index b1912c484a..fec671eab4 100644 --- a/src/ScalarMessaging.ts +++ b/src/ScalarMessaging.ts @@ -711,7 +711,7 @@ function returnStateEvent(event: MessageEvent, roomId: string, eventType: s sendResponse(event, stateEvent.getContent()); } -async function getOpenIdToken(event: MessageEvent) { +async function getOpenIdToken(event: MessageEvent): Promise { try { const tokenObject = await MatrixClientPeg.get().getOpenIdToken(); sendResponse(event, tokenObject); @@ -728,7 +728,7 @@ async function sendEvent( content?: IContent; }>, roomId: string, -) { +): Promise { const eventType = event.data.type; const stateKey = event.data.state_key; const content = event.data.content; @@ -786,7 +786,7 @@ async function readEvents( limit?: number; }>, roomId: string, -) { +): Promise { const eventType = event.data.type; const stateKey = event.data.state_key; const limit = event.data.limit; diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 75cc5ef059..06fd7f45e6 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -56,7 +56,7 @@ export default class SdkConfig { private static instance: IConfigOptions; private static fallback: SnakedObject; - private static setInstance(i: IConfigOptions) { + private static setInstance(i: IConfigOptions): void { SdkConfig.instance = i; SdkConfig.fallback = new SnakedObject(i); @@ -90,18 +90,18 @@ export default class SdkConfig { return val === undefined ? undefined : null; } - public static put(cfg: Partial) { + public static put(cfg: Partial): void { SdkConfig.setInstance({ ...DEFAULTS, ...cfg }); } /** * Resets the config to be completely empty. */ - public static unset() { + public static unset(): void { SdkConfig.setInstance({}); // safe to cast - defaults will be applied } - public static add(cfg: Partial) { + public static add(cfg: Partial): void { SdkConfig.put({ ...SdkConfig.get(), ...cfg }); } } diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index ed72417ecc..20db6594b0 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -86,7 +86,7 @@ async function confirmToDismiss(): Promise { type KeyParams = { passphrase: string; recoveryKey: string }; function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise { - return async ({ passphrase, recoveryKey }) => { + return async ({ passphrase, recoveryKey }): Promise => { if (passphrase) { return deriveKey(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations); } else { @@ -151,7 +151,7 @@ async function getSecretStorageKey({ /* props= */ { keyInfo, - checkPrivateKey: async (input: KeyParams) => { + checkPrivateKey: async (input: KeyParams): Promise => { const key = await inputToKey(input); return MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo); }, @@ -160,7 +160,7 @@ async function getSecretStorageKey({ /* isPriorityModal= */ false, /* isStaticModal= */ false, /* options= */ { - onBeforeClose: async (reason) => { + onBeforeClose: async (reason): Promise => { if (reason === "backgroundClick") { return confirmToDismiss(); } @@ -196,7 +196,7 @@ export async function getDehydrationKey( /* props= */ { keyInfo, - checkPrivateKey: async (input) => { + checkPrivateKey: async (input): Promise => { const key = await inputToKey(input); try { checkFunc(key); @@ -210,7 +210,7 @@ export async function getDehydrationKey( /* isPriorityModal= */ false, /* isStaticModal= */ false, /* options= */ { - onBeforeClose: async (reason) => { + onBeforeClose: async (reason): Promise => { if (reason === "backgroundClick") { return confirmToDismiss(); } @@ -324,7 +324,7 @@ export async function promptForBackupPassphrase(): Promise { * bootstrapped. Optional. * @param {bool} [forceReset] Reset secret storage even if it's already set up */ -export async function accessSecretStorage(func = async () => {}, forceReset = false) { +export async function accessSecretStorage(func = async (): Promise => {}, forceReset = false): Promise { const cli = MatrixClientPeg.get(); secretStorageBeingAccessed = true; try { @@ -342,7 +342,7 @@ export async function accessSecretStorage(func = async () => {}, forceReset = fa /* priority = */ false, /* static = */ true, /* options = */ { - onBeforeClose: async (reason) => { + onBeforeClose: async (reason): Promise => { // If Secure Backup is required, you cannot leave the modal. if (reason === "backgroundClick") { return !isSecureBackupRequired(); @@ -357,7 +357,7 @@ export async function accessSecretStorage(func = async () => {}, forceReset = fa } } else { await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (makeRequest) => { + authUploadDeviceSigningKeys: async (makeRequest): Promise => { const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("Setting up keys"), matrixClient: cli, diff --git a/src/SendHistoryManager.ts b/src/SendHistoryManager.ts index 1fd8f839bd..e95641d4c9 100644 --- a/src/SendHistoryManager.ts +++ b/src/SendHistoryManager.ts @@ -60,7 +60,7 @@ export default class SendHistoryManager { }; } - public save(editorModel: EditorModel, replyEvent?: MatrixEvent) { + public save(editorModel: EditorModel, replyEvent?: MatrixEvent): void { const item = SendHistoryManager.createItem(editorModel, replyEvent); this.history.push(item); this.currentIndex = this.history.length; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 910c077525..9b23bd4138 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -85,7 +85,7 @@ const singleMxcUpload = async (): Promise => { Modal.createDialog(UploadConfirmDialog, { file, - onFinished: async (shouldContinue) => { + onFinished: async (shouldContinue): Promise => { if (shouldContinue) { const { content_uri: uri } = await MatrixClientPeg.get().uploadContent(file); resolve(uri); @@ -151,11 +151,11 @@ export class Command { this.analyticsName = opts.analyticsName; } - public getCommand() { + public getCommand(): string { return `/${this.command}`; } - public getCommandWithArgs() { + public getCommandWithArgs(): string { return this.getCommand() + " " + this.args; } @@ -184,7 +184,7 @@ export class Command { return this.runFn(roomId, args); } - public getUsage() { + public getUsage(): string { return _t("Usage") + ": " + this.getCommandWithArgs(); } @@ -193,15 +193,15 @@ export class Command { } } -function reject(error) { +function reject(error?: any): RunResult { return { error }; } -function success(promise?: Promise) { +function success(promise?: Promise): RunResult { return { promise }; } -function successSync(value: any) { +function successSync(value: any): RunResult { return success(Promise.resolve(value)); } @@ -319,7 +319,7 @@ export const Commands = [ ); return success( - finished.then(async ([resp]) => { + finished.then(async ([resp]): Promise => { if (!resp?.continue) return; await upgradeRoom(room, args, resp.invite); }), @@ -338,7 +338,7 @@ export const Commands = [ runFn: function (roomId, args) { if (args) { return success( - (async () => { + (async (): Promise => { const unixTimestamp = Date.parse(args); if (!unixTimestamp) { throw newTranslatableError( @@ -501,7 +501,9 @@ export const Commands = [ ? ContentHelpers.parseTopicContent(content) : { text: _t("This room has no topic.") }; - const ref = (e) => e && linkifyElement(e); + const ref = (e): void => { + if (e) linkifyElement(e); + }; const body = topicToHtml(topic.text, topic.html, ref, true); Modal.createDialog(InfoDialog, { @@ -1028,7 +1030,7 @@ export const Commands = [ const fingerprint = matches[3]; return success( - (async () => { + (async (): Promise => { const device = cli.getStoredDevice(userId, deviceId); if (!device) { throw newTranslatableError("Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", { @@ -1205,7 +1207,7 @@ export const Commands = [ }, runFn: (roomId) => { return success( - (async () => { + (async (): Promise => { const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId); if (!room) throw newTranslatableError("No virtual room for this room"); dis.dispatch({ @@ -1231,7 +1233,7 @@ export const Commands = [ } return success( - (async () => { + (async (): Promise => { if (isPhoneNumber) { const results = await LegacyCallHandler.instance.pstnLookup(userId); if (!results || results.length === 0 || !results[0].userid) { @@ -1265,7 +1267,7 @@ export const Commands = [ const [userId, msg] = matches.slice(1); if (userId && userId.startsWith("@") && userId.includes(":")) { return success( - (async () => { + (async (): Promise => { const cli = MatrixClientPeg.get(); const roomId = await ensureDMExists(cli, userId); dis.dispatch({ diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index f9ce5ed2a4..75a796df19 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -302,7 +302,7 @@ export class SlidingSyncManager { * @param batchSize The number of rooms to return in each request. * @param gapBetweenRequestsMs The number of milliseconds to wait between requests. */ - public async startSpidering(batchSize: number, gapBetweenRequestsMs: number) { + public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load const listIndex = this.getOrAllocateListIndex(SlidingSyncManager.ListSearch); let startIndex = batchSize; diff --git a/src/Terms.ts b/src/Terms.ts index 101a778a94..bb18a18cf7 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -75,7 +75,7 @@ export type TermsInteractionCallback = ( export async function startTermsFlow( services: Service[], interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback, -) { +): Promise { const termsPromises = services.map((s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl)); /* @@ -176,7 +176,7 @@ export async function startTermsFlow( urlsForService, ); }); - return Promise.all(agreePromises); + await Promise.all(agreePromises); } export async function dialogTermsInteractionCallback( diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 8be6cd4a40..ef7d518e74 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -228,7 +228,7 @@ function textForTombstoneEvent(ev: MatrixEvent): () => string | null { return () => _t("%(senderDisplayName)s upgraded this room.", { senderDisplayName }); } -const onViewJoinRuleSettingsClick = () => { +const onViewJoinRuleSettingsClick = (): void => { defaultDispatcher.dispatch({ action: "open_room_settings", initial_tab_id: ROOM_SECURITY_TAB, diff --git a/src/UserActivity.ts b/src/UserActivity.ts index 0e163564e0..9217aca3c0 100644 --- a/src/UserActivity.ts +++ b/src/UserActivity.ts @@ -50,7 +50,7 @@ export default class UserActivity { this.activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS); } - public static sharedInstance() { + public static sharedInstance(): UserActivity { if (window.mxUserActivity === undefined) { window.mxUserActivity = new UserActivity(window, document); } @@ -66,7 +66,7 @@ export default class UserActivity { * later on when the user does become active. * @param {Timer} timer the timer to use */ - public timeWhileActiveNow(timer: Timer) { + public timeWhileActiveNow(timer: Timer): void { this.timeWhile(timer, this.attachedActiveNowTimers); if (this.userActiveNow()) { timer.start(); @@ -82,14 +82,14 @@ export default class UserActivity { * later on when the user does become active. * @param {Timer} timer the timer to use */ - public timeWhileActiveRecently(timer: Timer) { + public timeWhileActiveRecently(timer: Timer): void { this.timeWhile(timer, this.attachedActiveRecentlyTimers); if (this.userActiveRecently()) { timer.start(); } } - private timeWhile(timer: Timer, attachedTimers: Timer[]) { + private timeWhile(timer: Timer, attachedTimers: Timer[]): void { // important this happens first const index = attachedTimers.indexOf(timer); if (index === -1) { @@ -113,7 +113,7 @@ export default class UserActivity { /** * Start listening to user activity */ - public start() { + public start(): void { this.document.addEventListener("mousedown", this.onUserActivity); this.document.addEventListener("mousemove", this.onUserActivity); this.document.addEventListener("keydown", this.onUserActivity); @@ -133,7 +133,7 @@ export default class UserActivity { /** * Stop tracking user activity */ - public stop() { + public stop(): void { this.document.removeEventListener("mousedown", this.onUserActivity); this.document.removeEventListener("mousemove", this.onUserActivity); this.document.removeEventListener("keydown", this.onUserActivity); @@ -152,7 +152,7 @@ export default class UserActivity { * user's attention at any given moment. * @returns {boolean} true if user is currently 'active' */ - public userActiveNow() { + public userActiveNow(): boolean { return this.activeNowTimeout.isRunning(); } @@ -164,11 +164,11 @@ export default class UserActivity { * (or they may have gone to make tea and left the window focused). * @returns {boolean} true if user has been active recently */ - public userActiveRecently() { + public userActiveRecently(): boolean { return this.activeRecentlyTimeout.isRunning(); } - private onPageVisibilityChanged = (e) => { + private onPageVisibilityChanged = (e): void => { if (this.document.visibilityState === "hidden") { this.activeNowTimeout.abort(); this.activeRecentlyTimeout.abort(); @@ -177,12 +177,12 @@ export default class UserActivity { } }; - private onWindowBlurred = () => { + private onWindowBlurred = (): void => { this.activeNowTimeout.abort(); this.activeRecentlyTimeout.abort(); }; - private onUserActivity = (event: MouseEvent) => { + private onUserActivity = (event: MouseEvent): void => { // ignore anything if the window isn't focused if (!this.document.hasFocus()) return; @@ -214,7 +214,7 @@ export default class UserActivity { } }; - private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) { + private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer): Promise { attachedTimers.forEach((t) => t.start()); try { await timeout.finished(); diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index beb6eee004..e90aed87a9 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -87,7 +87,7 @@ interface IAction { }; } -export const reducer = (state: IState, action: IAction) => { +export const reducer: Reducer = (state: IState, action: IAction) => { switch (action.type) { case Type.Register: { if (!state.activeRef) { diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 22ef018241..3b5fbb0943 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -26,7 +26,7 @@ interface IProps extends Omit, "onKeyDown"> {} // https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar // All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref` const Toolbar: React.FC = ({ children, ...props }) => { - const onKeyDown = (ev: React.KeyboardEvent) => { + const onKeyDown = (ev: React.KeyboardEvent): void => { const target = ev.target as HTMLElement; // Don't interfere with input default keydown behaviour if (target.tagName === "INPUT") return; diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx index 867d3aeaab..ee3a0e4d36 100644 --- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -33,7 +33,7 @@ interface IProps extends React.ComponentProps { export const StyledMenuItemCheckbox: React.FC = ({ children, label, onChange, onClose, ...props }) => { const [onFocus, isActive, ref] = useRovingTabIndex(); - const onKeyDown = (e: React.KeyboardEvent) => { + const onKeyDown = (e: React.KeyboardEvent): void => { let handled = true; const action = getKeyBindingsManager().getAccessibilityAction(e); @@ -55,7 +55,7 @@ export const StyledMenuItemCheckbox: React.FC = ({ children, label, onCh e.preventDefault(); } }; - const onKeyUp = (e: React.KeyboardEvent) => { + const onKeyUp = (e: React.KeyboardEvent): void => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { case KeyBindingAction.Space: diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx index 6bbf5a1106..2fe8738434 100644 --- a/src/accessibility/context_menu/StyledMenuItemRadio.tsx +++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx @@ -33,7 +33,7 @@ interface IProps extends React.ComponentProps { export const StyledMenuItemRadio: React.FC = ({ children, label, onChange, onClose, ...props }) => { const [onFocus, isActive, ref] = useRovingTabIndex(); - const onKeyDown = (e: React.KeyboardEvent) => { + const onKeyDown = (e: React.KeyboardEvent): void => { let handled = true; const action = getKeyBindingsManager().getAccessibilityAction(e); @@ -55,7 +55,7 @@ export const StyledMenuItemRadio: React.FC = ({ children, label, onChang e.preventDefault(); } }; - const onKeyUp = (e: React.KeyboardEvent) => { + const onKeyUp = (e: React.KeyboardEvent): void => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { case KeyBindingAction.Enter: diff --git a/src/actions/actionCreators.ts b/src/actions/actionCreators.ts index 0341f03cac..b6eb263fb9 100644 --- a/src/actions/actionCreators.ts +++ b/src/actions/actionCreators.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { AsyncActionPayload } from "../dispatcher/payloads"; +import { AsyncActionFn, AsyncActionPayload } from "../dispatcher/payloads"; /** * Create an action thunk that will dispatch actions indicating the current @@ -45,7 +45,7 @@ import { AsyncActionPayload } from "../dispatcher/payloads"; * `fn`. */ export function asyncAction(id: string, fn: () => Promise, pendingFn: () => any | null): AsyncActionPayload { - const helper = (dispatch) => { + const helper: AsyncActionFn = (dispatch) => { dispatch({ action: id + ".pending", request: typeof pendingFn === "function" ? pendingFn() : undefined, diff --git a/src/actions/handlers/viewUserDeviceSettings.ts b/src/actions/handlers/viewUserDeviceSettings.ts index e1dc7b3f26..4525ba104d 100644 --- a/src/actions/handlers/viewUserDeviceSettings.ts +++ b/src/actions/handlers/viewUserDeviceSettings.ts @@ -22,7 +22,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; * Redirect to the correct device manager section * Based on the labs setting */ -export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean) => { +export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean): void => { defaultDispatcher.dispatch({ action: Action.ViewUserSettings, initialTabId: isNewDeviceManagerEnabled ? UserTab.SessionManager : UserTab.Security, diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 98bbb55069..63a132077f 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -56,7 +56,7 @@ export default class ManageEventIndexDialog extends React.Component { + public updateCurrentRoom = async (room): Promise => { const eventIndex = EventIndexPeg.get(); let stats; @@ -131,17 +131,17 @@ export default class ManageEventIndexDialog extends React.Component { + private onDisable = async (): Promise => { const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default; Modal.createDialog(DisableEventIndexDialog, null, null, /* priority = */ false, /* static = */ true); }; - private onCrawlerSleepTimeChange = (e) => { + private onCrawlerSleepTimeChange = (e): void => { this.setState({ crawlerSleepTime: e.target.value }); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); }; - public render() { + public render(): JSX.Element { const brand = SdkConfig.get().brand; let crawlerState; diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index f9df9b09a4..a75b41f602 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -125,7 +125,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { + await accessSecretStorage(async (): Promise => { info = await MatrixClientPeg.get().prepareKeyBackupVersion(null /* random key */, { secureSecretStorage: true, }); diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 0c976ec599..b595a60a2e 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -350,7 +350,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo, - getKeyBackupPassphrase: async () => { + getKeyBackupPassphrase: async (): Promise => { // We may already have the backup key if we earlier went // through the restore backup path, so pass it along // rather than prompting again. @@ -383,7 +383,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { // It's possible we'll need the backup key later on for bootstrapping, // so let's stash it here, rather than prompting for it twice. - const keyCallback = (k) => (this.backupKey = k); + const keyCallback = (k: Uint8Array): void => { + this.backupKey = k; + }; const { finished } = Modal.createDialog( RestoreKeyBackupDialog, @@ -420,7 +422,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { + private onPassPhraseNextClick = async (e: React.FormEvent): Promise => { e.preventDefault(); if (!this.passphraseField.current) return; // unmounting @@ -434,7 +436,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { + private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise => { e.preventDefault(); if (this.state.passPhrase !== this.state.passPhraseConfirm) return; diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx index 1998c7c7ed..c8a561e7da 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx @@ -121,7 +121,7 @@ export default class ExportE2eKeysDialog extends React.Component return false; }; - private onPassphraseChange = (ev: React.ChangeEvent, phrase: AnyPassphrase) => { + private onPassphraseChange = (ev: React.ChangeEvent, phrase: AnyPassphrase): void => { this.setState({ [phrase]: ev.target.value, } as Pick); diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index b546ffc91a..079271b021 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -91,7 +91,7 @@ export default class ImportE2eKeysDialog extends React.Component return false; }; - private startImport(file: File, passphrase: string) { + private startImport(file: File, passphrase: string): Promise { this.setState({ errStr: null, phase: Phase.Importing, diff --git a/src/audio/ManagedPlayback.ts b/src/audio/ManagedPlayback.ts index 5db07671f1..c33d032b68 100644 --- a/src/audio/ManagedPlayback.ts +++ b/src/audio/ManagedPlayback.ts @@ -30,7 +30,7 @@ export class ManagedPlayback extends Playback { return super.play(); } - public destroy() { + public destroy(): void { this.manager.destroyPlaybackInstance(this); super.destroy(); } diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts index d5971ad73c..e1ab1a1c59 100644 --- a/src/audio/Playback.ts +++ b/src/audio/Playback.ts @@ -145,7 +145,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte return true; // we don't ever care if the event had listeners, so just return "yes" } - public destroy() { + public destroy(): void { // Dev note: It's critical that we call stop() during cleanup to ensure that downstream callers // are aware of the final clock position before the user triggered an unload. // noinspection JSIgnoredPromiseFromCall - not concerned about being called async here @@ -159,7 +159,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte } } - public async prepare() { + public async prepare(): Promise { // don't attempt to decode the media again // AudioContext.decodeAudioData detaches the array buffer `this.buf` // meaning it cannot be re-read @@ -190,7 +190,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte this.context.decodeAudioData( this.buf, (b) => resolve(b), - async (e) => { + async (e): Promise => { try { // This error handler is largely for Safari as well, which doesn't support Opus/Ogg // very well. @@ -232,12 +232,12 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore } - private onPlaybackEnd = async () => { + private onPlaybackEnd = async (): Promise => { await this.context.suspend(); this.emit(PlaybackState.Stopped); }; - public async play() { + public async play(): Promise { // We can't restart a buffer source, so we need to create a new one if we hit the end if (this.state === PlaybackState.Stopped) { this.disconnectSource(); @@ -256,13 +256,13 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte this.emit(PlaybackState.Playing); } - private disconnectSource() { + private disconnectSource(): void { if (this.element) return; // leave connected, we can (and must) re-use it this.source?.disconnect(); this.source?.removeEventListener("ended", this.onPlaybackEnd); } - private makeNewSourceBuffer() { + private makeNewSourceBuffer(): void { if (this.element && this.source) return; // leave connected, we can (and must) re-use it if (this.element) { @@ -276,22 +276,22 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte this.source.connect(this.context.destination); } - public async pause() { + public async pause(): Promise { await this.context.suspend(); this.emit(PlaybackState.Paused); } - public async stop() { + public async stop(): Promise { await this.onPlaybackEnd(); this.clock.flagStop(); } - public async toggle() { + public async toggle(): Promise { if (this.isPlaying) await this.pause(); else await this.play(); } - public async skipTo(timeSeconds: number) { + public async skipTo(timeSeconds: number): Promise { // Dev note: this function talks a lot about clock desyncs. There is a clock running // independently to the audio context and buffer so that accurate human-perceptible // time can be exposed. The PlaybackClock class has more information, but the short diff --git a/src/audio/PlaybackClock.ts b/src/audio/PlaybackClock.ts index 4f2d8f14aa..1a6d1a2e5d 100644 --- a/src/audio/PlaybackClock.ts +++ b/src/audio/PlaybackClock.ts @@ -89,7 +89,7 @@ export class PlaybackClock implements IDestroyable { return this.observable; } - private checkTime = (force = false) => { + private checkTime = (force = false): void => { const now = this.timeSeconds; // calculated dynamically if (this.lastCheck !== now || force) { this.observable.update([now, this.durationSeconds]); @@ -102,7 +102,7 @@ export class PlaybackClock implements IDestroyable { * The placeholders will be overridden once known. * @param {MatrixEvent} event The event to use for placeholders. */ - public populatePlaceholdersFrom(event: MatrixEvent) { + public populatePlaceholdersFrom(event: MatrixEvent): void { const durationMs = Number(event.getContent()["info"]?.["duration"]); if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000; } @@ -112,11 +112,11 @@ export class PlaybackClock implements IDestroyable { * This is to ensure the clock isn't skewed into thinking it is ~0.5s into * a clip when the duration is set. */ - public flagLoadTime() { + public flagLoadTime(): void { this.clipStart = this.context.currentTime; } - public flagStart() { + public flagStart(): void { if (this.stopped) { this.clipStart = this.context.currentTime; this.stopped = false; @@ -128,7 +128,7 @@ export class PlaybackClock implements IDestroyable { } } - public flagStop() { + public flagStop(): void { this.stopped = true; // Reset the clock time now so that the update going out will trigger components @@ -136,13 +136,13 @@ export class PlaybackClock implements IDestroyable { this.clipStart = this.context.currentTime; } - public syncTo(contextTime: number, clipTime: number) { + public syncTo(contextTime: number, clipTime: number): void { this.clipStart = contextTime - clipTime; this.stopped = false; // count as a mid-stream pause (if we were stopped) this.checkTime(true); } - public destroy() { + public destroy(): void { this.observable.close(); if (this.timerId) clearInterval(this.timerId); } diff --git a/src/audio/PlaybackManager.ts b/src/audio/PlaybackManager.ts index 01a597d58f..0cc52e7f0e 100644 --- a/src/audio/PlaybackManager.ts +++ b/src/audio/PlaybackManager.ts @@ -38,13 +38,13 @@ export class PlaybackManager { * instances are paused. * @param playback Optional. The playback to leave untouched. */ - public pauseAllExcept(playback?: Playback) { + public pauseAllExcept(playback?: Playback): void { this.instances .filter((p) => p !== playback && p.currentState === PlaybackState.Playing) .forEach((p) => p.pause()); } - public destroyPlaybackInstance(playback: ManagedPlayback) { + public destroyPlaybackInstance(playback: ManagedPlayback): void { this.instances = this.instances.filter((p) => p !== playback); } diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts index 1ea8a85fa6..7c521b9ca6 100644 --- a/src/audio/PlaybackQueue.ts +++ b/src/audio/PlaybackQueue.ts @@ -75,28 +75,28 @@ export class PlaybackQueue { return queue; } - private persistClocks() { + private persistClocks(): void { localStorage.setItem( `mx_voice_message_clocks_${this.room.roomId}`, JSON.stringify(Array.from(this.clockStates.entries())), ); } - private loadClocks() { + private loadClocks(): void { const val = localStorage.getItem(`mx_voice_message_clocks_${this.room.roomId}`); if (!!val) { this.clockStates = new Map(JSON.parse(val)); } } - public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback) { + public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback): void { // We don't ever detach our listeners: we expect the Playback to clean up for us this.playbacks.set(mxEvent.getId(), playback); playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, mxEvent, state)); playback.clockInfo.liveData.onUpdate((clock) => this.onPlaybackClock(playback, mxEvent, clock)); } - private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState) { + private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState): void { // Remember where the user got to in playback const wasLastPlaying = this.currentPlaybackId === mxEvent.getId(); if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()) && !wasLastPlaying) { @@ -210,7 +210,7 @@ export class PlaybackQueue { } } - private onPlaybackClock(playback: Playback, mxEvent: MatrixEvent, clocks: number[]) { + private onPlaybackClock(playback: Playback, mxEvent: MatrixEvent, clocks: number[]): void { if (playback.currentState === PlaybackState.Decoding) return; // ignore pre-ready values if (playback.currentState !== PlaybackState.Stopped) { diff --git a/src/audio/RecorderWorklet.ts b/src/audio/RecorderWorklet.ts index 5079ec58a9..0c0cc56cd6 100644 --- a/src/audio/RecorderWorklet.ts +++ b/src/audio/RecorderWorklet.ts @@ -43,7 +43,7 @@ class MxVoiceWorklet extends AudioWorkletProcessor { private nextAmplitudeSecond = 0; private amplitudeIndex = 0; - public process(inputs, outputs, parameters) { + public process(inputs, outputs, parameters): boolean { const currentSecond = roundTimeToTargetFreq(currentTime); // We special case the first ping because there's a fairly good chance that we'll miss the zeroth // update. Firefox for instance takes 0.06 seconds (roughly) to call this function for the first diff --git a/src/audio/VoiceMessageRecording.ts b/src/audio/VoiceMessageRecording.ts index f27fc36135..7d5c491261 100644 --- a/src/audio/VoiceMessageRecording.ts +++ b/src/audio/VoiceMessageRecording.ts @@ -141,7 +141,7 @@ export class VoiceMessageRecording implements IDestroyable { this.voiceRecording.destroy(); } - private onDataAvailable = (data: ArrayBuffer) => { + private onDataAvailable = (data: ArrayBuffer): void => { const buf = new Uint8Array(data); this.buffer = concat(this.buffer, buf); }; @@ -153,6 +153,6 @@ export class VoiceMessageRecording implements IDestroyable { } } -export const createVoiceMessageRecording = (matrixClient: MatrixClient) => { +export const createVoiceMessageRecording = (matrixClient: MatrixClient): VoiceMessageRecording => { return new VoiceMessageRecording(matrixClient, new VoiceRecording()); }; diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts index 20434d998d..32fcb5a97a 100644 --- a/src/audio/VoiceRecording.ts +++ b/src/audio/VoiceRecording.ts @@ -110,7 +110,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { return !MediaDeviceHandler.getAudioNoiseSuppression(); } - private async makeRecorder() { + private async makeRecorder(): Promise { try { this.recorderStream = await navigator.mediaDevices.getUserMedia({ audio: { @@ -212,14 +212,14 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { return !!Recorder.isRecordingSupported(); } - private onAudioProcess = (ev: AudioProcessingEvent) => { + private onAudioProcess = (ev: AudioProcessingEvent): void => { this.processAudioUpdate(ev.playbackTime); // We skip the functionality of the worklet regarding waveform calculations: we // should get that information pretty quick during the playback info. }; - private processAudioUpdate = (timeSeconds: number) => { + private processAudioUpdate = (timeSeconds: number): void => { if (!this.recording) return; this.observable.update({ @@ -260,7 +260,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { /** * {@link https://github.com/chris-rudmin/opus-recorder#instance-fields ref for recorderSeconds} */ - public get recorderSeconds() { + public get recorderSeconds(): number { return this.recorder.encodedSamplePosition / 48000; } @@ -279,7 +279,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { } public async stop(): Promise { - return Singleflight.for(this, "stop").do(async () => { + return Singleflight.for(this, "stop").do(async (): Promise => { if (!this.recording) { throw new Error("No recording to stop"); } @@ -307,7 +307,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { }); } - public destroy() { + public destroy(): void { // noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here this.stop(); this.removeAllListeners(); diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index e76c4f1903..546e052f58 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -22,7 +22,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext"; import type { ICompletion, ISelectionRange } from "./Autocompleter"; export interface ICommand { - command: string | null; + command: RegExpExecArray | null; range: { start: number; end: number; @@ -59,7 +59,7 @@ export default abstract class AutocompleteProvider { } } - public destroy() { + public destroy(): void { // stub } @@ -70,7 +70,7 @@ export default abstract class AutocompleteProvider { * @param {boolean} force True if the user is forcing completion * @return {object} { command, range } where both objects fields are null if no match */ - public getCurrentCommand(query: string, selection: ISelectionRange, force = false) { + public getCurrentCommand(query: string, selection: ISelectionRange, force = false): ICommand { let commandRegex = this.commandRegex; if (force && this.shouldForceComplete()) { @@ -83,7 +83,7 @@ export default abstract class AutocompleteProvider { commandRegex.lastIndex = 0; - let match; + let match: RegExpExecArray; while ((match = commandRegex.exec(query)) !== null) { const start = match.index; const end = start + match[0].length; diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 67a40db158..b609f265f1 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -69,7 +69,7 @@ export default class Autocompleter { }); } - public destroy() { + public destroy(): void { this.providers.forEach((p) => { p.destroy(); }); @@ -88,7 +88,7 @@ export default class Autocompleter { */ // list of results from each provider, each being a list of completions or null if it times out const completionsList: ICompletion[][] = await Promise.all( - this.providers.map(async (provider) => { + this.providers.map(async (provider): Promise => { return timeout( provider.getCompletions(query, selection, force, limit), null, diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index 68850a9a15..caafe98f08 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -100,7 +100,7 @@ export default class CommandProvider extends AutocompleteProvider { }); } - public getName() { + public getName(): string { return "*️⃣ " + _t("Commands"); } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 821edb4a3e..cc25068db8 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -55,7 +55,7 @@ const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => { _orderBy: index, })); -function score(query, space) { +function score(query: string, space: string): number { const index = space.indexOf(query); if (index === -1) { return Infinity; @@ -154,7 +154,7 @@ export default class EmojiProvider extends AutocompleteProvider { return []; } - public getName() { + public getName(): string { return "😃 " + _t("Emoji"); } diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 28f01c178a..5efe0e86f6 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -65,7 +65,7 @@ export default class NotifProvider extends AutocompleteProvider { return []; } - public getName() { + public getName(): string { return "❗️ " + _t("Room Notification"); } diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 23545075bc..1f7b5a5a7f 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -61,7 +61,7 @@ export default class QueryMatcher { } } - public setObjects(objects: T[]) { + public setObjects(objects: T[]): void { this._items = new Map(); for (const object of objects) { diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index a225676898..bf102d55bc 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -37,7 +37,15 @@ function canonicalScore(displayedAlias: string, room: Room): number { return displayedAlias === room.getCanonicalAlias() ? 0 : 1; } -function matcherObject(room: Room, displayedAlias: string, matchName = "") { +function matcherObject( + room: Room, + displayedAlias: string, + matchName = "", +): { + room: Room; + matchName: string; + displayedAlias: string; +} { return { room, matchName, @@ -46,7 +54,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") { } export default class RoomProvider extends AutocompleteProvider { - protected matcher: QueryMatcher; + protected matcher: QueryMatcher>; public constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: ROOM_REGEX, renderingType }); @@ -55,7 +63,7 @@ export default class RoomProvider extends AutocompleteProvider { }); } - protected getRooms() { + protected getRooms(): Room[] { const cli = MatrixClientPeg.get(); // filter out spaces here as they get their own autocomplete provider @@ -68,7 +76,6 @@ export default class RoomProvider extends AutocompleteProvider { force = false, limit = -1, ): Promise { - let completions = []; const { command, range } = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties @@ -96,15 +103,15 @@ export default class RoomProvider extends AutocompleteProvider { this.matcher.setObjects(matcherObjects); const matchedString = command[0]; - completions = this.matcher.match(matchedString, limit); + let completions = this.matcher.match(matchedString, limit); completions = sortBy(completions, [ (c) => canonicalScore(c.displayedAlias, c.room), (c) => c.displayedAlias.length, ]); completions = uniqBy(completions, (match) => match.room); - completions = completions - .map((room) => { - return { + return completions + .map( + (room): ICompletion => ({ completion: room.displayedAlias, completionId: room.room.roomId, type: "room", @@ -116,14 +123,14 @@ export default class RoomProvider extends AutocompleteProvider { ), range, - }; - }) + }), + ) .filter((completion) => !!completion.completion && completion.completion.length > 0); } - return completions; + return []; } - public getName() { + public getName(): string { return _t("Rooms"); } diff --git a/src/autocomplete/SpaceProvider.tsx b/src/autocomplete/SpaceProvider.tsx index 14f9e2c375..bef3b57354 100644 --- a/src/autocomplete/SpaceProvider.tsx +++ b/src/autocomplete/SpaceProvider.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Room } from "matrix-js-sdk/src/models/room"; import React from "react"; import { _t } from "../languageHandler"; @@ -21,13 +22,13 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; import RoomProvider from "./RoomProvider"; export default class SpaceProvider extends RoomProvider { - protected getRooms() { + protected getRooms(): Room[] { return MatrixClientPeg.get() .getVisibleRooms() .filter((r) => r.isSpaceRoom()); } - public getName() { + public getName(): string { return _t("Spaces"); } diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 551a7bc141..65de4b1bb4 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -64,7 +64,7 @@ export default class UserProvider extends AutocompleteProvider { MatrixClientPeg.get().on(RoomStateEvent.Update, this.onRoomStateUpdate); } - public destroy() { + public destroy(): void { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.onRoomTimeline); MatrixClientPeg.get().removeListener(RoomStateEvent.Update, this.onRoomStateUpdate); @@ -77,7 +77,7 @@ export default class UserProvider extends AutocompleteProvider { toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData, - ) => { + ): void => { if (!room) return; // notification timeline, we'll get this event again with a room specific timeline if (removed) return; if (room.roomId !== this.room.roomId) return; @@ -93,7 +93,7 @@ export default class UserProvider extends AutocompleteProvider { this.onUserSpoke(ev.sender); }; - private onRoomStateUpdate = (state: RoomState) => { + private onRoomStateUpdate = (state: RoomState): void => { // ignore updates in other rooms if (state.roomId !== this.room.roomId) return; @@ -150,7 +150,7 @@ export default class UserProvider extends AutocompleteProvider { return _t("Users"); } - private makeUsers() { + private makeUsers(): void { const events = this.room.getLiveTimeline().getEvents(); const lastSpoken = {}; @@ -167,7 +167,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher.setObjects(this.users); } - public onUserSpoke(user: RoomMember) { + public onUserSpoke(user: RoomMember): void { if (!this.users) return; if (!user) return; if (user.userId === MatrixClientPeg.get().credentials.userId) return; diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 719be59f6c..90fda3fe21 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -22,7 +22,7 @@ type DynamicHtmlElementProps = JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; type DynamicElementProps = Partial>; -export type IProps = DynamicHtmlElementProps & { +export type IProps = Omit, "onScroll"> & { element?: T; className?: string; onScroll?: (event: Event) => void; @@ -39,7 +39,7 @@ export default class AutoHideScrollbar ex public readonly containerRef: React.RefObject = React.createRef(); - public componentDidMount() { + public componentDidMount(): void { if (this.containerRef.current && this.props.onScroll) { // Using the passive option to not block the main thread // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners @@ -49,13 +49,13 @@ export default class AutoHideScrollbar ex this.props.wrappedRef?.(this.containerRef.current); } - public componentWillUnmount() { + public componentWillUnmount(): void { if (this.containerRef.current && this.props.onScroll) { this.containerRef.current.removeEventListener("scroll", this.props.onScroll); } } - public render() { + public render(): JSX.Element { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props; diff --git a/src/components/structures/AutocompleteInput.tsx b/src/components/structures/AutocompleteInput.tsx index 2d09a247c6..9f16211022 100644 --- a/src/components/structures/AutocompleteInput.tsx +++ b/src/components/structures/AutocompleteInput.tsx @@ -52,11 +52,11 @@ export const AutocompleteInput: React.FC = ({ const editorContainerRef = useRef(null); const editorRef = useRef(null); - const focusEditor = () => { + const focusEditor = (): void => { editorRef?.current?.focus(); }; - const onQueryChange = async (e: ChangeEvent) => { + const onQueryChange = async (e: ChangeEvent): Promise => { const value = e.target.value.trim(); setQuery(value); @@ -74,11 +74,11 @@ export const AutocompleteInput: React.FC = ({ setSuggestions(matches); }; - const onClickInputArea = () => { + const onClickInputArea = (): void => { focusEditor(); }; - const onKeyDown = (e: KeyboardEvent) => { + const onKeyDown = (e: KeyboardEvent): void => { const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey; // when the field is empty and the user hits backspace remove the right-most target @@ -87,7 +87,7 @@ export const AutocompleteInput: React.FC = ({ } }; - const toggleSelection = (completion: ICompletion) => { + const toggleSelection = (completion: ICompletion): void => { const newSelection = [...selection]; const index = selection.findIndex((selection) => selection.completionId === completion.completionId); @@ -101,7 +101,7 @@ export const AutocompleteInput: React.FC = ({ focusEditor(); }; - const removeSelection = (completion: ICompletion) => { + const removeSelection = (completion: ICompletion): void => { const newSelection = [...selection]; const index = selection.findIndex((selection) => selection.completionId === completion.completionId); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 6dbdc4a7eb..978dd07be9 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -64,7 +64,7 @@ export enum ChevronFace { None = "none", } -export interface IProps extends IPosition { +export interface MenuProps extends IPosition { menuWidth?: number; menuHeight?: number; @@ -77,7 +77,9 @@ export interface IProps extends IPosition { menuPaddingRight?: number; zIndex?: number; +} +export interface IProps extends MenuProps { // If true, insert an invisible screen-sized element behind the menu that when clicked will close it. hasBackground?: boolean; // whether this context menu should be focus managed. If false it must handle itself @@ -128,21 +130,21 @@ export default class ContextMenu extends React.PureComponent { this.initialFocus = document.activeElement as HTMLElement; } - public componentDidMount() { + public componentDidMount(): void { Modal.on(ModalManagerEvent.Opened, this.onModalOpen); } - public componentWillUnmount() { + public componentWillUnmount(): void { Modal.off(ModalManagerEvent.Opened, this.onModalOpen); // return focus to the thing which had it before us this.initialFocus.focus(); } - private onModalOpen = () => { + private onModalOpen = (): void => { this.props.onFinished?.(); }; - private collectContextMenuRect = (element: HTMLDivElement) => { + private collectContextMenuRect = (element: HTMLDivElement): void => { // We don't need to clean up when unmounting, so ignore if (!element) return; @@ -159,7 +161,7 @@ export default class ContextMenu extends React.PureComponent { }); }; - private onContextMenu = (e) => { + private onContextMenu = (e: React.MouseEvent): void => { if (this.props.onFinished) { this.props.onFinished(); @@ -184,20 +186,20 @@ export default class ContextMenu extends React.PureComponent { } }; - private onContextMenuPreventBubbling = (e) => { + private onContextMenuPreventBubbling = (e: React.MouseEvent): void => { // stop propagation so that any context menu handlers don't leak out of this context menu // but do not inhibit the default browser menu e.stopPropagation(); }; // Prevent clicks on the background from going through to the component which opened the menu. - private onFinished = (ev: React.MouseEvent) => { + private onFinished = (ev: React.MouseEvent): void => { ev.stopPropagation(); ev.preventDefault(); this.props.onFinished?.(); }; - private onClick = (ev: React.MouseEvent) => { + private onClick = (ev: React.MouseEvent): void => { // Don't allow clicks to escape the context menu wrapper ev.stopPropagation(); @@ -208,7 +210,7 @@ export default class ContextMenu extends React.PureComponent { // We now only handle closing the ContextMenu in this keyDown handler. // All of the item/option navigation is delegated to RovingTabIndex. - private onKeyDown = (ev: React.KeyboardEvent) => { + private onKeyDown = (ev: React.KeyboardEvent): void => { ev.stopPropagation(); // prevent keyboard propagating out of the context menu, we're focus-locked const action = getKeyBindingsManager().getAccessibilityAction(ev); @@ -243,7 +245,7 @@ export default class ContextMenu extends React.PureComponent { } }; - protected renderMenu(hasBackground = this.props.hasBackground) { + protected renderMenu(hasBackground = this.props.hasBackground): JSX.Element { const position: Partial> = {}; const { top, @@ -501,17 +503,13 @@ export const toLeftOrRightOf = (elementRect: DOMRect, chevronOffset = 12): ToRig return toRightOf(elementRect, chevronOffset); }; -export type AboveLeftOf = IPosition & { - chevronFace: ChevronFace; -}; - // Placement method for to position context menu right-aligned and flowing to the left of elementRect, // and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?) export const aboveLeftOf = ( elementRect: Pick, chevronFace = ChevronFace.None, vPadding = 0, -): AboveLeftOf => { +): MenuProps => { const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const buttonRight = elementRect.right + window.scrollX; @@ -535,7 +533,7 @@ export const aboveRightOf = ( elementRect: Pick, chevronFace = ChevronFace.None, vPadding = 0, -): AboveLeftOf => { +): MenuProps => { const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const buttonLeft = elementRect.left + window.scrollX; @@ -555,11 +553,11 @@ export const aboveRightOf = ( // Placement method for to position context menu right-aligned and flowing to the left of elementRect // and always above elementRect -export const alwaysAboveLeftOf = ( +export const alwaysMenuProps = ( elementRect: Pick, chevronFace = ChevronFace.None, vPadding = 0, -) => { +): IPosition & { chevronFace: ChevronFace } => { const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const buttonRight = elementRect.right + window.scrollX; @@ -578,7 +576,7 @@ export const alwaysAboveRightOf = ( elementRect: Pick, chevronFace = ChevronFace.None, vPadding = 0, -) => { +): IPosition & { chevronFace: ChevronFace } => { const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const buttonLeft = elementRect.left + window.scrollX; @@ -607,12 +605,12 @@ export const useContextMenu = (inputRef?: RefObject } const [isOpen, setIsOpen] = useState(false); - const open = (ev?: SyntheticEvent) => { + const open = (ev?: SyntheticEvent): void => { ev?.preventDefault(); ev?.stopPropagation(); setIsOpen(true); }; - const close = (ev?: SyntheticEvent) => { + const close = (ev?: SyntheticEvent): void => { ev?.preventDefault(); ev?.stopPropagation(); setIsOpen(false); @@ -622,8 +620,11 @@ export const useContextMenu = (inputRef?: RefObject }; // XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs. -export function createMenu(ElementClass, props) { - const onFinished = function (...args) { +export function createMenu( + ElementClass: typeof React.Component, + props: Record, +): { close: (...args: any[]) => void } { + const onFinished = function (...args): void { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); props?.onFinished?.apply(null, args); }; diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index d531e4fcc4..e3cacf0114 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -60,7 +60,7 @@ export default class EmbeddedPage extends React.PureComponent { return sanitizeHtml(_t(s)); } - private async fetchEmbed() { + private async fetchEmbed(): Promise { let res: Response; try { diff --git a/src/components/structures/FileDropTarget.tsx b/src/components/structures/FileDropTarget.tsx index ce24bb3783..e8a8fa5e28 100644 --- a/src/components/structures/FileDropTarget.tsx +++ b/src/components/structures/FileDropTarget.tsx @@ -37,7 +37,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { useEffect(() => { if (!parent || parent.ondrop) return; - const onDragEnter = (ev: DragEvent) => { + const onDragEnter = (ev: DragEvent): void => { ev.stopPropagation(); ev.preventDefault(); @@ -55,7 +55,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { })); }; - const onDragLeave = (ev: DragEvent) => { + const onDragLeave = (ev: DragEvent): void => { ev.stopPropagation(); ev.preventDefault(); @@ -65,7 +65,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { })); }; - const onDragOver = (ev: DragEvent) => { + const onDragOver = (ev: DragEvent): void => { ev.stopPropagation(); ev.preventDefault(); @@ -79,7 +79,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { } }; - const onDrop = (ev: DragEvent) => { + const onDrop = (ev: DragEvent): void => { ev.stopPropagation(); ev.preventDefault(); onFileDrop(ev.dataTransfer); diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 6efa4f857a..4390dcf36e 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -223,7 +223,7 @@ class FilePanel extends React.Component { } } - public render() { + public render(): JSX.Element { if (MatrixClientPeg.get().isGuest()) { return ( diff --git a/src/components/structures/GenericErrorPage.tsx b/src/components/structures/GenericErrorPage.tsx index 4179abe7fd..4261d9b2f4 100644 --- a/src/components/structures/GenericErrorPage.tsx +++ b/src/components/structures/GenericErrorPage.tsx @@ -22,7 +22,7 @@ interface IProps { } export default class GenericErrorPage extends React.PureComponent { - public render() { + public render(): JSX.Element { return (
diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 54aa635fe7..13fc132516 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -33,17 +33,17 @@ import MiniAvatarUploader, { AVATAR_SIZE } from "../views/elements/MiniAvatarUpl import PosthogTrackers from "../../PosthogTrackers"; import EmbeddedPage from "./EmbeddedPage"; -const onClickSendDm = (ev: ButtonEvent) => { +const onClickSendDm = (ev: ButtonEvent): void => { PosthogTrackers.trackInteraction("WebHomeCreateChatButton", ev); dis.dispatch({ action: "view_create_chat" }); }; -const onClickExplore = (ev: ButtonEvent) => { +const onClickExplore = (ev: ButtonEvent): void => { PosthogTrackers.trackInteraction("WebHomeExploreRoomsButton", ev); dis.fire(Action.ViewRoomDirectory); }; -const onClickNewRoom = (ev: ButtonEvent) => { +const onClickNewRoom = (ev: ButtonEvent): void => { PosthogTrackers.trackInteraction("WebHomeCreateRoomButton", ev); dis.dispatch({ action: "view_create_room" }); }; @@ -52,12 +52,17 @@ interface IProps { justRegistered?: boolean; } -const getOwnProfile = (userId: string) => ({ +const getOwnProfile = ( + userId: string, +): { + displayName: string; + avatarUrl: string; +} => ({ displayName: OwnProfileStore.instance.displayName || userId, avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE), }); -const UserWelcomeTop = () => { +const UserWelcomeTop: React.FC = () => { const cli = useContext(MatrixClientContext); const userId = cli.getUserId(); const [ownProfile, setOwnProfile] = useState(getOwnProfile(userId)); diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx index 7d4652e3ee..757a7360fb 100644 --- a/src/components/structures/HostSignupAction.tsx +++ b/src/components/structures/HostSignupAction.tsx @@ -28,7 +28,7 @@ interface IProps { interface IState {} export default class HostSignupAction extends React.PureComponent { - private openDialog = async () => { + private openDialog = async (): Promise => { this.props.onClick?.(); await HostSignupStore.instance.setHostSignupActive(true); }; diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index b5582323bf..99be8705a4 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -130,7 +130,7 @@ export default class InteractiveAuthComponent extends React.Component { @@ -155,7 +155,7 @@ export default class InteractiveAuthComponent extends React.Component { return SettingsStore.getValue("feature_breadcrumbs_v2") ? BreadcrumbsMode.Labs : BreadcrumbsMode.Legacy; } - public componentDidMount() { + public componentDidMount(): void { UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); UIStore.instance.on("ListContainer", this.refreshStickyHeaders); // Using the passive option to not block the main thread @@ -97,7 +97,7 @@ export default class LeftPanel extends React.Component { this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true }); } - public componentWillUnmount() { + public componentWillUnmount(): void { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); @@ -112,25 +112,25 @@ export default class LeftPanel extends React.Component { } } - private updateActiveSpace = (activeSpace: SpaceKey) => { + private updateActiveSpace = (activeSpace: SpaceKey): void => { this.setState({ activeSpace }); }; - private onDialPad = () => { + private onDialPad = (): void => { dis.fire(Action.OpenDialPad); }; - private onExplore = (ev: ButtonEvent) => { + private onExplore = (ev: ButtonEvent): void => { dis.fire(Action.ViewRoomDirectory); PosthogTrackers.trackInteraction("WebLeftPanelExploreRoomsButton", ev); }; - private refreshStickyHeaders = () => { + private refreshStickyHeaders = (): void => { if (!this.listContainerRef.current) return; // ignore: no headers to sticky this.handleStickyHeaders(this.listContainerRef.current); }; - private onBreadcrumbsUpdate = () => { + private onBreadcrumbsUpdate = (): void => { const newVal = LeftPanel.breadcrumbsMode; if (newVal !== this.state.showBreadcrumbs) { this.setState({ showBreadcrumbs: newVal }); @@ -141,7 +141,7 @@ export default class LeftPanel extends React.Component { } }; - private handleStickyHeaders(list: HTMLDivElement) { + private handleStickyHeaders(list: HTMLDivElement): void { if (this.isDoingStickyHeaders) return; this.isDoingStickyHeaders = true; window.requestAnimationFrame(() => { @@ -150,7 +150,7 @@ export default class LeftPanel extends React.Component { }); } - private doStickyHeaders(list: HTMLDivElement) { + private doStickyHeaders(list: HTMLDivElement): void { const topEdge = list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist:not(.mx_RoomSublist_hidden)"); @@ -282,20 +282,20 @@ export default class LeftPanel extends React.Component { } } - private onScroll = (ev: Event) => { + private onScroll = (ev: Event): void => { const list = ev.target as HTMLDivElement; this.handleStickyHeaders(list); }; - private onFocus = (ev: React.FocusEvent) => { + private onFocus = (ev: React.FocusEvent): void => { this.focusedElement = ev.target; }; - private onBlur = () => { + private onBlur = (): void => { this.focusedElement = null; }; - private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState) => { + private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState): void => { if (!this.focusedElement) return; const action = getKeyBindingsManager().getRoomListAction(ev); diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 9a4d82a9f8..5365352921 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -142,7 +142,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { return [...this.events][0]?.getRoomId(); } - private onSilencedCallsChanged = () => { + private onSilencedCallsChanged = (): void => { const newState = LegacyCallHandler.instance.isCallSilenced(this.callId); this.emit(LegacyCallEventGrouperEvent.SilencedChanged, newState); }; @@ -163,20 +163,20 @@ export default class LegacyCallEventGrouper extends EventEmitter { LegacyCallHandler.instance.placeCall(this.roomId, this.isVoice ? CallType.Voice : CallType.Video); }; - public toggleSilenced = () => { + public toggleSilenced = (): void => { const silenced = LegacyCallHandler.instance.isCallSilenced(this.callId); silenced ? LegacyCallHandler.instance.unSilenceCall(this.callId) : LegacyCallHandler.instance.silenceCall(this.callId); }; - private setCallListeners() { + private setCallListeners(): void { if (!this.call) return; this.call.addListener(CallEvent.State, this.setState); this.call.addListener(CallEvent.LengthChanged, this.onLengthChanged); } - private setState = () => { + private setState = (): void => { if (CONNECTING_STATES.includes(this.call?.state)) { this.state = CallState.Connecting; } else if (SUPPORTED_STATES.includes(this.call?.state)) { @@ -190,7 +190,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { this.emit(LegacyCallEventGrouperEvent.StateChanged, this.state); }; - private setCall = () => { + private setCall = (): void => { if (this.call) return; this.call = LegacyCallHandler.instance.getCallById(this.callId); @@ -198,7 +198,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { this.setState(); }; - public add(event: MatrixEvent) { + public add(event: MatrixEvent): void { if (this.events.has(event)) return; // nothing to do this.events.add(event); this.setCall(); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 6e18f8a6f7..242bbdc028 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -159,7 +159,7 @@ class LoggedInView extends React.Component { this.resizeHandler = React.createRef(); } - public componentDidMount() { + public componentDidMount(): void { document.addEventListener("keydown", this.onNativeKeyDown, false); LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.onCallState); @@ -191,7 +191,7 @@ class LoggedInView extends React.Component { this.refreshBackgroundImage(); } - public componentWillUnmount() { + public componentWillUnmount(): void { document.removeEventListener("keydown", this.onNativeKeyDown, false); LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState); this._matrixClient.removeListener(ClientEvent.AccountData, this.onAccountData); @@ -221,14 +221,14 @@ class LoggedInView extends React.Component { this.setState({ backgroundImage }); }; - public canResetTimelineInRoom = (roomId: string) => { + public canResetTimelineInRoom = (roomId: string): boolean => { if (!this._roomView.current) { return true; } return this._roomView.current.canResetTimeline(); }; - private createResizer() { + private createResizer(): Resizer { let panelSize; let panelCollapsed; const collapseConfig: ICollapseConfig = { @@ -268,7 +268,7 @@ class LoggedInView extends React.Component { return resizer; } - private loadResizerPreferences() { + private loadResizerPreferences(): void { let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10); if (isNaN(lhsSize)) { lhsSize = 350; @@ -276,13 +276,13 @@ class LoggedInView extends React.Component { this.resizer.forHandleWithId("lp-resizer").resize(lhsSize); } - private onAccountData = (event: MatrixEvent) => { + private onAccountData = (event: MatrixEvent): void => { if (event.getType() === "m.ignored_user_list") { dis.dispatch({ action: "ignore_state_changed" }); } }; - private onCompactLayoutChanged = () => { + private onCompactLayoutChanged = (): void => { this.setState({ useCompactLayout: SettingsStore.getValue("useCompactLayout"), }); @@ -311,13 +311,13 @@ class LoggedInView extends React.Component { } }; - private onUsageLimitDismissed = () => { + private onUsageLimitDismissed = (): void => { this.setState({ usageLimitDismissed: true, }); }; - private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { + private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit): void { const error = (syncError?.error as MatrixError)?.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { usageLimitEventContent = (syncError?.error as MatrixError).data as IUsageLimit; @@ -337,9 +337,9 @@ class LoggedInView extends React.Component { } } - private updateServerNoticeEvents = async () => { + private updateServerNoticeEvents = async (): Promise => { const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; - if (!serverNoticeList) return []; + if (!serverNoticeList) return; const events = []; let pinnedEventTs = 0; @@ -379,7 +379,7 @@ class LoggedInView extends React.Component { }); }; - private onPaste = (ev: ClipboardEvent) => { + private onPaste = (ev: ClipboardEvent): void => { const element = ev.target as HTMLElement; const inputableElement = getInputableElement(element); if (inputableElement === document.activeElement) return; // nothing to do @@ -422,13 +422,13 @@ class LoggedInView extends React.Component { We also listen with a native listener on the document to get keydown events when no element is focused. Bubbling is irrelevant here as the target is the body element. */ - private onReactKeyDown = (ev) => { + private onReactKeyDown = (ev): void => { // events caught while bubbling up on the root element // of this component, so something must be focused. this.onKeyDown(ev); }; - private onNativeKeyDown = (ev) => { + private onNativeKeyDown = (ev): void => { // only pass this if there is no focused element. // if there is, onKeyDown will be called by the // react keydown handler that respects the react bubbling order. @@ -437,7 +437,7 @@ class LoggedInView extends React.Component { } }; - private onKeyDown = (ev) => { + private onKeyDown = (ev): void => { let handled = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); @@ -615,13 +615,13 @@ class LoggedInView extends React.Component { * dispatch a page-up/page-down/etc to the appropriate component * @param {Object} ev The key event */ - private onScrollKeyPressed = (ev) => { + private onScrollKeyPressed = (ev): void => { if (this._roomView.current) { this._roomView.current.handleScrollKey(ev); } }; - public render() { + public render(): JSX.Element { let pageElement; switch (this.props.page_type) { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 536626f270..df2aa87f2c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -216,7 +216,7 @@ export default class MatrixChat extends React.PureComponent { realQueryParams: {}, startingFragmentQueryParams: {}, config: {}, - onTokenLoginCompleted: () => {}, + onTokenLoginCompleted: (): void => {}, }; private firstSyncComplete = false; @@ -317,7 +317,7 @@ export default class MatrixChat extends React.PureComponent { this.props.realQueryParams, this.props.defaultDeviceDisplayName, this.getFragmentAfterLogin(), - ).then(async (loggedIn) => { + ).then(async (loggedIn): Promise => { if (this.props.realQueryParams?.loginToken) { // remove the loginToken from the URL regardless this.props.onTokenLoginCompleted(); @@ -353,7 +353,7 @@ export default class MatrixChat extends React.PureComponent { initSentry(SdkConfig.get("sentry")); } - private async postLoginSetup() { + private async postLoginSetup(): Promise { const cli = MatrixClientPeg.get(); const cryptoEnabled = cli.isCryptoEnabled(); if (!cryptoEnabled) { @@ -367,7 +367,7 @@ export default class MatrixChat extends React.PureComponent { // as a proxy to figure out if it's worth prompting the user to verify // from another device. promisesList.push( - (async () => { + (async (): Promise => { crossSigningIsSetUp = await cli.userHasCrossSigningKeys(); })(), ); @@ -417,7 +417,7 @@ export default class MatrixChat extends React.PureComponent { window.addEventListener("resize", this.onWindowResized); } - public componentDidUpdate(prevProps, prevState) { + public componentDidUpdate(prevProps, prevState): void { if (this.shouldTrackPageChange(prevState, this.state)) { const durationMs = this.stopPageChangeTimer(); PosthogTrackers.instance.trackPageChange(this.state.view, this.state.page_type, durationMs); @@ -428,7 +428,7 @@ export default class MatrixChat extends React.PureComponent { } } - public componentWillUnmount() { + public componentWillUnmount(): void { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); this.themeWatcher.stop(); @@ -477,7 +477,7 @@ export default class MatrixChat extends React.PureComponent { } } - private getServerProperties() { + private getServerProperties(): { serverConfig: ValidatedServerConfig } { let props = this.state.serverConfig; if (!props) props = this.props.serverConfig; // for unit tests if (!props) props = SdkConfig.get("validated_server_config"); @@ -513,11 +513,11 @@ export default class MatrixChat extends React.PureComponent { // to try logging out. } - private startPageChangeTimer() { + private startPageChangeTimer(): void { PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE); } - private stopPageChangeTimer() { + private stopPageChangeTimer(): number | null { const perfMonitor = PerformanceMonitor.instance; perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); @@ -876,13 +876,13 @@ export default class MatrixChat extends React.PureComponent { } }; - private setPage(pageType: PageType) { + private setPage(pageType: PageType): void { this.setState({ page_type: pageType, }); } - private async startRegistration(params: { [key: string]: string }) { + private async startRegistration(params: { [key: string]: string }): Promise { const newState: Partial = { view: Views.REGISTER, }; @@ -916,7 +916,7 @@ export default class MatrixChat extends React.PureComponent { } // switch view to the given room - private async viewRoom(roomInfo: ViewRoomPayload) { + private async viewRoom(roomInfo: ViewRoomPayload): Promise { this.focusComposer = true; if (roomInfo.room_alias) { @@ -992,7 +992,7 @@ export default class MatrixChat extends React.PureComponent { ); } - private viewSomethingBehindModal() { + private viewSomethingBehindModal(): void { if (this.state.view !== Views.LOGGED_IN) { this.viewWelcome(); return; @@ -1002,7 +1002,7 @@ export default class MatrixChat extends React.PureComponent { } } - private viewWelcome() { + private viewWelcome(): void { if (shouldUseLoginForWelcome(SdkConfig.get())) { return this.viewLogin(); } @@ -1014,7 +1014,7 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } - private viewLogin(otherState?: any) { + private viewLogin(otherState?: any): void { this.setStateForNewView({ view: Views.LOGIN, ...otherState, @@ -1024,7 +1024,7 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } - private viewHome(justRegistered = false) { + private viewHome(justRegistered = false): void { // The home page requires the "logged in" view, so we'll set that. this.setStateForNewView({ view: Views.LOGGED_IN, @@ -1037,7 +1037,7 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } - private viewUser(userId: string, subAction: string) { + private viewUser(userId: string, subAction: string): void { // Wait for the first sync so that `getRoom` gives us a room object if it's // in the sync response const waitForSync = this.firstSyncPromise ? this.firstSyncPromise.promise : Promise.resolve(); @@ -1052,7 +1052,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType) { + private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType): Promise { const modal = Modal.createDialog(CreateRoomDialog, { type, defaultPublic, @@ -1065,7 +1065,7 @@ export default class MatrixChat extends React.PureComponent { } } - private chatCreateOrReuse(userId: string) { + private chatCreateOrReuse(userId: string): void { const snakedConfig = new SnakedObject(this.props.config); // Use a deferred action to reshow the dialog once the user has registered if (MatrixClientPeg.get().isGuest()) { @@ -1115,11 +1115,11 @@ export default class MatrixChat extends React.PureComponent { } } - private leaveRoomWarnings(roomId: string) { + private leaveRoomWarnings(roomId: string): JSX.Element[] { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const isSpace = roomToLeave?.isSpaceRoom(); // Show a warning if there are additional complications. - const warnings = []; + const warnings: JSX.Element[] = []; const memberCount = roomToLeave.currentState.getJoinedMemberCount(); if (memberCount === 1) { @@ -1153,7 +1153,7 @@ export default class MatrixChat extends React.PureComponent { return warnings; } - private leaveRoom(roomId: string) { + private leaveRoom(roomId: string): void { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); @@ -1184,7 +1184,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private forgetRoom(roomId: string) { + private forgetRoom(roomId: string): void { const room = MatrixClientPeg.get().getRoom(roomId); MatrixClientPeg.get() .forget(roomId) @@ -1208,7 +1208,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private async copyRoom(roomId: string) { + private async copyRoom(roomId: string): Promise { const roomLink = makeRoomPermalink(roomId); const success = await copyPlaintext(roomLink); if (!success) { @@ -1223,13 +1223,13 @@ export default class MatrixChat extends React.PureComponent { * Starts a chat with the welcome user, if the user doesn't already have one * @returns {string} The room ID of the new room, or null if no room was created */ - private async startWelcomeUserChat() { + private async startWelcomeUserChat(): Promise { // We can end up with multiple tabs post-registration where the user // might then end up with a session and we don't want them all making // a chat with the welcome user: try to de-dupe. // We need to wait for the first sync to complete for this to // work though. - let waitFor; + let waitFor: Promise; if (!this.firstSyncComplete) { waitFor = this.firstSyncPromise.promise; } else { @@ -1254,7 +1254,7 @@ export default class MatrixChat extends React.PureComponent { // run without the update to m.direct, making another welcome // user room (it doesn't wait for new data from the server, just // the saved sync to be loaded). - const saveWelcomeUser = (ev: MatrixEvent) => { + const saveWelcomeUser = (ev: MatrixEvent): void => { if (ev.getType() === EventType.Direct && ev.getContent()[snakedConfig.get("welcome_user_id")]) { MatrixClientPeg.get().store.save(true); MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser); @@ -1270,7 +1270,7 @@ export default class MatrixChat extends React.PureComponent { /** * Called when a new logged in session has started */ - private async onLoggedIn() { + private async onLoggedIn(): Promise { ThemeController.isLogin = false; this.themeWatcher.recheck(); StorageManager.tryPersistStorage(); @@ -1301,7 +1301,7 @@ export default class MatrixChat extends React.PureComponent { } } - private async onShowPostLoginScreen(useCase?: UseCase) { + private async onShowPostLoginScreen(useCase?: UseCase): Promise { if (useCase) { PosthogAnalytics.instance.setProperty("ftueUseCaseSelection", useCase); SettingsStore.setValue("FTUE.useCaseSelection", null, SettingLevel.ACCOUNT, useCase); @@ -1370,7 +1370,7 @@ export default class MatrixChat extends React.PureComponent { } } - private initPosthogAnalyticsToast() { + private initPosthogAnalyticsToast(): void { // Show the analytics toast if necessary if (SettingsStore.getValue("pseudonymousAnalyticsOptIn") === null) { showAnalyticsToast(); @@ -1397,7 +1397,7 @@ export default class MatrixChat extends React.PureComponent { ); } - private showScreenAfterLogin() { + private showScreenAfterLogin(): void { // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory if (this.screenAfterLogin && this.screenAfterLogin.screen) { @@ -1415,7 +1415,7 @@ export default class MatrixChat extends React.PureComponent { } } - private viewLastRoom() { + private viewLastRoom(): void { dis.dispatch({ action: Action.ViewRoom, room_id: localStorage.getItem("mx_last_room_id"), @@ -1426,7 +1426,7 @@ export default class MatrixChat extends React.PureComponent { /** * Called when the session is logged out */ - private onLoggedOut() { + private onLoggedOut(): void { this.viewLogin({ ready: false, collapseLhs: false, @@ -1439,7 +1439,7 @@ export default class MatrixChat extends React.PureComponent { /** * Called when the session is softly logged out */ - private onSoftLogout() { + private onSoftLogout(): void { this.notifyNewScreen("soft_logout"); this.setStateForNewView({ view: Views.SOFT_LOGOUT, @@ -1455,7 +1455,7 @@ export default class MatrixChat extends React.PureComponent { * Called just before the matrix client is started * (useful for setting listeners) */ - private onWillStartClient() { + private onWillStartClient(): void { // reset the 'have completed first sync' flag, // since we're about to start the client and therefore about // to do the first sync @@ -1610,7 +1610,7 @@ export default class MatrixChat extends React.PureComponent { break; } }); - cli.on(CryptoEvent.KeyBackupFailed, async (errcode) => { + cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise => { let haveNewVersion; let newVersionInfo; // if key backup is still enabled, there must be a new backup in place @@ -1678,7 +1678,7 @@ export default class MatrixChat extends React.PureComponent { * setting up anything that requires the client to be started. * @private */ - private onClientStarted() { + private onClientStarted(): void { const cli = MatrixClientPeg.get(); if (cli.isCryptoEnabled()) { @@ -1700,7 +1700,7 @@ export default class MatrixChat extends React.PureComponent { } } - public showScreen(screen: string, params?: { [key: string]: any }) { + public showScreen(screen: string, params?: { [key: string]: any }): void { const cli = MatrixClientPeg.get(); const isLoggedOutOrGuest = !cli || cli.isGuest(); if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) { @@ -1861,14 +1861,14 @@ export default class MatrixChat extends React.PureComponent { } } - private notifyNewScreen(screen: string, replaceLast = false) { + private notifyNewScreen(screen: string, replaceLast = false): void { if (this.props.onNewScreen) { this.props.onNewScreen(screen, replaceLast); } this.setPageSubtitle(); } - private onLogoutClick(event: React.MouseEvent) { + private onLogoutClick(event: React.MouseEvent): void { dis.dispatch({ action: "logout", }); @@ -1876,7 +1876,7 @@ export default class MatrixChat extends React.PureComponent { event.preventDefault(); } - private handleResize = () => { + private handleResize = (): void => { const LHS_THRESHOLD = 1000; const width = UIStore.instance.windowWidth; @@ -1892,19 +1892,19 @@ export default class MatrixChat extends React.PureComponent { this.state.resizeNotifier.notifyWindowResized(); }; - private dispatchTimelineResize() { + private dispatchTimelineResize(): void { dis.dispatch({ action: "timeline_resize" }); } - private onRegisterClick = () => { + private onRegisterClick = (): void => { this.showScreen("register"); }; - private onLoginClick = () => { + private onLoginClick = (): void => { this.showScreen("login"); }; - private onForgotPasswordClick = () => { + private onForgotPasswordClick = (): void => { this.showScreen("forgot_password"); }; @@ -1926,7 +1926,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private setPageSubtitle(subtitle = "") { + private setPageSubtitle(subtitle = ""): void { if (this.state.currentRoomId) { const client = MatrixClientPeg.get(); const room = client && client.getRoom(this.state.currentRoomId); @@ -1963,11 +1963,11 @@ export default class MatrixChat extends React.PureComponent { this.setPageSubtitle(); }; - private onServerConfigChange = (serverConfig: ValidatedServerConfig) => { + private onServerConfigChange = (serverConfig: ValidatedServerConfig): void => { this.setState({ serverConfig }); }; - private makeRegistrationUrl = (params: QueryDict) => { + private makeRegistrationUrl = (params: QueryDict): string => { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; } @@ -2016,7 +2016,7 @@ export default class MatrixChat extends React.PureComponent { return fragmentAfterLogin; } - public render() { + public render(): JSX.Element { const fragmentAfterLogin = this.getFragmentAfterLogin(); let view = null; @@ -2132,7 +2132,7 @@ export default class MatrixChat extends React.PureComponent { /> ); } else if (this.state.view === Views.USE_CASE_SELECTION) { - view = this.onShowPostLoginScreen(useCase)} />; + view = => this.onShowPostLoginScreen(useCase)} />; } else { logger.error(`Unknown view ${this.state.view}`); } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index cd3f322369..98e8f79ec7 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -296,19 +296,19 @@ export default class MessagePanel extends React.Component { ); } - public componentDidMount() { + public componentDidMount(): void { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); this.isMounted = true; } - public componentWillUnmount() { + public componentWillUnmount(): void { this.isMounted = false; this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); } - public componentDidUpdate(prevProps, prevState) { + public componentDidUpdate(prevProps, prevState): void { if (prevProps.layout !== this.props.layout) { this.calculateRoomMembersCount(); } @@ -752,7 +752,7 @@ export default class MessagePanel extends React.Component { const readReceipts = this.readReceiptsByEvent[eventId]; let isLastSuccessful = false; - const isSentState = (s) => !s || s === "sent"; + const isSentState = (s): boolean => !s || s === "sent"; const isSent = isSentState(mxEv.getAssociatedStatus()); const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); if (!hasNextEvent && isSent) { @@ -982,7 +982,7 @@ export default class MessagePanel extends React.Component { } } - public render() { + public render(): JSX.Element { let topSpinner; let bottomSpinner; if (this.props.backPaginating) { diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index c2926a6448..813522ffcb 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -37,15 +37,15 @@ export default class NonUrgentToastContainer extends React.PureComponent { + private onUpdateToasts = (): void => { this.setState({ toasts: NonUrgentToastStore.instance.components }); }; - public render() { + public render(): JSX.Element { const toasts = this.state.toasts.map((t, i) => { return (
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 9e4365880e..ac351399d4 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -55,7 +55,7 @@ export default class NotificationPanel extends React.PureComponent

{_t("You're all caught up")}

diff --git a/src/components/structures/PictureInPictureDragger.tsx b/src/components/structures/PictureInPictureDragger.tsx index 1daea9eb89..19205229c8 100644 --- a/src/components/structures/PictureInPictureDragger.tsx +++ b/src/components/structures/PictureInPictureDragger.tsx @@ -79,7 +79,7 @@ export default class PictureInPictureDragger extends React.Component { this._moving = value; } - public componentDidMount() { + public componentDidMount(): void { document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); UIStore.instance.on(UI_EVENTS.Resize, this.onResize); @@ -87,7 +87,7 @@ export default class PictureInPictureDragger extends React.Component { this.snap(); } - public componentWillUnmount() { + public componentWillUnmount(): void { document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); UIStore.instance.off(UI_EVENTS.Resize, this.onResize); @@ -97,7 +97,7 @@ export default class PictureInPictureDragger extends React.Component { if (prevProps.children !== this.props.children) this.snap(true); } - private animationCallback = () => { + private animationCallback = (): void => { if ( !this.moving && Math.abs(this.translationX - this.desiredTranslationX) <= 1 && @@ -119,13 +119,13 @@ export default class PictureInPictureDragger extends React.Component { this.props.onMove?.(); }; - private setStyle = () => { + private setStyle = (): void => { if (!this.callViewWrapper.current) return; // Set the element's style directly, bypassing React for efficiency this.callViewWrapper.current.style.transform = `translateX(${this.translationX}px) translateY(${this.translationY}px)`; }; - private setTranslation(inTranslationX: number, inTranslationY: number) { + private setTranslation(inTranslationX: number, inTranslationY: number): void { const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; @@ -152,7 +152,7 @@ export default class PictureInPictureDragger extends React.Component { this.snap(false); }; - private snap = (animate = false) => { + private snap = (animate = false): void => { const translationX = this.desiredTranslationX; const translationY = this.desiredTranslationY; // We subtract the PiP size from the window size in order to calculate @@ -187,14 +187,14 @@ export default class PictureInPictureDragger extends React.Component { this.scheduledUpdate.mark(); }; - private onStartMoving = (event: React.MouseEvent | MouseEvent) => { + private onStartMoving = (event: React.MouseEvent | MouseEvent): void => { event.preventDefault(); event.stopPropagation(); this.mouseHeld = true; }; - private onMoving = (event: MouseEvent) => { + private onMoving = (event: MouseEvent): void => { if (!this.mouseHeld) return; event.preventDefault(); @@ -210,7 +210,7 @@ export default class PictureInPictureDragger extends React.Component { this.setTranslation(event.pageX - this.initX, event.pageY - this.initY); }; - private onEndMoving = (event: MouseEvent) => { + private onEndMoving = (event: MouseEvent): void => { if (!this.mouseHeld) return; event.preventDefault(); @@ -223,7 +223,7 @@ export default class PictureInPictureDragger extends React.Component { this.snap(true); }; - private onClickCapture = (event: React.MouseEvent) => { + private onClickCapture = (event: React.MouseEvent): void => { // To prevent mouse up events during dragging from being double-counted // as clicks, we cancel clicks before they ever reach the target if (this.moving) { @@ -232,7 +232,7 @@ export default class PictureInPictureDragger extends React.Component { } }; - public render() { + public render(): JSX.Element { const style = { transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`, }; diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index a932c43e7d..416458e6ff 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -135,7 +135,7 @@ class PipContainerInner extends React.Component { }; } - public componentDidMount() { + public componentDidMount(): void { LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls); LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls); SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate); @@ -149,7 +149,7 @@ class PipContainerInner extends React.Component { ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges); } - public componentWillUnmount() { + public componentWillUnmount(): void { LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls); LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls); const cli = MatrixClientPeg.get(); @@ -164,9 +164,9 @@ class PipContainerInner extends React.Component { ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges); } - private onMove = () => this.props.movePersistedElement.current?.(); + private onMove = (): void => this.props.movePersistedElement.current?.(); - private onRoomViewStoreUpdate = () => { + private onRoomViewStoreUpdate = (): void => { const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); const oldRoomId = this.state.viewedRoomId; if (newRoomId === oldRoomId) return; @@ -213,7 +213,7 @@ class PipContainerInner extends React.Component { this.updateShowWidgetInPip(); }; - private onCallRemoteHold = () => { + private onCallRemoteHold = (): void => { if (!this.state.viewedRoomId) return; const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.viewedRoomId); @@ -238,7 +238,7 @@ class PipContainerInner extends React.Component { public updateShowWidgetInPip( persistentWidgetId = this.state.persistentWidgetId, persistentRoomId = this.state.persistentRoomId, - ) { + ): void { let fromAnotherRoom = false; let notDocked = false; // Sanity check the room - the widget may have been destroyed between render cycles, and @@ -293,7 +293,7 @@ class PipContainerInner extends React.Component { ); } - public render() { + public render(): JSX.Element { const pipMode = true; let pipContent: Array = []; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 8759160057..3748ee0ec7 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -101,7 +101,7 @@ export default class RightPanel extends React.Component { }; } - private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => { + private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => { if (!this.props.room || member.roomId !== this.props.room.roomId) { return; } @@ -118,11 +118,11 @@ export default class RightPanel extends React.Component { } }; - private onRightPanelStoreUpdate = () => { + private onRightPanelStoreUpdate = (): void => { this.setState({ ...(RightPanel.getDerivedStateFromProps(this.props) as IState) }); }; - private onClose = () => { + private onClose = (): void => { // XXX: There are three different ways of 'closing' this panel depending on what state // things are in... this knows far more than it should do about the state of the rest // of the app and is generally a bit silly. diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 6c7ddbe755..a387a2e0d5 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -39,15 +39,15 @@ export default class RoomSearch extends React.PureComponent { this.dispatcherRef = defaultDispatcher.register(this.onAction); } - public componentWillUnmount() { + public componentWillUnmount(): void { defaultDispatcher.unregister(this.dispatcherRef); } - private openSpotlight() { + private openSpotlight(): void { Modal.createDialog(SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true); } - private onAction = (payload: ActionPayload) => { + private onAction = (payload: ActionPayload): void => { if (payload.action === "focus_room_filter") { this.openSpotlight(); } diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index d7a995b5c0..132e2a191b 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -37,7 +37,7 @@ import RoomContext from "../../contexts/RoomContext"; import SettingsStore from "../../settings/SettingsStore"; const DEBUG = false; -let debuglog = function (msg: string) {}; +let debuglog = function (msg: string): void {}; /* istanbul ignore next */ if (DEBUG) { @@ -76,7 +76,7 @@ export const RoomSearchView = forwardRef( return searchPromise .then( - async (results) => { + async (results): Promise => { debuglog("search complete"); if (aborted.current) { logger.error("Discarding stale search results"); @@ -209,7 +209,7 @@ export const RoomSearchView = forwardRef( // once dynamic content in the search results load, make the scrollPanel check // the scroll offsets. - const onHeightChanged = () => { + const onHeightChanged = (): void => { const scrollPanel = ref.current; scrollPanel?.checkScroll(); }; diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 7d621afc5d..f370091a8a 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -146,7 +146,7 @@ export default class RoomStatusBar extends React.PureComponent { dis.fire(Action.FocusSendMessageComposer); }; - private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { + private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room): void => { if (room.roomId !== this.props.room.roomId) return; const messages = getUnsentMessages(this.props.room); this.setState({ diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index eb034fd2b7..8d85b54df7 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -115,7 +115,7 @@ import VoipUserMapper from "../../VoipUserMapper"; import { isCallEvent } from "./LegacyCallEventGrouper"; const DEBUG = false; -let debuglog = function (msg: string) {}; +let debuglog = function (msg: string): void {}; const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe"); @@ -248,7 +248,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { encryptionTile = ; } - const onRetryClicked = () => { + const onRetryClicked = (): void => { room.state = LocalRoomState.NEW; defaultDispatcher.dispatch({ action: "local_room_event", @@ -470,21 +470,21 @@ export class RoomView extends React.Component { ]; } - private onIsResizing = (resizing: boolean) => { + private onIsResizing = (resizing: boolean): void => { this.setState({ resizing }); }; - private onWidgetStoreUpdate = () => { + private onWidgetStoreUpdate = (): void => { if (!this.state.room) return; this.checkWidgets(this.state.room); }; - private onWidgetEchoStoreUpdate = () => { + private onWidgetEchoStoreUpdate = (): void => { if (!this.state.room) return; this.checkWidgets(this.state.room); }; - private onWidgetLayoutChange = () => { + private onWidgetLayoutChange = (): void => { if (!this.state.room) return; dis.dispatch({ action: "appsDrawer", @@ -505,7 +505,7 @@ export class RoomView extends React.Component { }); }; - private getMainSplitContentType = (room: Room) => { + private getMainSplitContentType = (room: Room): MainSplitContentType => { if ( (SettingsStore.getValue("feature_group_calls") && this.context.roomViewStore.isViewingCall()) || isVideoRoom(room) @@ -707,7 +707,7 @@ export class RoomView extends React.Component { } }; - private onActiveCalls = () => { + private onActiveCalls = (): void => { if (this.state.roomId === undefined) return; const activeCall = CallStore.instance.getActiveCall(this.state.roomId); @@ -727,7 +727,7 @@ export class RoomView extends React.Component { this.setState({ activeCall }); }; - private getRoomId = () => { + private getRoomId = (): string => { // According to `onRoomViewStoreUpdate`, `state.roomId` can be null // if we have a room alias we haven't resolved yet. To work around this, // first we'll try the room object if it's there, and then fallback to @@ -736,7 +736,7 @@ export class RoomView extends React.Component { return this.state.room ? this.state.room.roomId : this.state.roomId; }; - private getPermalinkCreatorForRoom(room: Room) { + private getPermalinkCreatorForRoom(room: Room): RoomPermalinkCreator { if (this.permalinkCreators[room.roomId]) return this.permalinkCreators[room.roomId]; this.permalinkCreators[room.roomId] = new RoomPermalinkCreator(room); @@ -750,14 +750,14 @@ export class RoomView extends React.Component { return this.permalinkCreators[room.roomId]; } - private stopAllPermalinkCreators() { + private stopAllPermalinkCreators(): void { if (!this.permalinkCreators) return; for (const roomId of Object.keys(this.permalinkCreators)) { this.permalinkCreators[roomId].stop(); } } - private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) { + private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean): void { // if this is an unknown room then we're in one of three states: // - This is a room we can peek into (search engine) (we can /peek) // - This is a room we can publicly join or were invited to. (we can /join) @@ -822,7 +822,7 @@ export class RoomView extends React.Component { } } - private shouldShowApps(room: Room) { + private shouldShowApps(room: Room): boolean { if (!BROWSER_SUPPORTS_SANDBOX || !room) return false; // Check if user has previously chosen to hide the app drawer for this @@ -838,7 +838,7 @@ export class RoomView extends React.Component { return isManuallyShown && widgets.length > 0; } - public componentDidMount() { + public componentDidMount(): void { this.onRoomViewStoreUpdate(true); const call = this.getCallForRoom(); @@ -851,7 +851,7 @@ export class RoomView extends React.Component { window.addEventListener("beforeunload", this.onPageUnload); } - public shouldComponentUpdate(nextProps, nextState) { + public shouldComponentUpdate(nextProps, nextState): boolean { const hasPropsDiff = objectHasDiff(this.props, nextProps); const { upgradeRecommendation, ...state } = this.state; @@ -864,7 +864,7 @@ export class RoomView extends React.Component { return hasPropsDiff || hasStateDiff; } - public componentDidUpdate() { + public componentDidUpdate(): void { // Note: We check the ref here with a flag because componentDidMount, despite // documentation, does not define our messagePanel ref. It looks like our spinner // in render() prevents the ref from being set on first mount, so we try and @@ -877,7 +877,7 @@ export class RoomView extends React.Component { } } - public componentWillUnmount() { + public componentWillUnmount(): void { // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. // @@ -947,13 +947,13 @@ export class RoomView extends React.Component { } } - private onRightPanelStoreUpdate = () => { + private onRightPanelStoreUpdate = (): void => { this.setState({ showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.state.roomId), }); }; - private onPageUnload = (event) => { + private onPageUnload = (event): string => { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { return (event.returnValue = _t("You seem to be uploading files, are you sure you want to quit?")); } else if (this.getCallForRoom() && this.state.callState !== "ended") { @@ -961,7 +961,7 @@ export class RoomView extends React.Component { } }; - private onReactKeyDown = (ev) => { + private onReactKeyDown = (ev): void => { let handled = false; const action = getKeyBindingsManager().getRoomAction(ev); @@ -1120,12 +1120,12 @@ export class RoomView extends React.Component { } }; - private onLocalRoomEvent(roomId: string) { + private onLocalRoomEvent(roomId: string): void { if (roomId !== this.state.room.roomId) return; createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom); } - private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data) => { + private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data): void => { if (this.unmounted) return; // ignore events for other rooms or the notification timeline set @@ -1167,7 +1167,7 @@ export class RoomView extends React.Component { } }; - private onEventDecrypted = (ev: MatrixEvent) => { + private onEventDecrypted = (ev: MatrixEvent): void => { if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all if (ev.getRoomId() !== this.state.room.roomId) return; // not for us this.updateVisibleDecryptionFailures(); @@ -1175,7 +1175,7 @@ export class RoomView extends React.Component { this.handleEffects(ev); }; - private handleEffects = (ev: MatrixEvent) => { + private handleEffects = (ev: MatrixEvent): void => { const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room); if (!notifState.isUnread) return; @@ -1189,19 +1189,19 @@ export class RoomView extends React.Component { }); }; - private onRoomName = (room: Room) => { + private onRoomName = (room: Room): void => { if (this.state.room && room.roomId == this.state.room.roomId) { this.forceUpdate(); } }; - private onKeyBackupStatus = () => { + private onKeyBackupStatus = (): void => { // Key backup status changes affect whether the in-room recovery // reminder is displayed. this.forceUpdate(); }; - public canResetTimeline = () => { + public canResetTimeline = (): boolean => { if (!this.messagePanel) { return true; } @@ -1216,7 +1216,7 @@ export class RoomView extends React.Component { // called when state.room is first initialised (either at initial load, // after a successful peek, or after we join the room). - private onRoomLoaded = (room: Room) => { + private onRoomLoaded = (room: Room): void => { if (this.unmounted) return; // Attach a widget store listener only when we get a room this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); @@ -1251,17 +1251,17 @@ export class RoomView extends React.Component { } }; - private getRoomTombstone(room = this.state.room) { + private getRoomTombstone(room = this.state.room): MatrixEvent | undefined { return room?.currentState.getStateEvents(EventType.RoomTombstone, ""); } - private async calculateRecommendedVersion(room: Room) { + private async calculateRecommendedVersion(room: Room): Promise { const upgradeRecommendation = await room.getRecommendedVersion(); if (this.unmounted) return; this.setState({ upgradeRecommendation }); } - private async loadMembersIfJoined(room: Room) { + private async loadMembersIfJoined(room: Room): Promise { // lazy load members if enabled if (this.context.client.hasLazyLoadMembersEnabled()) { if (room && room.getMyMembership() === "join") { @@ -1280,14 +1280,14 @@ export class RoomView extends React.Component { } } - private calculatePeekRules(room: Room) { + private calculatePeekRules(room: Room): void { const historyVisibility = room.currentState.getStateEvents(EventType.RoomHistoryVisibility, ""); this.setState({ canPeek: historyVisibility?.getContent().history_visibility === HistoryVisibility.WorldReadable, }); } - private updatePreviewUrlVisibility({ roomId }: Room) { + private updatePreviewUrlVisibility({ roomId }: Room): void { // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit const key = this.context.client.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; this.setState({ @@ -1295,7 +1295,7 @@ export class RoomView extends React.Component { }); } - private onRoom = (room: Room) => { + private onRoom = (room: Room): void => { if (!room || room.roomId !== this.state.roomId) { return; } @@ -1318,7 +1318,7 @@ export class RoomView extends React.Component { ); }; - private onDeviceVerificationChanged = (userId: string) => { + private onDeviceVerificationChanged = (userId: string): void => { const room = this.state.room; if (!room?.currentState.getMember(userId)) { return; @@ -1326,7 +1326,7 @@ export class RoomView extends React.Component { this.updateE2EStatus(room); }; - private onUserVerificationChanged = (userId: string) => { + private onUserVerificationChanged = (userId: string): void => { const room = this.state.room; if (!room || !room.currentState.getMember(userId)) { return; @@ -1334,14 +1334,14 @@ export class RoomView extends React.Component { this.updateE2EStatus(room); }; - private onCrossSigningKeysChanged = () => { + private onCrossSigningKeysChanged = (): void => { const room = this.state.room; if (room) { this.updateE2EStatus(room); } }; - private async updateE2EStatus(room: Room) { + private async updateE2EStatus(room: Room): Promise { if (!this.context.client.isRoomEncrypted(room.roomId)) return; // If crypto is not currently enabled, we aren't tracking devices at all, @@ -1357,13 +1357,13 @@ export class RoomView extends React.Component { this.setState({ e2eStatus }); } - private onUrlPreviewsEnabledChange = () => { + private onUrlPreviewsEnabledChange = (): void => { if (this.state.room) { this.updatePreviewUrlVisibility(this.state.room); } }; - private onRoomStateEvents = (ev: MatrixEvent, state: RoomState) => { + private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { // ignore if we don't have a room yet if (!this.state.room || this.state.room.roomId !== state.roomId) return; @@ -1377,7 +1377,7 @@ export class RoomView extends React.Component { } }; - private onRoomStateUpdate = (state: RoomState) => { + private onRoomStateUpdate = (state: RoomState): void => { // ignore members in other rooms if (state.roomId !== this.state.room?.roomId) { return; @@ -1386,7 +1386,7 @@ export class RoomView extends React.Component { this.updateRoomMembers(); }; - private onMyMembership = (room: Room, membership: string, oldMembership: string) => { + private onMyMembership = (room: Room, membership: string, oldMembership: string): void => { if (room.roomId === this.state.roomId) { this.forceUpdate(); this.loadMembersIfJoined(room); @@ -1394,7 +1394,7 @@ export class RoomView extends React.Component { } }; - private updatePermissions(room: Room) { + private updatePermissions(room: Room): void { if (room) { const me = this.context.client.getUserId(); const canReact = @@ -1420,7 +1420,7 @@ export class RoomView extends React.Component { { leading: true, trailing: true }, ); - private checkDesktopNotifications() { + private checkDesktopNotifications(): void { const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); // if they are not alone prompt the user about notifications so they don't miss replies if (memberCount > 1 && Notifier.shouldShowPrompt()) { @@ -1428,7 +1428,7 @@ export class RoomView extends React.Component { } } - private updateDMState() { + private updateDMState(): void { const room = this.state.room; if (room.getMyMembership() != "join") { return; @@ -1439,7 +1439,7 @@ export class RoomView extends React.Component { } } - private onInviteClick = () => { + private onInviteClick = (): void => { // open the room inviter dis.dispatch({ action: "view_invite", @@ -1447,7 +1447,7 @@ export class RoomView extends React.Component { }); }; - private onJoinButtonClicked = () => { + private onJoinButtonClicked = (): void => { // If the user is a ROU, allow them to transition to a PWLU if (this.context.client?.isGuest()) { // Join this room once the user has registered and logged in @@ -1489,7 +1489,7 @@ export class RoomView extends React.Component { { leading: false, trailing: true }, ); - private onMessageListScroll = () => { + private onMessageListScroll = (): void => { if (this.messagePanel.isAtEndOfLiveTimeline()) { this.setState({ numUnreadMessages: 0, @@ -1504,7 +1504,7 @@ export class RoomView extends React.Component { this.updateVisibleDecryptionFailures(); }; - private resetJumpToEvent = (eventId?: string) => { + private resetJumpToEvent = (eventId?: string): void => { if ( this.state.initialEventId && this.state.initialEventScrollIntoView && @@ -1523,7 +1523,7 @@ export class RoomView extends React.Component { } }; - private injectSticker(url: string, info: object, text: string, threadId: string | null) { + private injectSticker(url: string, info: object, text: string, threadId: string | null): void { if (this.context.client.isGuest()) { dis.dispatch({ action: "require_registration" }); return; @@ -1539,7 +1539,7 @@ export class RoomView extends React.Component { }); } - private onSearch = (term: string, scope: SearchScope) => { + private onSearch = (term: string, scope: SearchScope): void => { const roomId = scope === SearchScope.Room ? this.state.room.roomId : undefined; debuglog("sending search request"); const abortController = new AbortController(); @@ -1569,21 +1569,21 @@ export class RoomView extends React.Component { }); }; - private onAppsClick = () => { + private onAppsClick = (): void => { dis.dispatch({ action: "appsDrawer", show: !this.state.showApps, }); }; - private onForgetClick = () => { + private onForgetClick = (): void => { dis.dispatch({ action: "forget_room", room_id: this.state.room.roomId, }); }; - private onRejectButtonClicked = () => { + private onRejectButtonClicked = (): void => { this.setState({ rejecting: true, }); @@ -1611,7 +1611,7 @@ export class RoomView extends React.Component { ); }; - private onRejectAndIgnoreClick = async () => { + private onRejectAndIgnoreClick = async (): Promise => { this.setState({ rejecting: true, }); @@ -1644,7 +1644,7 @@ export class RoomView extends React.Component { } }; - private onRejectThreepidInviteButtonClicked = () => { + private onRejectThreepidInviteButtonClicked = (): void => { // We can reject 3pid invites in the same way that we accept them, // using /leave rather than /join. In the short term though, we // just ignore them. @@ -1652,7 +1652,7 @@ export class RoomView extends React.Component { dis.fire(Action.ViewRoomDirectory); }; - private onSearchClick = () => { + private onSearchClick = (): void => { this.setState({ timelineRenderingType: this.state.timelineRenderingType === TimelineRenderingType.Search @@ -1674,7 +1674,7 @@ export class RoomView extends React.Component { }; // jump down to the bottom of this room, where new events are arriving - private jumpToLiveTimeline = () => { + private jumpToLiveTimeline = (): void => { if (this.state.initialEventId && this.state.isInitialEventHighlighted) { // If we were viewing a highlighted event, firing view_room without // an event will take care of both clearing the URL fragment and @@ -1692,18 +1692,18 @@ export class RoomView extends React.Component { }; // jump up to wherever our read marker is - private jumpToReadMarker = () => { + private jumpToReadMarker = (): void => { this.messagePanel.jumpToReadMarker(); }; // update the read marker to match the read-receipt - private forgetReadMarker = (ev) => { + private forgetReadMarker = (ev): void => { ev.stopPropagation(); this.messagePanel.forgetReadMarker(); }; // decide whether or not the top 'unread messages' bar should be shown - private updateTopUnreadMessagesBar = () => { + private updateTopUnreadMessagesBar = (): void => { if (!this.messagePanel) { return; } @@ -1754,12 +1754,12 @@ export class RoomView extends React.Component { }; } - private onStatusBarVisible = () => { + private onStatusBarVisible = (): void => { if (this.unmounted || this.state.statusBarVisible) return; this.setState({ statusBarVisible: true }); }; - private onStatusBarHidden = () => { + private onStatusBarHidden = (): void => { // This is currently not desired as it is annoying if it keeps expanding and collapsing if (this.unmounted || !this.state.statusBarVisible) return; this.setState({ statusBarVisible: false }); @@ -1770,7 +1770,7 @@ export class RoomView extends React.Component { * * We pass it down to the scroll panel. */ - public handleScrollKey = (ev) => { + public handleScrollKey = (ev): void => { let panel: ScrollPanel | TimelinePanel; if (this.searchResultsPanel.current) { panel = this.searchResultsPanel.current; @@ -1793,24 +1793,24 @@ export class RoomView extends React.Component { // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. - private gatherTimelinePanelRef = (r) => { + private gatherTimelinePanelRef = (r): void => { this.messagePanel = r; }; - private getOldRoom() { + private getOldRoom(): Room | null { const createEvent = this.state.room.currentState.getStateEvents(EventType.RoomCreate, ""); if (!createEvent || !createEvent.getContent()["predecessor"]) return null; return this.context.client.getRoom(createEvent.getContent()["predecessor"]["room_id"]); } - public getHiddenHighlightCount() { + public getHiddenHighlightCount(): number { const oldRoom = this.getOldRoom(); if (!oldRoom) return 0; return oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight); } - public onHiddenHighlightsClick = () => { + public onHiddenHighlightsClick = (): void => { const oldRoom = this.getOldRoom(); if (!oldRoom) return; dis.dispatch({ @@ -1826,7 +1826,7 @@ export class RoomView extends React.Component { }); } - private onFileDrop = (dataTransfer: DataTransfer) => + private onFileDrop = (dataTransfer: DataTransfer): Promise => ContentMessages.sharedInstance().sendContentListToRoom( Array.from(dataTransfer.files), this.state.room?.roomId ?? this.state.roomId, @@ -1869,7 +1869,7 @@ export class RoomView extends React.Component { ); } - public render() { + public render(): JSX.Element { if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { return this.renderLocalRoomCreateLoader(); diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 66676666df..f51cba66a3 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -35,7 +35,7 @@ const UNFILL_REQUEST_DEBOUNCE_MS = 200; // much while the content loads. const PAGE_SIZE = 400; -const debuglog = (...args: any[]) => { +const debuglog = (...args: any[]): void => { if (SettingsStore.getValue("debug_scroll_panel")) { logger.log.call(console, "ScrollPanel debuglog:", ...args); } @@ -227,14 +227,14 @@ export default class ScrollPanel extends React.Component { this.props.resizeNotifier?.removeListener("middlePanelResizedNoisy", this.onResize); } - private onScroll = (ev: Event | React.UIEvent): void => { + private onScroll = (ev: Event): void => { // skip scroll events caused by resizing if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return; debuglog("onScroll called past resize gate; scroll node top:", this.getScrollNode().scrollTop); this.scrollTimeout.restart(); this.saveScrollState(); this.updatePreventShrinking(); - this.props.onScroll?.(ev as Event); + this.props.onScroll?.(ev); // noinspection JSIgnoredPromiseFromCall this.checkFillState(); }; @@ -587,7 +587,7 @@ export default class ScrollPanel extends React.Component { * Scroll up/down in response to a scroll key * @param {object} ev the keyboard event */ - public handleScrollKey = (ev: KeyboardEvent) => { + public handleScrollKey = (ev: KeyboardEvent): void => { const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { case KeyBindingAction.ScrollUp: @@ -853,7 +853,7 @@ export default class ScrollPanel extends React.Component { return this.divScroll; } - private collectScroll = (divScroll: HTMLDivElement) => { + private collectScroll = (divScroll: HTMLDivElement): void => { this.divScroll = divScroll; }; diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 13674347aa..af6f298382 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -114,12 +114,12 @@ const Tile: React.FC = ({ const [onFocus, isActive, ref] = useRovingTabIndex(); const [busy, setBusy] = useState(false); - const onPreviewClick = (ev: ButtonEvent) => { + const onPreviewClick = (ev: ButtonEvent): void => { ev.preventDefault(); ev.stopPropagation(); onViewRoomClick(); }; - const onJoinClick = async (ev: ButtonEvent) => { + const onJoinClick = async (ev: ButtonEvent): Promise => { setBusy(true); ev.preventDefault(); ev.stopPropagation(); @@ -271,7 +271,7 @@ const Tile: React.FC = ({ ); if (showChildren) { - const onChildrenKeyDown = (e) => { + const onChildrenKeyDown = (e): void => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { case KeyBindingAction.ArrowLeft: @@ -439,7 +439,7 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom): IHierarchyRoom => return room; }; -export const HierarchyLevel = ({ +export const HierarchyLevel: React.FC = ({ root, roomSet, hierarchy, @@ -448,7 +448,7 @@ export const HierarchyLevel = ({ onViewRoomClick, onJoinRoomClick, onToggleClick, -}: IHierarchyLevelProps) => { +}) => { const cli = useContext(MatrixClientContext); const space = cli.getRoom(root.room_id); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); @@ -553,7 +553,7 @@ export const useRoomHierarchy = ( }); const loadMore = useCallback( - async (pageSize?: number) => { + async (pageSize?: number): Promise => { if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport || error) return; await hierarchy.load(pageSize).catch(setError); setRooms(hierarchy.rooms); @@ -578,8 +578,8 @@ export const useRoomHierarchy = ( }; }; -const useIntersectionObserver = (callback: () => void) => { - const handleObserver = (entries: IntersectionObserverEntry[]) => { +const useIntersectionObserver = (callback: () => void): ((element: HTMLDivElement) => void) => { + const handleObserver = (entries: IntersectionObserverEntry[]): void => { const target = entries[0]; if (target.isIntersecting) { callback(); @@ -610,7 +610,7 @@ interface IManageButtonsProps { setError: Dispatch>; } -const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageButtonsProps) => { +const ManageButtons: React.FC = ({ hierarchy, selected, setSelected, setError }) => { const cli = useContext(MatrixClientContext); const [removing, setRemoving] = useState(false); @@ -645,7 +645,7 @@ const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageBu <>
-
{_t("Sessions")}
From 33e8a62daedfdcb85dc588bc4dd9df1d2942a4af Mon Sep 17 00:00:00 2001 From: Kerry Date: Mon, 16 Jan 2023 14:25:33 +1300 Subject: [PATCH 32/92] convert MPollBody tests into rtl (#9906) * convert MPollBody tests into rtl * strict fixes * more strict * more semantic assertions * update types for extensible events changes --- src/components/views/messages/MPollBody.tsx | 13 +- .../views/messages/MPollBody-test.tsx | 618 +-- .../__snapshots__/MPollBody-test.tsx.snap | 3575 ++++++----------- 3 files changed, 1616 insertions(+), 2590 deletions(-) diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 85500cfbb2..a9317957c6 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -448,7 +448,7 @@ export default class MPollBody extends React.Component { return (
-

+

{poll.question.text} {editedSpan}

@@ -471,7 +471,12 @@ export default class MPollBody extends React.Component { const answerPercent = totalVotes === 0 ? 0 : Math.round((100.0 * answerVotes) / totalVotes); return ( -
this.selectOption(answer.id)}> +
this.selectOption(answer.id)} + > {ended ? ( ) : ( @@ -493,7 +498,9 @@ export default class MPollBody extends React.Component { ); })}
-
{totalText}
+
+ {totalText} +
); } diff --git a/test/components/views/messages/MPollBody-test.tsx b/test/components/views/messages/MPollBody-test.tsx index 1be993f88e..a6f9b5e11c 100644 --- a/test/components/views/messages/MPollBody-test.tsx +++ b/test/components/views/messages/MPollBody-test.tsx @@ -15,8 +15,7 @@ limitations under the License. */ import React from "react"; -// eslint-disable-next-line deprecate/import -import { mount, ReactWrapper } from "enzyme"; +import { fireEvent, render, RenderResult } from "@testing-library/react"; import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { Relations } from "matrix-js-sdk/src/models/relations"; import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; @@ -44,6 +43,8 @@ import { IBodyProps } from "../../../../src/components/views/messages/IBodyProps import { getMockClientWithEventEmitter } from "../../../test-utils"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import MPollBody from "../../../../src/components/views/messages/MPollBody"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; +import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; const CHECKED = "mx_MPollBody_option_checked"; @@ -85,13 +86,13 @@ describe("MPollBody", () => { new RelatedRelations([newEndRelations([])]), ), ).toEqual([ - new UserVote(ev1.getTs(), ev1.getSender(), ev1.getContent()[M_POLL_RESPONSE.name].answers), + new UserVote(ev1.getTs(), ev1.getSender()!, ev1.getContent()[M_POLL_RESPONSE.name].answers), new UserVote( badEvent.getTs(), - badEvent.getSender(), + badEvent.getSender()!, [], // should be spoiled ), - new UserVote(ev2.getTs(), ev2.getSender(), ev2.getContent()[M_POLL_RESPONSE.name].answers), + new UserVote(ev2.getTs(), ev2.getSender()!, ev2.getContent()[M_POLL_RESPONSE.name].answers), ]); }); @@ -146,14 +147,14 @@ describe("MPollBody", () => { }); it("renders no votes if none were made", () => { - const votes = []; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe(""); - expect(votesCount(body, "poutine")).toBe(""); - expect(votesCount(body, "italian")).toBe(""); - expect(votesCount(body, "wings")).toBe(""); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("No votes cast"); - expect(body.find("h2").html()).toEqual("

What should we order for the party?

"); + const votes: MatrixEvent[] = []; + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe(""); + expect(votesCount(renderResult, "poutine")).toBe(""); + expect(votesCount(renderResult, "italian")).toBe(""); + expect(votesCount(renderResult, "wings")).toBe(""); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("No votes cast"); + expect(renderResult.getByText("What should we order for the party?")).toBeTruthy(); }); it("finds votes from multiple people", () => { @@ -163,12 +164,12 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe("2 votes"); - expect(votesCount(body, "poutine")).toBe("1 vote"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe("2 votes"); + expect(votesCount(renderResult, "poutine")).toBe("1 vote"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); }); it("ignores end poll events from unauthorised users", () => { @@ -179,15 +180,15 @@ describe("MPollBody", () => { responseEvent("@dune2:example.com", "wings"), ]; const ends = [endEvent("@notallowed:example.com", 12)]; - const body = newMPollBody(votes, ends); + const renderResult = newMPollBody(votes, ends); // Even though an end event was sent, we render the poll as unfinished // because this person is not allowed to send these events - expect(votesCount(body, "pizza")).toBe("2 votes"); - expect(votesCount(body, "poutine")).toBe("1 vote"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes"); + expect(votesCount(renderResult, "pizza")).toBe("2 votes"); + expect(votesCount(renderResult, "poutine")).toBe("1 vote"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); }); it("hides scores if I have not voted", () => { @@ -197,22 +198,22 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe(""); - expect(votesCount(body, "poutine")).toBe(""); - expect(votesCount(body, "italian")).toBe(""); - expect(votesCount(body, "wings")).toBe(""); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("4 votes cast. Vote to see the results"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe(""); + expect(votesCount(renderResult, "poutine")).toBe(""); + expect(votesCount(renderResult, "italian")).toBe(""); + expect(votesCount(renderResult, "wings")).toBe(""); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("4 votes cast. Vote to see the results"); }); it("hides a single vote if I have not voted", () => { const votes = [responseEvent("@alice:example.com", "pizza")]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe(""); - expect(votesCount(body, "poutine")).toBe(""); - expect(votesCount(body, "italian")).toBe(""); - expect(votesCount(body, "wings")).toBe(""); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("1 vote cast. Vote to see the results"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe(""); + expect(votesCount(renderResult, "poutine")).toBe(""); + expect(votesCount(renderResult, "italian")).toBe(""); + expect(votesCount(renderResult, "wings")).toBe(""); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("1 vote cast. Vote to see the results"); }); it("takes someone's most recent vote if they voted several times", () => { @@ -223,12 +224,12 @@ describe("MPollBody", () => { responseEvent("@qbert:example.com", "poutine", 16), // latest qbert responseEvent("@qbert:example.com", "wings", 15), ]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe("0 votes"); - expect(votesCount(body, "poutine")).toBe("1 vote"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe("0 votes"); + expect(votesCount(renderResult, "poutine")).toBe("1 vote"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); }); it("uses my local vote", () => { @@ -238,18 +239,18 @@ describe("MPollBody", () => { responseEvent("@fg:example.com", "pizza", 15), responseEvent("@hi:example.com", "pizza", 15), ]; - const body = newMPollBody(votes); + const renderResult = newMPollBody(votes); // When I vote for Italian - clickRadio(body, "italian"); + clickOption(renderResult, "italian"); // My vote is counted - expect(votesCount(body, "pizza")).toBe("3 votes"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("1 vote"); - expect(votesCount(body, "wings")).toBe("0 votes"); + expect(votesCount(renderResult, "pizza")).toBe("3 votes"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("1 vote"); + expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); }); it("overrides my other votes with my local vote", () => { @@ -260,53 +261,66 @@ describe("MPollBody", () => { responseEvent("@me:example.com", "italian", 14), responseEvent("@nf:example.com", "italian", 15), ]; - const body = newMPollBody(votes); + const renderResult = newMPollBody(votes); // When I click Wings - clickRadio(body, "wings"); + clickOption(renderResult, "wings"); // Then my vote is counted for Wings, and not for Italian - expect(votesCount(body, "pizza")).toBe("0 votes"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("1 vote"); - expect(votesCount(body, "wings")).toBe("1 vote"); + expect(votesCount(renderResult, "pizza")).toBe("0 votes"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("1 vote"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); // And my vote is highlighted - expect(voteButton(body, "wings").hasClass(CHECKED)).toBe(true); - expect(voteButton(body, "italian").hasClass(CHECKED)).toBe(false); + expect(voteButton(renderResult, "wings").className.includes(CHECKED)).toBe(true); + expect(voteButton(renderResult, "italian").className.includes(CHECKED)).toBe(false); }); it("cancels my local vote if another comes in", () => { // Given I voted locally const votes = [responseEvent("@me:example.com", "pizza", 100)]; - const body = newMPollBody(votes); - const props: IBodyProps = body.instance().props as IBodyProps; + const mxEvent = new MatrixEvent({ + type: M_POLL_START.name, + event_id: "$mypoll", + room_id: "#myroom:example.com", + content: newPollStart(undefined, undefined, true), + }); + const props = getMPollBodyPropsFromEvent(mxEvent, votes); + const renderResult = renderMPollBodyWithWrapper(props); const voteRelations = props!.getRelationsForEvent!("$mypoll", "m.reference", M_POLL_RESPONSE.name); expect(voteRelations).toBeDefined(); - clickRadio(body, "pizza"); + clickOption(renderResult, "pizza"); // When a new vote from me comes in voteRelations!.addEvent(responseEvent("@me:example.com", "wings", 101)); // Then the new vote is counted, not the old one - expect(votesCount(body, "pizza")).toBe("0 votes"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("1 vote"); + expect(votesCount(renderResult, "pizza")).toBe("0 votes"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); }); it("doesn't cancel my local vote if someone else votes", () => { // Given I voted locally const votes = [responseEvent("@me:example.com", "pizza")]; - const body = newMPollBody(votes); - const props: IBodyProps = body.instance().props as IBodyProps; + const mxEvent = new MatrixEvent({ + type: M_POLL_START.name, + event_id: "$mypoll", + room_id: "#myroom:example.com", + content: newPollStart(undefined, undefined, true), + }); + const props = getMPollBodyPropsFromEvent(mxEvent, votes); + const renderResult = renderMPollBodyWithWrapper(props); + const voteRelations = props!.getRelationsForEvent!("$mypoll", "m.reference", M_POLL_RESPONSE.name); expect(voteRelations).toBeDefined(); - clickRadio(body, "pizza"); + clickOption(renderResult, "pizza"); // When a new vote from someone else comes in voteRelations!.addEvent(responseEvent("@xx:example.com", "wings", 101)); @@ -315,39 +329,39 @@ describe("MPollBody", () => { // NOTE: the new event does not affect the counts for other people - // that is handled through the Relations, not by listening to // these timeline events. - expect(votesCount(body, "pizza")).toBe("1 vote"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("1 vote"); + expect(votesCount(renderResult, "pizza")).toBe("1 vote"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); // And my vote is highlighted - expect(voteButton(body, "pizza").hasClass(CHECKED)).toBe(true); - expect(voteButton(body, "wings").hasClass(CHECKED)).toBe(false); + expect(voteButton(renderResult, "pizza").className.includes(CHECKED)).toBe(true); + expect(voteButton(renderResult, "wings").className.includes(CHECKED)).toBe(false); }); it("highlights my vote even if I did it on another device", () => { // Given I voted italian const votes = [responseEvent("@me:example.com", "italian"), responseEvent("@nf:example.com", "wings")]; - const body = newMPollBody(votes); + const renderResult = newMPollBody(votes); // But I didn't click anything locally // Then my vote is highlighted, and others are not - expect(voteButton(body, "italian").hasClass(CHECKED)).toBe(true); - expect(voteButton(body, "wings").hasClass(CHECKED)).toBe(false); + expect(voteButton(renderResult, "italian").className.includes(CHECKED)).toBe(true); + expect(voteButton(renderResult, "wings").className.includes(CHECKED)).toBe(false); }); it("ignores extra answers", () => { // When cb votes for 2 things, we consider the first only const votes = [responseEvent("@cb:example.com", ["pizza", "wings"]), responseEvent("@me:example.com", "wings")]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe("1 vote"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe("1 vote"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); }); it("allows un-voting by passing an empty vote", () => { @@ -356,12 +370,12 @@ describe("MPollBody", () => { responseEvent("@nc:example.com", [], 13), responseEvent("@me:example.com", "italian"), ]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe("0 votes"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("1 vote"); - expect(votesCount(body, "wings")).toBe("0 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe("0 votes"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("1 vote"); + expect(votesCount(renderResult, "wings")).toBe("0 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); }); it("allows re-voting after un-voting", () => { @@ -371,12 +385,12 @@ describe("MPollBody", () => { responseEvent("@op:example.com", "italian", 14), responseEvent("@me:example.com", "italian"), ]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe("0 votes"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("2 votes"); - expect(votesCount(body, "wings")).toBe("0 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe("0 votes"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("2 votes"); + expect(votesCount(renderResult, "wings")).toBe("0 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); }); it("treats any invalid answer as a spoiled ballot", () => { @@ -389,12 +403,12 @@ describe("MPollBody", () => { responseEvent("@uy:example.com", "italian", 14), responseEvent("@uy:example.com", "doesntexist", 15), ]; - const body = newMPollBody(votes); - expect(votesCount(body, "pizza")).toBe("0 votes"); - expect(votesCount(body, "poutine")).toBe("0 votes"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("0 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 0 votes"); + const renderResult = newMPollBody(votes); + expect(votesCount(renderResult, "pizza")).toBe("0 votes"); + expect(votesCount(renderResult, "poutine")).toBe("0 votes"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("0 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 0 votes"); }); it("allows re-voting after a spoiled ballot", () => { @@ -405,31 +419,31 @@ describe("MPollBody", () => { responseEvent("@uy:example.com", "doesntexist", 15), responseEvent("@uy:example.com", "poutine", 16), ]; - const body = newMPollBody(votes); - expect(body.find('input[type="radio"]')).toHaveLength(4); - expect(votesCount(body, "pizza")).toBe("0 votes"); - expect(votesCount(body, "poutine")).toBe("1 vote"); - expect(votesCount(body, "italian")).toBe("0 votes"); - expect(votesCount(body, "wings")).toBe("0 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote"); + const renderResult = newMPollBody(votes); + expect(renderResult.container.querySelectorAll('input[type="radio"]')).toHaveLength(4); + expect(votesCount(renderResult, "pizza")).toBe("0 votes"); + expect(votesCount(renderResult, "poutine")).toBe("1 vote"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("0 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); }); it("renders nothing if poll has no answers", () => { - const answers = []; - const votes = []; - const ends = []; - const body = newMPollBody(votes, ends, answers); - expect(body.html()).toBeNull(); + const answers: PollAnswer[] = []; + const votes: MatrixEvent[] = []; + const ends: MatrixEvent[] = []; + const { container } = newMPollBody(votes, ends, answers); + expect(container.childElementCount).toEqual(0); }); it("renders the first 20 answers if 21 were given", () => { const answers = Array.from(Array(21).keys()).map((i) => { return { id: `id${i}`, [M_TEXT.name]: `Name ${i}` }; }); - const votes = []; - const ends = []; - const body = newMPollBody(votes, ends, answers); - expect(body.find(".mx_MPollBody_option").length).toBe(20); + const votes: MatrixEvent[] = []; + const ends: MatrixEvent[] = []; + const { container } = newMPollBody(votes, ends, answers); + expect(container.querySelectorAll(".mx_MPollBody_option").length).toBe(20); }); it("hides scores if I voted but the poll is undisclosed", () => { @@ -440,12 +454,12 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const body = newMPollBody(votes, [], undefined, false); - expect(votesCount(body, "pizza")).toBe(""); - expect(votesCount(body, "poutine")).toBe(""); - expect(votesCount(body, "italian")).toBe(""); - expect(votesCount(body, "wings")).toBe(""); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Results will be visible when the poll is ended"); + const renderResult = newMPollBody(votes, [], undefined, false); + expect(votesCount(renderResult, "pizza")).toBe(""); + expect(votesCount(renderResult, "poutine")).toBe(""); + expect(votesCount(renderResult, "italian")).toBe(""); + expect(votesCount(renderResult, "wings")).toBe(""); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Results will be visible when the poll is ended"); }); it("highlights my vote if the poll is undisclosed", () => { @@ -456,13 +470,13 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const body = newMPollBody(votes, [], undefined, false); + const { container } = newMPollBody(votes, [], undefined, false); // My vote is marked - expect(body.find('input[value="pizza"]').prop("checked")).toBeTruthy(); + expect(container.querySelector('input[value="pizza"]')!).toBeChecked(); // Sanity: other items are not checked - expect(body.find('input[value="poutine"]').prop("checked")).toBeFalsy(); + expect(container.querySelector('input[value="poutine"]')!).not.toBeChecked(); }); it("shows scores if the poll is undisclosed but ended", () => { @@ -474,47 +488,47 @@ describe("MPollBody", () => { responseEvent("@dune2:example.com", "wings"), ]; const ends = [endEvent("@me:example.com", 12)]; - const body = newMPollBody(votes, ends, undefined, false); - expect(endedVotesCount(body, "pizza")).toBe("3 votes"); - expect(endedVotesCount(body, "poutine")).toBe("1 vote"); - expect(endedVotesCount(body, "italian")).toBe("0 votes"); - expect(endedVotesCount(body, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); + const renderResult = newMPollBody(votes, ends, undefined, false); + expect(endedVotesCount(renderResult, "pizza")).toBe("3 votes"); + expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); + expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "wings")).toBe("1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); it("sends a vote event when I choose an option", () => { - const votes = []; - const body = newMPollBody(votes); - clickRadio(body, "wings"); + const votes: MatrixEvent[] = []; + const renderResult = newMPollBody(votes); + clickOption(renderResult, "wings"); expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings")); }); it("sends only one vote event when I click several times", () => { - const votes = []; - const body = newMPollBody(votes); - clickRadio(body, "wings"); - clickRadio(body, "wings"); - clickRadio(body, "wings"); - clickRadio(body, "wings"); + const votes: MatrixEvent[] = []; + const renderResult = newMPollBody(votes); + clickOption(renderResult, "wings"); + clickOption(renderResult, "wings"); + clickOption(renderResult, "wings"); + clickOption(renderResult, "wings"); expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings")); }); it("sends no vote event when I click what I already chose", () => { const votes = [responseEvent("@me:example.com", "wings")]; - const body = newMPollBody(votes); - clickRadio(body, "wings"); - clickRadio(body, "wings"); - clickRadio(body, "wings"); - clickRadio(body, "wings"); + const renderResult = newMPollBody(votes); + clickOption(renderResult, "wings"); + clickOption(renderResult, "wings"); + clickOption(renderResult, "wings"); + clickOption(renderResult, "wings"); expect(mockClient.sendEvent).not.toHaveBeenCalled(); }); it("sends several events when I click different options", () => { - const votes = []; - const body = newMPollBody(votes); - clickRadio(body, "wings"); - clickRadio(body, "italian"); - clickRadio(body, "poutine"); + const votes: MatrixEvent[] = []; + const renderResult = newMPollBody(votes); + clickOption(renderResult, "wings"); + clickOption(renderResult, "italian"); + clickOption(renderResult, "poutine"); expect(mockClient.sendEvent).toHaveBeenCalledTimes(3); expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings")); expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("italian")); @@ -524,10 +538,10 @@ describe("MPollBody", () => { it("sends no events when I click in an ended poll", () => { const ends = [endEvent("@me:example.com", 25)]; const votes = [responseEvent("@uy:example.com", "wings", 15), responseEvent("@uy:example.com", "poutine", 15)]; - const body = newMPollBody(votes, ends); - clickEndedOption(body, "wings"); - clickEndedOption(body, "italian"); - clickEndedOption(body, "poutine"); + const renderResult = newMPollBody(votes, ends); + clickOption(renderResult, "wings"); + clickOption(renderResult, "italian"); + clickOption(renderResult, "poutine"); expect(mockClient.sendEvent).not.toHaveBeenCalled(); }); @@ -577,9 +591,9 @@ describe("MPollBody", () => { it("shows non-radio buttons if the poll is ended", () => { const events = [endEvent()]; - const body = newMPollBody([], events); - expect(body.find(".mx_StyledRadioButton")).toHaveLength(0); - expect(body.find('input[type="radio"]')).toHaveLength(0); + const { container } = newMPollBody([], events); + expect(container.querySelector(".mx_StyledRadioButton")).not.toBeInTheDocument(); + expect(container.querySelector('input[type="radio"]')).not.toBeInTheDocument(); }); it("counts votes as normal if the poll is ended", () => { @@ -591,23 +605,23 @@ describe("MPollBody", () => { responseEvent("@qbert:example.com", "wings", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); - expect(endedVotesCount(body, "pizza")).toBe("0 votes"); - expect(endedVotesCount(body, "poutine")).toBe("1 vote"); - expect(endedVotesCount(body, "italian")).toBe("0 votes"); - expect(endedVotesCount(body, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 2 votes"); + const renderResult = newMPollBody(votes, ends); + expect(endedVotesCount(renderResult, "pizza")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); + expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "wings")).toBe("1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 2 votes"); }); it("counts a single vote as normal if the poll is ended", () => { const votes = [responseEvent("@qbert:example.com", "poutine", 16)]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); - expect(endedVotesCount(body, "pizza")).toBe("0 votes"); - expect(endedVotesCount(body, "poutine")).toBe("1 vote"); - expect(endedVotesCount(body, "italian")).toBe("0 votes"); - expect(endedVotesCount(body, "wings")).toBe("0 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 1 vote"); + const renderResult = newMPollBody(votes, ends); + expect(endedVotesCount(renderResult, "pizza")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); + expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "wings")).toBe("0 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 1 vote"); }); it("shows ended vote counts of different numbers", () => { @@ -619,15 +633,15 @@ describe("MPollBody", () => { responseEvent("@hi:example.com", "pizza", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); + const renderResult = newMPollBody(votes, ends); - expect(body.find(".mx_StyledRadioButton")).toHaveLength(0); - expect(body.find('input[type="radio"]')).toHaveLength(0); - expect(endedVotesCount(body, "pizza")).toBe("2 votes"); - expect(endedVotesCount(body, "poutine")).toBe("0 votes"); - expect(endedVotesCount(body, "italian")).toBe("0 votes"); - expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); + expect(renderResult.container.querySelectorAll(".mx_StyledRadioButton")).toHaveLength(0); + expect(renderResult.container.querySelectorAll('input[type="radio"]')).toHaveLength(0); + expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); + expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "wings")).toBe("3 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); it("ignores votes that arrived after poll ended", () => { @@ -641,13 +655,13 @@ describe("MPollBody", () => { responseEvent("@ld:example.com", "pizza", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); + const renderResult = newMPollBody(votes, ends); - expect(endedVotesCount(body, "pizza")).toBe("2 votes"); - expect(endedVotesCount(body, "poutine")).toBe("0 votes"); - expect(endedVotesCount(body, "italian")).toBe("0 votes"); - expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); + expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); + expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "wings")).toBe("3 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); it("counts votes that arrived after an unauthorised poll end event", () => { @@ -664,13 +678,13 @@ describe("MPollBody", () => { endEvent("@unauthorised:example.com", 5), // Should be ignored endEvent("@me:example.com", 25), ]; - const body = newMPollBody(votes, ends); + const renderResult = newMPollBody(votes, ends); - expect(endedVotesCount(body, "pizza")).toBe("2 votes"); - expect(endedVotesCount(body, "poutine")).toBe("0 votes"); - expect(endedVotesCount(body, "italian")).toBe("0 votes"); - expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); + expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); + expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "wings")).toBe("3 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); it("ignores votes that arrived after the first end poll event", () => { @@ -691,13 +705,13 @@ describe("MPollBody", () => { endEvent("@me:example.com", 25), endEvent("@me:example.com", 75), ]; - const body = newMPollBody(votes, ends); + const renderResult = newMPollBody(votes, ends); - expect(endedVotesCount(body, "pizza")).toBe("2 votes"); - expect(endedVotesCount(body, "poutine")).toBe("0 votes"); - expect(endedVotesCount(body, "italian")).toBe("0 votes"); - expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); + expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); + expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); + expect(endedVotesCount(renderResult, "wings")).toBe("3 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); it("highlights the winning vote in an ended poll", () => { @@ -708,15 +722,15 @@ describe("MPollBody", () => { responseEvent("@xy:example.com", "wings", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); + const renderResult = newMPollBody(votes, ends); // Then the winner is highlighted - expect(endedVoteChecked(body, "wings")).toBe(true); - expect(endedVoteChecked(body, "pizza")).toBe(false); + expect(endedVoteChecked(renderResult, "wings")).toBe(true); + expect(endedVoteChecked(renderResult, "pizza")).toBe(false); // Double-check by looking for the endedOptionWinner class - expect(endedVoteDiv(body, "wings").hasClass("mx_MPollBody_endedOptionWinner")).toBe(true); - expect(endedVoteDiv(body, "pizza").hasClass("mx_MPollBody_endedOptionWinner")).toBe(false); + expect(endedVoteDiv(renderResult, "wings").className.includes("mx_MPollBody_endedOptionWinner")).toBe(true); + expect(endedVoteDiv(renderResult, "pizza").className.includes("mx_MPollBody_endedOptionWinner")).toBe(false); }); it("highlights multiple winning votes", () => { @@ -726,23 +740,23 @@ describe("MPollBody", () => { responseEvent("@fg:example.com", "poutine", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); + const renderResult = newMPollBody(votes, ends); - expect(endedVoteChecked(body, "pizza")).toBe(true); - expect(endedVoteChecked(body, "wings")).toBe(true); - expect(endedVoteChecked(body, "poutine")).toBe(true); - expect(endedVoteChecked(body, "italian")).toBe(false); - expect(body.find(".mx_MPollBody_option_checked")).toHaveLength(3); + expect(endedVoteChecked(renderResult, "pizza")).toBe(true); + expect(endedVoteChecked(renderResult, "wings")).toBe(true); + expect(endedVoteChecked(renderResult, "poutine")).toBe(true); + expect(endedVoteChecked(renderResult, "italian")).toBe(false); + expect(renderResult.container.getElementsByClassName("mx_MPollBody_option_checked")).toHaveLength(3); }); it("highlights nothing if poll has no votes", () => { const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody([], ends); - expect(body.find(".mx_MPollBody_option_checked")).toHaveLength(0); + const renderResult = newMPollBody([], ends); + expect(renderResult.container.getElementsByClassName("mx_MPollBody_option_checked")).toHaveLength(0); }); it("says poll is not ended if there is no end event", () => { - const ends = []; + const ends: MatrixEvent[] = []; expect(runIsPollEnded(ends)).toBe(false); }); @@ -810,26 +824,26 @@ describe("MPollBody", () => { }, }); pollEvent.makeReplaced(replacingEvent); - const body = newMPollBodyFromEvent(pollEvent, []); - expect(body.find("h2").html()).toEqual( - "

new question" + ' (edited)' + "

", + const { getByTestId, container } = newMPollBodyFromEvent(pollEvent, []); + expect(getByTestId("pollQuestion").innerHTML).toEqual( + 'new question (edited)', ); - const inputs = body.find('input[type="radio"]'); + const inputs = container.querySelectorAll('input[type="radio"]'); expect(inputs).toHaveLength(3); - expect(inputs.at(0).prop("value")).toEqual("n1"); - expect(inputs.at(1).prop("value")).toEqual("n2"); - expect(inputs.at(2).prop("value")).toEqual("n3"); - const options = body.find(".mx_MPollBody_optionText"); + expect(inputs[0].getAttribute("value")).toEqual("n1"); + expect(inputs[1].getAttribute("value")).toEqual("n2"); + expect(inputs[2].getAttribute("value")).toEqual("n3"); + const options = container.querySelectorAll(".mx_MPollBody_optionText"); expect(options).toHaveLength(3); - expect(options.at(0).text()).toEqual("new answer 1"); - expect(options.at(1).text()).toEqual("new answer 2"); - expect(options.at(2).text()).toEqual("new answer 3"); + expect(options[0].innerHTML).toEqual("new answer 1"); + expect(options[1].innerHTML).toEqual("new answer 2"); + expect(options[2].innerHTML).toEqual("new answer 3"); }); it("renders a poll with no votes", () => { - const votes = []; - const body = newMPollBody(votes); - expect(body).toMatchSnapshot(); + const votes: MatrixEvent[] = []; + const { container } = newMPollBody(votes); + expect(container).toMatchSnapshot(); }); it("renders a poll with only non-local votes", () => { @@ -840,8 +854,8 @@ describe("MPollBody", () => { responseEvent("@me:example.com", "wings", 15), responseEvent("@qr:example.com", "italian", 16), ]; - const body = newMPollBody(votes); - expect(body).toMatchSnapshot(); + const { container } = newMPollBody(votes); + expect(container).toMatchSnapshot(); }); it("renders a poll with local, non-local and invalid votes", () => { @@ -853,9 +867,9 @@ describe("MPollBody", () => { responseEvent("@e:example.com", "wings", 15), responseEvent("@me:example.com", "italian", 16), ]; - const body = newMPollBody(votes); - clickRadio(body, "italian"); - expect(body).toMatchSnapshot(); + const renderResult = newMPollBody(votes); + clickOption(renderResult, "italian"); + expect(renderResult.container).toMatchSnapshot(); }); it("renders a poll that I have not voted in", () => { @@ -866,14 +880,14 @@ describe("MPollBody", () => { responseEvent("@yo:example.com", "wings", 15), responseEvent("@qr:example.com", "italian", 16), ]; - const body = newMPollBody(votes); - expect(body).toMatchSnapshot(); + const { container } = newMPollBody(votes); + expect(container).toMatchSnapshot(); }); it("renders a finished poll with no votes", () => { const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody([], ends); - expect(body).toMatchSnapshot(); + const { container } = newMPollBody([], ends); + expect(container).toMatchSnapshot(); }); it("renders a finished poll", () => { @@ -885,8 +899,8 @@ describe("MPollBody", () => { responseEvent("@qr:example.com", "italian", 16), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); - expect(body).toMatchSnapshot(); + const { container } = newMPollBody(votes, ends); + expect(container).toMatchSnapshot(); }); it("renders a finished poll with multiple winners", () => { @@ -899,8 +913,8 @@ describe("MPollBody", () => { responseEvent("@yh:example.com", "poutine", 14), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends); - expect(body).toMatchSnapshot(); + const { container } = newMPollBody(votes, ends); + expect(container).toMatchSnapshot(); }); it("renders an undisclosed, unfinished poll", () => { @@ -912,9 +926,9 @@ describe("MPollBody", () => { responseEvent("@th:example.com", "poutine", 13), responseEvent("@yh:example.com", "poutine", 14), ]; - const ends = []; - const body = newMPollBody(votes, ends, undefined, false); - expect(body.html()).toMatchSnapshot(); + const ends: MatrixEvent[] = []; + const { container } = newMPollBody(votes, ends, undefined, false); + expect(container).toMatchSnapshot(); }); it("renders an undisclosed, finished poll", () => { @@ -927,8 +941,8 @@ describe("MPollBody", () => { responseEvent("@yh:example.com", "poutine", 14), ]; const ends = [endEvent("@me:example.com", 25)]; - const body = newMPollBody(votes, ends, undefined, false); - expect(body.html()).toMatchSnapshot(); + const { container } = newMPollBody(votes, ends, undefined, false); + expect(container).toMatchSnapshot(); }); }); @@ -941,7 +955,7 @@ function newEndRelations(relationEvents: Array): Relations { } function newRelations(relationEvents: Array, eventType: string): Relations { - const voteRelations = new Relations("m.reference", eventType, null); + const voteRelations = new Relations("m.reference", eventType, mockClient); for (const ev of relationEvents) { voteRelations.addEvent(ev); } @@ -953,84 +967,88 @@ function newMPollBody( endEvents: Array = [], answers?: PollAnswer[], disclosed = true, -): ReactWrapper { +): RenderResult { const mxEvent = new MatrixEvent({ type: M_POLL_START.name, event_id: "$mypoll", room_id: "#myroom:example.com", - content: newPollStart(answers, null, disclosed), + content: newPollStart(answers, undefined, disclosed), }); return newMPollBodyFromEvent(mxEvent, relationEvents, endEvents); } +function getMPollBodyPropsFromEvent( + mxEvent: MatrixEvent, + relationEvents: Array, + endEvents: Array = [], +): IBodyProps { + const voteRelations = newVoteRelations(relationEvents); + const endRelations = newEndRelations(endEvents); + + const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { + expect(eventId).toBe("$mypoll"); + expect(relationType).toBe("m.reference"); + if (M_POLL_RESPONSE.matches(eventType)) { + return voteRelations; + } else if (M_POLL_END.matches(eventType)) { + return endRelations; + } else { + fail("Unexpected eventType: " + eventType); + } + }; + + return { + mxEvent, + getRelationsForEvent, + // We don't use any of these props, but they're required. + highlightLink: "unused", + highlights: [], + mediaEventHelper: {} as unknown as MediaEventHelper, + onHeightChanged: () => {}, + onMessageAllowed: () => {}, + permalinkCreator: {} as unknown as RoomPermalinkCreator, + }; +} + +function renderMPollBodyWithWrapper(props: IBodyProps): RenderResult { + return render(, { + wrapper: ({ children }) => ( + {children} + ), + }); +} + function newMPollBodyFromEvent( mxEvent: MatrixEvent, relationEvents: Array, endEvents: Array = [], -): ReactWrapper { - const voteRelations = newVoteRelations(relationEvents); - const endRelations = newEndRelations(endEvents); - return mount( - { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - if (M_POLL_RESPONSE.matches(eventType)) { - return voteRelations; - } else if (M_POLL_END.matches(eventType)) { - return endRelations; - } else { - fail("Unexpected eventType: " + eventType); - } - }} - // We don't use any of these props, but they're required. - highlightLink="unused" - highlights={[]} - mediaEventHelper={null} - onHeightChanged={() => {}} - onMessageAllowed={() => {}} - permalinkCreator={null} - />, - { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { - value: mockClient, - }, - }, - ); +): RenderResult { + const props = getMPollBodyPropsFromEvent(mxEvent, relationEvents, endEvents); + return renderMPollBodyWithWrapper(props); } -function clickRadio(wrapper: ReactWrapper, value: string) { - const div = wrapper.find(`StyledRadioButton[value="${value}"]`); - expect(div).toHaveLength(1); - div.simulate("click"); +function clickOption({ getByTestId }: RenderResult, value: string) { + fireEvent.click(getByTestId(`pollOption-${value}`)); } -function clickEndedOption(wrapper: ReactWrapper, value: string) { - const div = wrapper.find(`div[data-value="${value}"]`); - expect(div).toHaveLength(1); - div.simulate("click"); +function voteButton({ getByTestId }: RenderResult, value: string): Element { + return getByTestId(`pollOption-${value}`); } -function voteButton(wrapper: ReactWrapper, value: string): ReactWrapper { - return wrapper.find(`div.mx_MPollBody_option`).findWhere((w) => w.key() === value); +function votesCount({ getByTestId }: RenderResult, value: string): string { + return getByTestId(`pollOption-${value}`).querySelector(".mx_MPollBody_optionVoteCount")!.innerHTML; } -function votesCount(wrapper: ReactWrapper, value: string): string { - return wrapper.find(`StyledRadioButton[value="${value}"] .mx_MPollBody_optionVoteCount`).text(); +function endedVoteChecked({ getByTestId }: RenderResult, value: string): boolean { + return getByTestId(`pollOption-${value}`).className.includes("mx_MPollBody_option_checked"); } -function endedVoteChecked(wrapper: ReactWrapper, value: string): boolean { - return endedVoteDiv(wrapper, value).closest(".mx_MPollBody_option").hasClass("mx_MPollBody_option_checked"); +function endedVoteDiv({ getByTestId }: RenderResult, value: string): Element { + return getByTestId(`pollOption-${value}`).firstElementChild!; } -function endedVoteDiv(wrapper: ReactWrapper, value: string): ReactWrapper { - return wrapper.find(`div[data-value="${value}"]`); -} - -function endedVotesCount(wrapper: ReactWrapper, value: string): string { - return wrapper.find(`div[data-value="${value}"] .mx_MPollBody_optionVoteCount`).text(); +function endedVotesCount(renderResult: RenderResult, value: string): string { + return votesCount(renderResult, value); } function newPollStart(answers?: PollAnswer[], question?: string, disclosed = true): PollStartEventContent { diff --git a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap index e4039b82b6..2263527148 100644 --- a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap @@ -1,2626 +1,1627 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MPollBody renders a finished poll 1`] = ` - +
-

+

What should we order for the party?

-
+ Pizza +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Poutine +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Italian +
+
+ 2 votes +
+
+
+
+
+
+
+
+
+
+
+ Wings +
+
+ 1 vote +
+
+
+
+
+
+
+
+
+ Final result based on 3 votes +
+
+
+`; + +exports[`MPollBody renders a finished poll with multiple winners 1`] = ` +
+
+

+ What should we order for the party? +

+
+
+
+
+
+ Pizza +
+
+ 2 votes +
+
+
+
+
+
+
+
+
+
+
+ Poutine +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Italian +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Wings +
+
+ 2 votes +
+
+
+
+
+
+
+
+
+ Final result based on 4 votes +
+
+
+`; + +exports[`MPollBody renders a finished poll with no votes 1`] = ` +
+
+

+ What should we order for the party? +

+
+
+
+
+
+ Pizza +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Poutine +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Italian +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Wings +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+ Final result based on 0 votes +
+
+
+`; + +exports[`MPollBody renders a poll that I have not voted in 1`] = ` +
+
+

+ What should we order for the party? +

+
+
+
+
+
+
+
Pizza
- 0 votes -
+ class="mx_MPollBody_optionVoteCount" + />
- +
+
- + +
+
+
Poutine
- 0 votes -
+ class="mx_MPollBody_optionVoteCount" + />
- +
+
- + +
+
+
Italian
- 2 votes -
+ class="mx_MPollBody_optionVoteCount" + />
- +
+
- + +
+
+
Wings
+
+
+
+ +
+
+
+
+
+
+ 3 votes cast. Vote to see the results +
+
+
+`; + +exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = ` +
+
+

+ What should we order for the party? +

+
+
+
-

- What should we order for the party? -

-
- -
-
-
- Pizza -
-
- 2 votes -
-
+ +
+
- -
-
-
-
- -
Poutine
0 votes
-
+
+
- + +
+
+
Italian
- 0 votes + 3 votes
- +
+
- + +
+
+
Wings
- 2 votes + 1 vote
- +
+
- Final result based on 4 votes -
-
- -`; - -exports[`MPollBody renders a finished poll with no votes 1`] = ` - -
-

- What should we order for the party? -

-
-
- -
-
-
- Pizza -
-
- 0 votes -
-
-
-
-
-
-
-
-
- -
-
-
- Poutine -
-
- 0 votes -
-
-
-
-
-
-
-
-
- -
-
-
- Italian -
-
- 0 votes -
-
-
-
-
-
-
-
-
- -
-
-
- Wings -
-
- 0 votes -
-
-
-
-
-
-
-
-
-
- Final result based on 0 votes -
-
- -`; - -exports[`MPollBody renders a poll that I have not voted in 1`] = ` - -
-

- What should we order for the party? -

-
-
- - -
-
-
-
-
-
- Pizza -
-
-
-
-
- - - -
-
-
-
-
- - -
-
-
-
-
-
- Poutine -
-
-
-
-
- - - -
-
-
-
-
- - -
-
-
-
-
-
- Italian -
-
-
-
-
- - - -
-
-
-
-
- - -
-
-
-
-
-
- Wings -
-
-
-
-
- - - -
-
-
-
-
-
- 3 votes cast. Vote to see the results -
-
- -`; - -exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = ` - -
-

- What should we order for the party? -

-
-
- - -
-
-
-
-
-
- Pizza -
-
- 1 vote -
-
-
-
- - - -
-
-
-
-
- - -
-
-
-
-
-
- Poutine -
-
- 0 votes -
-
-
-
- - - -
-
-
-
-
- - -
-
-
-
-
-
- Italian -
-
- 3 votes -
-
-
-
- - - -
-
-
-
-
- - -
`; exports[`MPollBody renders a poll with no votes 1`] = ` - +
-

+

What should we order for the party?

- - +
+
+
+
-
-
-
-
-
- Pizza -
-
-
+ Pizza
- - - +
+
+
+
- - +
+
+
+
-
-
-
-
-
- Poutine -
-
-
+ Poutine
- - - +
+
+
+
- - +
+
+
+
-
-
-
-
-
- Italian -
-
-
+ Italian
- - - +
+
+
+
- - +
+
+
+
-
`; exports[`MPollBody renders a poll with only non-local votes 1`] = ` - +
-

+

What should we order for the party?

- - +
+
+
+
-
+
+
- - +
+
+
+
-
+
+
- - +
+
+
+
-
+
+
- - +
+
+
+
-
+
+
Based on 3 votes
- +
`; -exports[`MPollBody renders an undisclosed, finished poll 1`] = `"

What should we order for the party?

Pizza
2 votes
Poutine
0 votes
Italian
0 votes
Wings
2 votes
Final result based on 4 votes
"`; +exports[`MPollBody renders an undisclosed, finished poll 1`] = ` +
+
+

+ What should we order for the party? +

+
+
+
+
+
+ Pizza +
+
+ 2 votes +
+
+
+
+
+
+
+
+
+
+
+ Poutine +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Italian +
+
+ 0 votes +
+
+
+
+
+
+
+
+
+
+
+ Wings +
+
+ 2 votes +
+
+
+
+
+
+
+
+
+ Final result based on 4 votes +
+
+
+`; -exports[`MPollBody renders an undisclosed, unfinished poll 1`] = `"

What should we order for the party?

Results will be visible when the poll is ended
"`; +exports[`MPollBody renders an undisclosed, unfinished poll 1`] = ` +
+
+

+ What should we order for the party? +

+
+
+
+
+
+
+
+
+ Pizza +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Poutine +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Italian +
+
+
+
+
+ +
+
+
+
+
+
- -
+ const controls = + recordingState === "connection_error" ? ( + + ) : (
{toggleControl}
+ ); + + return ( +
+ +
+ {controls} {showDeviceSelect && ( void; + [VoiceBroadcastRecordingEvent.StateChanged]: (state: VoiceBroadcastRecordingState) => void; [VoiceBroadcastRecordingEvent.TimeLeftChanged]: (timeLeft: number) => void; } @@ -58,13 +63,17 @@ export class VoiceBroadcastRecording extends TypedEventEmitter implements IDestroyable { - private state: VoiceBroadcastInfoState; - private recorder: VoiceBroadcastRecorder; + private state: VoiceBroadcastRecordingState; + private recorder: VoiceBroadcastRecorder | null = null; private dispatcherRef: string; private chunkEvents = new VoiceBroadcastChunkEvents(); private chunkRelationHelper: RelationsHelper; private maxLength: number; private timeLeft: number; + private toRetry: Array<() => Promise> = []; + private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; + private roomId: string; + private infoEventId: string; /** * Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing. @@ -82,11 +91,13 @@ export class VoiceBroadcastRecording super(); this.maxLength = getMaxBroadcastLength(); this.timeLeft = this.maxLength; + this.infoEventId = this.determineEventIdFromInfoEvent(); + this.roomId = this.determineRoomIdFromInfoEvent(); if (initialState) { this.state = initialState; } else { - this.setInitialStateFromInfoEvent(); + this.state = this.determineInitialStateFromInfoEvent(); } // TODO Michael W: listen for state updates @@ -94,6 +105,8 @@ export class VoiceBroadcastRecording this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.dispatcherRef = dis.register(this.onAction); this.chunkRelationHelper = this.initialiseChunkEventRelation(); + this.reconnectedListener = createReconnectedListener(this.onReconnect); + this.client.on(ClientEvent.Sync, this.reconnectedListener); } private initialiseChunkEventRelation(): RelationsHelper { @@ -125,17 +138,37 @@ export class VoiceBroadcastRecording this.chunkEvents.addEvent(event); }; - private setInitialStateFromInfoEvent(): void { - const room = this.client.getRoom(this.infoEvent.getRoomId()); + private determineEventIdFromInfoEvent(): string { + const infoEventId = this.infoEvent.getId(); + + if (!infoEventId) { + throw new Error("Cannot create broadcast for info event without Id."); + } + + return infoEventId; + } + + private determineRoomIdFromInfoEvent(): string { + const roomId = this.infoEvent.getRoomId(); + + if (!roomId) { + throw new Error(`Cannot create broadcast for unknown room (info event ${this.infoEventId})`); + } + + return roomId; + } + + /** + * Determines the initial broadcast state. + * Checks all related events. If one has the "stopped" state → stopped, else started. + */ + private determineInitialStateFromInfoEvent(): VoiceBroadcastRecordingState { + const room = this.client.getRoom(this.roomId); const relations = room ?.getUnfilteredTimelineSet() - ?.relations?.getChildEventsForEvent( - this.infoEvent.getId(), - RelationType.Reference, - VoiceBroadcastInfoEventType, - ); + ?.relations?.getChildEventsForEvent(this.infoEventId, RelationType.Reference, VoiceBroadcastInfoEventType); const relatedEvents = relations?.getRelations(); - this.state = !relatedEvents?.find((event: MatrixEvent) => { + return !relatedEvents?.find((event: MatrixEvent) => { return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; }) ? VoiceBroadcastInfoState.Started @@ -146,6 +179,35 @@ export class VoiceBroadcastRecording return this.timeLeft; } + /** + * Retries failed actions on reconnect. + */ + private onReconnect = async (): Promise => { + // Do nothing if not in connection_error state. + if (this.state !== "connection_error") return; + + // Copy the array, so that it is possible to remove elements from it while iterating over the original. + const toRetryCopy = [...this.toRetry]; + + for (const retryFn of this.toRetry) { + try { + await retryFn(); + // Successfully retried. Remove from array copy. + toRetryCopy.splice(toRetryCopy.indexOf(retryFn), 1); + } catch { + // The current retry callback failed. Stop the loop. + break; + } + } + + this.toRetry = toRetryCopy; + + if (this.toRetry.length === 0) { + // Everything has been successfully retried. Recover from error state to paused. + await this.pause(); + } + }; + private async setTimeLeft(timeLeft: number): Promise { if (timeLeft <= 0) { // time is up - stop the recording @@ -173,7 +235,12 @@ export class VoiceBroadcastRecording public async pause(): Promise { // stopped or already paused recordings cannot be paused - if ([VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused].includes(this.state)) return; + if ( + ( + [VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused] as VoiceBroadcastRecordingState[] + ).includes(this.state) + ) + return; this.setState(VoiceBroadcastInfoState.Paused); await this.stopRecorder(); @@ -191,12 +258,16 @@ export class VoiceBroadcastRecording public toggle = async (): Promise => { if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume(); - if ([VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed].includes(this.getState())) { + if ( + ( + [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[] + ).includes(this.getState()) + ) { return this.pause(); } }; - public getState(): VoiceBroadcastInfoState { + public getState(): VoiceBroadcastRecordingState { return this.state; } @@ -221,6 +292,7 @@ export class VoiceBroadcastRecording dis.unregister(this.dispatcherRef); this.chunkEvents = new VoiceBroadcastChunkEvents(); this.chunkRelationHelper.destroy(); + this.client.off(ClientEvent.Sync, this.reconnectedListener); } private onBeforeRedaction = (): void => { @@ -238,7 +310,7 @@ export class VoiceBroadcastRecording this.pause(); }; - private setState(state: VoiceBroadcastInfoState): void { + private setState(state: VoiceBroadcastRecordingState): void { this.state = state; this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state); } @@ -248,56 +320,102 @@ export class VoiceBroadcastRecording }; private onChunkRecorded = async (chunk: ChunkRecordedPayload): Promise => { - const { url, file } = await this.uploadFile(chunk); - await this.sendVoiceMessage(chunk, url, file); + const uploadAndSendFn = async (): Promise => { + const { url, file } = await this.uploadFile(chunk); + await this.sendVoiceMessage(chunk, url, file); + }; + + await this.callWithRetry(uploadAndSendFn); }; - private uploadFile(chunk: ChunkRecordedPayload): ReturnType { + /** + * This function is called on connection errors. + * It sets the connection error state and stops the recorder. + */ + private async onConnectionError(): Promise { + await this.stopRecorder(); + this.setState("connection_error"); + } + + private async uploadFile(chunk: ChunkRecordedPayload): ReturnType { return uploadFile( this.client, - this.infoEvent.getRoomId(), + this.roomId, new Blob([chunk.buffer], { type: this.getRecorder().contentType, }), ); } - private async sendVoiceMessage(chunk: ChunkRecordedPayload, url: string, file: IEncryptedFile): Promise { - const content = createVoiceMessageContent( - url, - this.getRecorder().contentType, - Math.round(chunk.length * 1000), - chunk.buffer.length, - file, - ); - content["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: this.infoEvent.getId(), - }; - content["io.element.voice_broadcast_chunk"] = { - /** Increment the last sequence number and use it for this message. Also see {@link sequence}. */ - sequence: ++this.sequence, + private async sendVoiceMessage(chunk: ChunkRecordedPayload, url?: string, file?: IEncryptedFile): Promise { + /** + * Increment the last sequence number and use it for this message. + * Done outside of the sendMessageFn to get a scoped value. + * Also see {@link VoiceBroadcastRecording.sequence}. + */ + const sequence = ++this.sequence; + + const sendMessageFn = async (): Promise => { + const content = createVoiceMessageContent( + url, + this.getRecorder().contentType, + Math.round(chunk.length * 1000), + chunk.buffer.length, + file, + ); + content["m.relates_to"] = { + rel_type: RelationType.Reference, + event_id: this.infoEventId, + }; + content["io.element.voice_broadcast_chunk"] = { + sequence, + }; + + await this.client.sendMessage(this.roomId, content); }; - await this.client.sendMessage(this.infoEvent.getRoomId(), content); + await this.callWithRetry(sendMessageFn); } + /** + * Sends an info state event with given state. + * On error stores a resend function and setState(state) in {@link toRetry} and + * sets the broadcast state to connection_error. + */ private async sendInfoStateEvent(state: VoiceBroadcastInfoState): Promise { - // TODO Michael W: add error handling for state event - await this.client.sendStateEvent( - this.infoEvent.getRoomId(), - VoiceBroadcastInfoEventType, - { - device_id: this.client.getDeviceId(), - state, - last_chunk_sequence: this.sequence, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: this.infoEvent.getId(), - }, - } as VoiceBroadcastInfoEventContent, - this.client.getUserId(), - ); + const sendEventFn = async (): Promise => { + await this.client.sendStateEvent( + this.roomId, + VoiceBroadcastInfoEventType, + { + device_id: this.client.getDeviceId(), + state, + last_chunk_sequence: this.sequence, + ["m.relates_to"]: { + rel_type: RelationType.Reference, + event_id: this.infoEventId, + }, + } as VoiceBroadcastInfoEventContent, + this.client.getSafeUserId(), + ); + }; + + await this.callWithRetry(sendEventFn); + } + + /** + * Calls the function. + * On failure adds it to the retry list and triggers connection error. + * {@link toRetry} + * {@link onConnectionError} + */ + private async callWithRetry(retryAbleFn: () => Promise): Promise { + try { + await retryAbleFn(); + } catch { + this.toRetry.push(retryAbleFn); + this.onConnectionError(); + } } private async stopRecorder(): Promise { diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx index 662d7b2619..dddaa85827 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx @@ -18,9 +18,10 @@ limitations under the License. import React from "react"; import { act, render, RenderResult, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; import { mocked } from "jest-mock"; +import { SyncState } from "matrix-js-sdk/src/sync"; import { VoiceBroadcastInfoState, @@ -182,6 +183,30 @@ describe("VoiceBroadcastRecordingPip", () => { }); }); }); + + describe("and there is no connection and clicking the pause button", () => { + beforeEach(async () => { + mocked(client.sendStateEvent).mockImplementation(() => { + throw new Error(); + }); + await userEvent.click(screen.getByLabelText("pause voice broadcast")); + }); + + it("should show a connection error info", () => { + expect(screen.getByText("Connection error - Recording paused")).toBeInTheDocument(); + }); + + describe("and the connection is back", () => { + beforeEach(() => { + mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" }); + client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); + }); + + it("should render a paused recording", () => { + expect(screen.getByLabelText("resume voice broadcast")).toBeInTheDocument(); + }); + }); + }); }); describe("when rendering a paused recording", () => { diff --git a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts index 58c5e7b0cd..3cf71a7a94 100644 --- a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts @@ -16,6 +16,7 @@ limitations under the License. import { mocked } from "jest-mock"; import { + ClientEvent, EventTimelineSet, EventType, MatrixClient, @@ -26,6 +27,7 @@ import { Room, } from "matrix-js-sdk/src/matrix"; import { Relations } from "matrix-js-sdk/src/models/relations"; +import { SyncState } from "matrix-js-sdk/src/sync"; import { uploadFile } from "../../../src/ContentMessages"; import { IEncryptedFile } from "../../../src/customisations/models/IMediaEventContent"; @@ -41,6 +43,7 @@ import { VoiceBroadcastRecorderEvent, VoiceBroadcastRecording, VoiceBroadcastRecordingEvent, + VoiceBroadcastRecordingState, } from "../../../src/voice-broadcast"; import { mkEvent, mkStubRoom, stubClient } from "../../test-utils"; import dis from "../../../src/dispatcher/dispatcher"; @@ -84,14 +87,14 @@ describe("VoiceBroadcastRecording", () => { let client: MatrixClient; let infoEvent: MatrixEvent; let voiceBroadcastRecording: VoiceBroadcastRecording; - let onStateChanged: (state: VoiceBroadcastInfoState) => void; + let onStateChanged: (state: VoiceBroadcastRecordingState) => void; let voiceBroadcastRecorder: VoiceBroadcastRecorder; const mkVoiceBroadcastInfoEvent = (content: VoiceBroadcastInfoEventContent) => { return mkEvent({ event: true, type: VoiceBroadcastInfoEventType, - user: client.getUserId(), + user: client.getSafeUserId(), room: roomId, content, }); @@ -105,12 +108,19 @@ describe("VoiceBroadcastRecording", () => { jest.spyOn(voiceBroadcastRecording, "removeAllListeners"); }; - const itShouldBeInState = (state: VoiceBroadcastInfoState) => { + const itShouldBeInState = (state: VoiceBroadcastRecordingState) => { it(`should be in state stopped ${state}`, () => { expect(voiceBroadcastRecording.getState()).toBe(state); }); }; + const emitFirsChunkRecorded = () => { + voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, { + buffer: new Uint8Array([1, 2, 3]), + length: 23, + }); + }; + const itShouldSendAnInfoEvent = (state: VoiceBroadcastInfoState, lastChunkSequence: number) => { it(`should send a ${state} info event`, () => { expect(client.sendStateEvent).toHaveBeenCalledWith( @@ -179,13 +189,22 @@ describe("VoiceBroadcastRecording", () => { }); }; + const setUpUploadFileMock = () => { + mocked(uploadFile).mockResolvedValue({ + url: uploadedUrl, + file: uploadedFile, + }); + }; + beforeEach(() => { client = stubClient(); room = mkStubRoom(roomId, "Test Room", client); - mocked(client.getRoom).mockImplementation((getRoomId: string) => { + mocked(client.getRoom).mockImplementation((getRoomId: string | undefined): Room | null => { if (getRoomId === roomId) { return room; } + + return null; }); onStateChanged = jest.fn(); voiceBroadcastRecorder = new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength()); @@ -194,14 +213,11 @@ describe("VoiceBroadcastRecording", () => { jest.spyOn(voiceBroadcastRecorder, "destroy"); mocked(createVoiceBroadcastRecorder).mockReturnValue(voiceBroadcastRecorder); - mocked(uploadFile).mockResolvedValue({ - url: uploadedUrl, - file: uploadedFile, - }); + setUpUploadFileMock(); mocked(createVoiceMessageContent).mockImplementation( ( - mxc: string, + mxc: string | undefined, mimetype: string, duration: number, size: number, @@ -238,13 +254,45 @@ describe("VoiceBroadcastRecording", () => { }); afterEach(() => { - voiceBroadcastRecording.off(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged); + voiceBroadcastRecording?.off(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged); + }); + + describe("when there is an info event without id", () => { + beforeEach(() => { + infoEvent = mkVoiceBroadcastInfoEvent({ + device_id: client.getDeviceId()!, + state: VoiceBroadcastInfoState.Started, + }); + jest.spyOn(infoEvent, "getId").mockReturnValue(undefined); + }); + + it("should raise an error when creating a broadcast", () => { + expect(() => { + setUpVoiceBroadcastRecording(); + }).toThrowError("Cannot create broadcast for info event without Id."); + }); + }); + + describe("when there is an info event without room", () => { + beforeEach(() => { + infoEvent = mkVoiceBroadcastInfoEvent({ + device_id: client.getDeviceId()!, + state: VoiceBroadcastInfoState.Started, + }); + jest.spyOn(infoEvent, "getRoomId").mockReturnValue(undefined); + }); + + it("should raise an error when creating a broadcast", () => { + expect(() => { + setUpVoiceBroadcastRecording(); + }).toThrowError(`Cannot create broadcast for unknown room (info event ${infoEvent.getId()})`); + }); }); describe("when created for a Voice Broadcast Info without relations", () => { beforeEach(() => { infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId(), + device_id: client.getDeviceId()!, state: VoiceBroadcastInfoState.Started, }); setUpVoiceBroadcastRecording(); @@ -278,7 +326,16 @@ describe("VoiceBroadcastRecording", () => { describe("and the info event is redacted", () => { beforeEach(() => { - infoEvent.emit(MatrixEventEvent.BeforeRedaction, null, null); + infoEvent.emit( + MatrixEventEvent.BeforeRedaction, + infoEvent, + mkEvent({ + event: true, + type: EventType.RoomRedaction, + user: client.getSafeUserId(), + content: {}, + }), + ); }); itShouldBeInState(VoiceBroadcastInfoState.Stopped); @@ -329,10 +386,7 @@ describe("VoiceBroadcastRecording", () => { describe("and a chunk has been recorded", () => { beforeEach(async () => { - voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, { - buffer: new Uint8Array([1, 2, 3]), - length: 23, - }); + emitFirsChunkRecorded(); }); itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); @@ -388,6 +442,34 @@ describe("VoiceBroadcastRecording", () => { }); }); + describe("and there is no connection", () => { + beforeEach(() => { + mocked(client.sendStateEvent).mockImplementation(() => { + throw new Error(); + }); + }); + + describe.each([ + ["pause", async () => voiceBroadcastRecording.pause()], + ["toggle", async () => voiceBroadcastRecording.toggle()], + ])("and calling %s", (_case: string, action: Function) => { + beforeEach(async () => { + await action(); + }); + + itShouldBeInState("connection_error"); + + describe("and the connection is back", () => { + beforeEach(() => { + mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" }); + client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); + }); + + itShouldBeInState(VoiceBroadcastInfoState.Paused); + }); + }); + }); + describe("and calling destroy", () => { beforeEach(() => { voiceBroadcastRecording.destroy(); @@ -399,6 +481,45 @@ describe("VoiceBroadcastRecording", () => { expect(mocked(voiceBroadcastRecording.removeAllListeners)).toHaveBeenCalled(); }); }); + + describe("and a chunk has been recorded and the upload fails", () => { + beforeEach(() => { + mocked(uploadFile).mockRejectedValue("Error"); + emitFirsChunkRecorded(); + }); + + itShouldBeInState("connection_error"); + + describe("and the connection is back", () => { + beforeEach(() => { + setUpUploadFileMock(); + client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); + }); + + itShouldBeInState(VoiceBroadcastInfoState.Paused); + itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); + }); + }); + + describe("and a chunk has been recorded and sending the voice message fails", () => { + beforeEach(() => { + mocked(client.sendMessage).mockRejectedValue("Error"); + emitFirsChunkRecorded(); + }); + + itShouldBeInState("connection_error"); + + describe("and the connection is back", () => { + beforeEach(() => { + mocked(client.sendMessage).mockClear(); + mocked(client.sendMessage).mockResolvedValue({ event_id: "e23" }); + client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); + }); + + itShouldBeInState(VoiceBroadcastInfoState.Paused); + itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); + }); + }); }); describe("and it is in paused state", () => { @@ -431,7 +552,7 @@ describe("VoiceBroadcastRecording", () => { describe("when created for a Voice Broadcast Info with a Stopped relation", () => { beforeEach(() => { infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId(), + device_id: client.getDeviceId()!, state: VoiceBroadcastInfoState.Started, chunk_length: 120, }); @@ -441,11 +562,11 @@ describe("VoiceBroadcastRecording", () => { } as unknown as Relations; mocked(relationsContainer.getRelations).mockReturnValue([ mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId(), + device_id: client.getDeviceId()!, state: VoiceBroadcastInfoState.Stopped, ["m.relates_to"]: { rel_type: RelationType.Reference, - event_id: infoEvent.getId(), + event_id: infoEvent.getId()!, }, }), ]); From 22a2a937516bedbe4743884a49f2a41ec587ddba Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 17 Jan 2023 10:04:36 +0100 Subject: [PATCH 40/92] Only notify for first broadcast chunk (#9901) * Only notify for first broadcast chunk * Trigger CI --- src/Notifier.ts | 15 ++++++++++++-- src/i18n/strings/en_EN.json | 1 + test/Notifier-test.ts | 41 ++++++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/Notifier.ts b/src/Notifier.ts index 7e1f9eb0a4..42909a2632 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -50,7 +50,8 @@ import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import ToastStore from "./stores/ToastStore"; import { ElementCall } from "./models/Call"; -import { VoiceBroadcastChunkEventType } from "./voice-broadcast"; +import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast"; +import { getSenderName } from "./utils/event/getSenderName"; /* * Dispatches: @@ -80,9 +81,16 @@ const msgTypeHandlers = { }, [MsgType.Audio]: (event: MatrixEvent): string | null => { if (event.getContent()?.[VoiceBroadcastChunkEventType]) { - // mute broadcast chunks + if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) { + // Show a notification for the first broadcast chunk. + // At this point a user received something to listen to. + return _t("%(senderName)s started a voice broadcast", { senderName: getSenderName(event) }); + } + + // Mute other broadcast chunks return null; } + return TextForEvent.textForEvent(event); }, }; @@ -448,6 +456,9 @@ export const Notifier = { }, _evaluateEvent: function (ev: MatrixEvent) { + // Mute notifications for broadcast info events + if (ev.getType() === VoiceBroadcastInfoEventType) return; + let roomId = ev.getRoomId(); if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { // Attempt to translate a virtual room to a native one diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f59272f19..c3f7e3613c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -112,6 +112,7 @@ "Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)", "Default Device": "Default Device", "%(name)s is requesting verification": "%(name)s is requesting verification", + "%(senderName)s started a voice broadcast": "%(senderName)s started a voice broadcast", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again", "Unable to enable Notifications": "Unable to enable Notifications", diff --git a/test/Notifier-test.ts b/test/Notifier-test.ts index a3b1f9b2db..20b2c2e361 100644 --- a/test/Notifier-test.ts +++ b/test/Notifier-test.ts @@ -40,7 +40,8 @@ import { mkThread } from "./test-utils/threads"; import dis from "../src/dispatcher/dispatcher"; import { ThreadPayload } from "../src/dispatcher/payloads/ThreadPayload"; import { Action } from "../src/dispatcher/actions"; -import { VoiceBroadcastChunkEventType } from "../src/voice-broadcast"; +import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../src/voice-broadcast"; +import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils"; jest.mock("../src/utils/notifications", () => ({ // @ts-ignore @@ -75,8 +76,8 @@ describe("Notifier", () => { }); }; - const mkAudioEvent = (broadcastChunk = false): MatrixEvent => { - const chunkContent = broadcastChunk ? { [VoiceBroadcastChunkEventType]: {} } : {}; + const mkAudioEvent = (broadcastChunkContent?: object): MatrixEvent => { + const chunkContent = broadcastChunkContent ? { [VoiceBroadcastChunkEventType]: broadcastChunkContent } : {}; return mkEvent({ event: true, @@ -289,8 +290,20 @@ describe("Notifier", () => { ); }); - it("should not display a notification for a broadcast chunk", () => { - const audioEvent = mkAudioEvent(true); + it("should display the expected notification for a broadcast chunk with sequence = 1", () => { + const audioEvent = mkAudioEvent({ sequence: 1 }); + Notifier._displayPopupNotification(audioEvent, testRoom); + expect(MockPlatform.displayNotification).toHaveBeenCalledWith( + "@user:example.com (!room1:server)", + "@user:example.com started a voice broadcast", + "data:image/png;base64,00", + testRoom, + audioEvent, + ); + }); + + it("should display the expected notification for a broadcast chunk with sequence = 1", () => { + const audioEvent = mkAudioEvent({ sequence: 2 }); Notifier._displayPopupNotification(audioEvent, testRoom); expect(MockPlatform.displayNotification).not.toHaveBeenCalled(); }); @@ -499,6 +512,24 @@ describe("Notifier", () => { Notifier._evaluateEvent(mkAudioEvent()); expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1); }); + + it("should not show a notification for broadcast info events in any case", () => { + // Let client decide to show a notification + mockClient.getPushActionsForEvent.mockReturnValue({ + notify: true, + tweaks: {}, + }); + + const broadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( + "!other:example.org", + VoiceBroadcastInfoState.Started, + "@user:example.com", + "ABC123", + ); + + Notifier._evaluateEvent(broadcastStartedEvent); + expect(Notifier._displayPopupNotification).not.toHaveBeenCalled(); + }); }); describe("setPromptHidden", () => { From 62913218d26c170e874fd81878a745c9dccfed2b Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 18 Jan 2023 14:57:58 +1300 Subject: [PATCH 41/92] use 100% rather than auto with for reply tile width (#9924) --- res/css/views/rooms/_ReplyTile.pcss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_ReplyTile.pcss b/res/css/views/rooms/_ReplyTile.pcss index 1e70b47956..1a2e1a3814 100644 --- a/res/css/views/rooms/_ReplyTile.pcss +++ b/res/css/views/rooms/_ReplyTile.pcss @@ -32,11 +32,12 @@ limitations under the License. grid-template: "sender" auto "message" auto - / auto; + / 100%; text-decoration: none; color: $secondary-content; transition: color ease 0.15s; gap: 2px; + max-width: 100%; // avoid overflow with wide content &:hover { color: $primary-content; From 70d3d03c153938bcb8f9da32f8d2f3165f5853d6 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 18 Jan 2023 08:25:03 +0100 Subject: [PATCH 42/92] Fix logout devices on password reset (#9925) --- src/PasswordReset.ts | 4 + .../structures/auth/ForgotPassword.tsx | 1 + .../structures/auth/ForgotPassword-test.tsx | 113 +++++++++++------- 3 files changed, 77 insertions(+), 41 deletions(-) diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts index f6661a35f1..7dbc8a5406 100644 --- a/src/PasswordReset.ts +++ b/src/PasswordReset.ts @@ -104,6 +104,10 @@ export default class PasswordReset { ); } + public setLogoutDevices(logoutDevices: boolean): void { + this.logoutDevices = logoutDevices; + } + public async setNewPassword(password: string): Promise { this.password = password; await this.checkEmailLinkClicked(); diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index d6e0fea1c1..2c8f922e39 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -258,6 +258,7 @@ export default class ForgotPassword extends React.Component { } this.phase = Phase.ResettingPassword; + this.reset.setLogoutDevices(this.state.logoutDevices); try { await this.reset.setNewPassword(this.state.password); diff --git a/test/components/structures/auth/ForgotPassword-test.tsx b/test/components/structures/auth/ForgotPassword-test.tsx index 57eccec014..2e1742fcee 100644 --- a/test/components/structures/auth/ForgotPassword-test.tsx +++ b/test/components/structures/auth/ForgotPassword-test.tsx @@ -49,12 +49,17 @@ describe("", () => { }); }; - const clickButton = async (label: string): Promise => { + const click = async (element: Element): Promise => { await act(async () => { - await userEvent.click(screen.getByText(label), { delay: null }); + await userEvent.click(element, { delay: null }); }); }; + const waitForDialog = async (): Promise => { + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + }; + const itShouldCloseTheDialogAndShowThePasswordInput = (): void => { it("should close the dialog and show the password input", () => { expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); @@ -121,9 +126,9 @@ describe("", () => { }); }); - describe("when clicking »Sign in instead«", () => { + describe("and clicking »Sign in instead«", () => { beforeEach(async () => { - await clickButton("Sign in instead"); + await click(screen.getByText("Sign in instead")); }); it("should call onLoginClick()", () => { @@ -131,7 +136,7 @@ describe("", () => { }); }); - describe("when entering a non-email value", () => { + describe("and entering a non-email value", () => { beforeEach(async () => { await typeIntoField("Email address", "not en email"); }); @@ -141,13 +146,13 @@ describe("", () => { }); }); - describe("when submitting an unknown email", () => { + describe("and submitting an unknown email", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(client).requestPasswordEmailToken.mockRejectedValue({ errcode: "M_THREEPID_NOT_FOUND", }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should show an email not found message", () => { @@ -155,13 +160,13 @@ describe("", () => { }); }); - describe("when a connection error occurs", () => { + describe("and a connection error occurs", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(client).requestPasswordEmailToken.mockRejectedValue({ name: "ConnectionError", }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should show an info about that", () => { @@ -174,7 +179,7 @@ describe("", () => { }); }); - describe("when the server liveness check fails", () => { + describe("and the server liveness check fails", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockRejectedValue({}); @@ -183,7 +188,7 @@ describe("", () => { serverIsAlive: false, serverDeadError: "server down", }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should show the server error", () => { @@ -191,13 +196,13 @@ describe("", () => { }); }); - describe("when submitting an known email", () => { + describe("and submitting an known email", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(client).requestPasswordEmailToken.mockResolvedValue({ sid: testSid, }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should send the mail and show the check email view", () => { @@ -210,9 +215,9 @@ describe("", () => { expect(screen.getByText(testEmail)).toBeInTheDocument(); }); - describe("when clicking re-enter email", () => { + describe("and clicking »Re-enter email address«", () => { beforeEach(async () => { - await clickButton("Re-enter email address"); + await click(screen.getByText("Re-enter email address")); }); it("go back to the email input", () => { @@ -220,9 +225,9 @@ describe("", () => { }); }); - describe("when clicking resend email", () => { + describe("and clicking »Resend«", () => { beforeEach(async () => { - await userEvent.click(screen.getByText("Resend"), { delay: null }); + await click(screen.getByText("Resend")); // the message is shown after some time jest.advanceTimersByTime(500); }); @@ -237,16 +242,16 @@ describe("", () => { }); }); - describe("when clicking next", () => { + describe("and clicking »Next«", () => { beforeEach(async () => { - await clickButton("Next"); + await click(screen.getByText("Next")); }); it("should show the password input view", () => { expect(screen.getByText("Reset your password")).toBeInTheDocument(); }); - describe("when entering different passwords", () => { + describe("and entering different passwords", () => { beforeEach(async () => { await typeIntoField("New Password", testPassword); await typeIntoField("Confirm new password", testPassword + "asd"); @@ -257,7 +262,7 @@ describe("", () => { }); }); - describe("when entering a new password", () => { + describe("and entering a new password", () => { beforeEach(async () => { mocked(client.setPassword).mockRejectedValue({ httpStatus: 401 }); await typeIntoField("New Password", testPassword); @@ -273,7 +278,7 @@ describe("", () => { retry_after_ms: (13 * 60 + 37) * 1000, }, }); - await clickButton("Reset password"); + await click(screen.getByText("Reset password")); }); it("should show the rate limit error message", () => { @@ -285,10 +290,8 @@ describe("", () => { describe("and submitting it", () => { beforeEach(async () => { - await clickButton("Reset password"); - // double flush promises for the modal to appear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await click(screen.getByText("Reset password")); + await waitForDialog(); }); it("should send the new password and show the click validation link dialog", () => { @@ -316,9 +319,7 @@ describe("", () => { await act(async () => { await userEvent.click(screen.getByTestId("dialog-background"), { delay: null }); }); - // double flush promises for the modal to disappear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await waitForDialog(); }); itShouldCloseTheDialogAndShowThePasswordInput(); @@ -326,23 +327,17 @@ describe("", () => { describe("and dismissing the dialog", () => { beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByLabelText("Close dialog"), { delay: null }); - }); - // double flush promises for the modal to disappear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await click(screen.getByLabelText("Close dialog")); + await waitForDialog(); }); itShouldCloseTheDialogAndShowThePasswordInput(); }); - describe("when clicking re-enter email", () => { + describe("and clicking »Re-enter email address«", () => { beforeEach(async () => { - await clickButton("Re-enter email address"); - // double flush promises for the modal to disappear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await click(screen.getByText("Re-enter email address")); + await waitForDialog(); }); it("should close the dialog and go back to the email input", () => { @@ -351,7 +346,7 @@ describe("", () => { }); }); - describe("when validating the link from the mail", () => { + describe("and validating the link from the mail", () => { beforeEach(async () => { mocked(client.setPassword).mockResolvedValue({}); // be sure the next set password attempt was sent @@ -369,6 +364,42 @@ describe("", () => { }); }); }); + + describe("and clicking »Sign out of all devices« and »Reset password«", () => { + beforeEach(async () => { + await click(screen.getByText("Sign out of all devices")); + await click(screen.getByText("Reset password")); + await waitForDialog(); + }); + + it("should show the sign out warning dialog", async () => { + expect( + screen.getByText( + "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", + ), + ).toBeInTheDocument(); + + // confirm dialog + await click(screen.getByText("Continue")); + + // expect setPassword with logoutDevices = true + expect(client.setPassword).toHaveBeenCalledWith( + { + type: "m.login.email.identity", + threepid_creds: { + client_secret: expect.any(String), + sid: testSid, + }, + threepidCreds: { + client_secret: expect.any(String), + sid: testSid, + }, + }, + testPassword, + true, + ); + }); + }); }); }); }); From e4a9684d76a12e7d05b5c77608c5cbd3476c6897 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 18 Jan 2023 10:09:25 +0100 Subject: [PATCH 43/92] Fix cypress RTE flaky test (#9920) * Update @matrix-org/matrix-wysiwyg to 0.19.0 * Press {enter} to send message --- cypress/e2e/composer/composer.spec.ts | 7 +++---- package.json | 2 +- .../components/FormattingButtons-test.tsx | 2 +- yarn.lock | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts index 6d2879ff10..68208b7622 100644 --- a/cypress/e2e/composer/composer.spec.ts +++ b/cypress/e2e/composer/composer.spec.ts @@ -120,9 +120,8 @@ describe("Composer", () => { // Type another cy.get("div[contenteditable=true]").type("my message 1"); - // Press enter. Would be nice to just use {enter} but we can't because Cypress - // does not trigger an insertParagraph when you do that. - cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" }); + // Send message + cy.get("div[contenteditable=true]").type("{enter}"); // It was sent cy.contains(".mx_EventTile_body", "my message 1"); }); @@ -141,7 +140,7 @@ describe("Composer", () => { it("only sends when you press Ctrl+Enter", () => { // Type a message and press Enter cy.get("div[contenteditable=true]").type("my message 3"); - cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" }); + cy.get("div[contenteditable=true]").type("{enter}"); // It has not been sent yet cy.contains(".mx_EventTile_body", "my message 3").should("not.exist"); diff --git a/package.json b/package.json index 8ecd753077..8c86c2aa99 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.4.0", - "@matrix-org/matrix-wysiwyg": "^0.16.0", + "@matrix-org/matrix-wysiwyg": "^0.19.0", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx index 012090d5d8..88b17e9f43 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx @@ -36,7 +36,7 @@ const mockWysiwyg = { const openLinkModalSpy = jest.spyOn(LinkModal, "openLinkModal"); const testCases: Record< - Exclude, + Exclude, { label: string; mockFormatFn: jest.Func | jest.SpyInstance } > = { bold: { label: "Bold", mockFormatFn: mockWysiwyg.bold }, diff --git a/yarn.lock b/yarn.lock index eae0ce7f25..45cc70e8d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1589,10 +1589,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== -"@matrix-org/matrix-wysiwyg@^0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.16.0.tgz#2eb81899cedc740f522bd7c2839bd9151d67a28e" - integrity sha512-w+/bUQ5x4lVRncrYSmdxy5ww4kkgXeSg4aFfby9c7c6o/+o4gfV6/XBdoJ71nhStyIYIweKAz8i3zA3rKonyvw== +"@matrix-org/matrix-wysiwyg@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.19.0.tgz#5ffbabf8a59317ecdb45ba5fa1d06fff150ede40" + integrity sha512-1iL/+kjwWAlpWAq64DbkDkE7KGxvR5lNojZgCKMIyuvuKWv8Ikqxa9VOOYFtovKvSqgGRJaYN7/OkKWxZjiDcw== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" From dacbf7622740c5a64e0fba09c6ec04f0dece6418 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 18 Jan 2023 14:20:49 +0100 Subject: [PATCH 44/92] Disable multiple messages when {enter} is pressed multiple times (#9929) --- cypress/e2e/composer/composer.spec.ts | 15 +++++++++++++++ src/components/views/rooms/MessageComposer.tsx | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts index 68208b7622..289d865ba6 100644 --- a/cypress/e2e/composer/composer.spec.ts +++ b/cypress/e2e/composer/composer.spec.ts @@ -126,6 +126,21 @@ describe("Composer", () => { cy.contains(".mx_EventTile_body", "my message 1"); }); + it("sends only one message when you press Enter multiple times", () => { + // Type a message + cy.get("div[contenteditable=true]").type("my message 0"); + // It has not been sent yet + cy.contains(".mx_EventTile_body", "my message 0").should("not.exist"); + + // Click send + cy.get("div[contenteditable=true]").type("{enter}"); + cy.get("div[contenteditable=true]").type("{enter}"); + cy.get("div[contenteditable=true]").type("{enter}"); + // It has been sent + cy.contains(".mx_EventTile_body", "my message 0"); + cy.get(".mx_EventTile_body").should("have.length", 1); + }); + it("can write formatted text", () => { cy.get("div[contenteditable=true]").type("my {ctrl+b}bold{ctrl+b} message"); cy.get('div[aria-label="Send message"]').click(); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 794b774db1..f2a6e963f7 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -334,7 +334,9 @@ export class MessageComposer extends React.Component { if (this.state.isWysiwygLabEnabled) { const { permalinkCreator, relation, replyToEvent } = this.props; - await sendMessage(this.state.composerContent, this.state.isRichTextEnabled, { + const composerContent = this.state.composerContent; + this.setState({ composerContent: "", initialComposerContent: "" }); + await sendMessage(composerContent, this.state.isRichTextEnabled, { mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, @@ -342,7 +344,6 @@ export class MessageComposer extends React.Component { replyToEvent, }); dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer }); - this.setState({ composerContent: "", initialComposerContent: "" }); } }; From d8947d0168431769d2a42bda7e30a50553d9b5c0 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 18 Jan 2023 13:48:11 +0000 Subject: [PATCH 45/92] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b7f3dc6f11..db2609a823 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "package.json", ".stylelintrc.js" ], - "main": "./lib/index.ts", + "main": "./src/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -259,6 +259,5 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - }, - "typings": "./lib/index.d.ts" + } } From baa120fff3bdd10c5156feb20f88f2e7f421fd34 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 18 Jan 2023 13:49:03 +0000 Subject: [PATCH 46/92] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index db2609a823..a9121dc123 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "23.1.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index fcecfd2e67..62d88362d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6491,10 +6491,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@23.1.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "23.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.1.0.tgz#c8dc89f82c5dce1ec25eedf05613cd9665cc1bbd" - integrity sha512-/9eFWJBQceQX+2dHYZ5/9p0kjJs1PVDbIEWvR6FPjdnuNK3R4qi2NwpM6MoAHt8m7mX5qUG7x0NUWlalaOwrGQ== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2fcc4811dd913bb774dd1c7f67cb693c4456d71e" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2" From 6d354e3e10b5c5abfe590e1459d8305c3b38cedf Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 18 Jan 2023 15:49:34 +0100 Subject: [PATCH 47/92] Add test coverage (#9928) --- src/stores/widgets/WidgetLayoutStore.ts | 4 +- .../RoomDeviceSettingsHandler-test.ts | 52 ++++-- test/stores/AutoRageshakeStore-test.ts | 100 ++++++++++ test/stores/WidgetLayoutStore-test.ts | 174 ++++++++++++++++-- 4 files changed, 301 insertions(+), 29 deletions(-) create mode 100644 test/stores/AutoRageshakeStore-test.ts diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index 352393c435..ef5f62de73 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -414,7 +414,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { widgets.forEach((w, i) => { localLayout[w.id] = { container: container, - width: widths[i], + width: widths?.[i], index: i, height: height, }; @@ -437,7 +437,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { widgets.forEach((w, i) => { localLayout[w.id] = { container: container, - width: widths[i], + width: widths?.[i], index: i, height: height, }; diff --git a/test/settings/handlers/RoomDeviceSettingsHandler-test.ts b/test/settings/handlers/RoomDeviceSettingsHandler-test.ts index 694cfd5d88..e451edf600 100644 --- a/test/settings/handlers/RoomDeviceSettingsHandler-test.ts +++ b/test/settings/handlers/RoomDeviceSettingsHandler-test.ts @@ -15,21 +15,49 @@ limitations under the License. */ import RoomDeviceSettingsHandler from "../../../src/settings/handlers/RoomDeviceSettingsHandler"; -import { WatchManager } from "../../../src/settings/WatchManager"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { CallbackFn, WatchManager } from "../../../src/settings/WatchManager"; describe("RoomDeviceSettingsHandler", () => { - it("should correctly read cached values", () => { - const watchers = new WatchManager(); - const handler = new RoomDeviceSettingsHandler(watchers); + const roomId = "!room:example.com"; + const value = "test value"; + const testSettings = [ + "RightPanel.phases", + // special case in RoomDeviceSettingsHandler + "blacklistUnverifiedDevices", + ]; + let watchers: WatchManager; + let handler: RoomDeviceSettingsHandler; + let settingListener: CallbackFn; - const settingName = "RightPanel.phases"; - const roomId = "!room:server"; - const value = { - isOpen: true, - history: [{}], - }; + beforeEach(() => { + watchers = new WatchManager(); + handler = new RoomDeviceSettingsHandler(watchers); + settingListener = jest.fn(); + }); - handler.setValue(settingName, roomId, value); - expect(handler.getValue(settingName, roomId)).toEqual(value); + afterEach(() => { + watchers.unwatchSetting(settingListener); + }); + + it.each(testSettings)("should write/read/clear the value for »%s«", (setting: string): void => { + // initial value should be null + watchers.watchSetting(setting, roomId, settingListener); + + expect(handler.getValue(setting, roomId)).toBeNull(); + + // set and read value + handler.setValue(setting, roomId, value); + expect(settingListener).toHaveBeenCalledWith(roomId, SettingLevel.ROOM_DEVICE, value); + expect(handler.getValue(setting, roomId)).toEqual(value); + + // clear value + handler.setValue(setting, roomId, null); + expect(settingListener).toHaveBeenCalledWith(roomId, SettingLevel.ROOM_DEVICE, null); + expect(handler.getValue(setting, roomId)).toBeNull(); + }); + + it("canSetValue should return true", () => { + expect(handler.canSetValue("test setting", roomId)).toBe(true); }); }); diff --git a/test/stores/AutoRageshakeStore-test.ts b/test/stores/AutoRageshakeStore-test.ts new file mode 100644 index 0000000000..ff57bda59c --- /dev/null +++ b/test/stores/AutoRageshakeStore-test.ts @@ -0,0 +1,100 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked } from "jest-mock"; +import { ClientEvent, EventType, MatrixClient, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; +import { SyncState } from "matrix-js-sdk/src/sync"; + +import SettingsStore from "../../src/settings/SettingsStore"; +import AutoRageshakeStore from "../../src/stores/AutoRageshakeStore"; +import { mkEvent, stubClient } from "../test-utils"; + +jest.mock("../../src/rageshake/submit-rageshake"); + +describe("AutoRageshakeStore", () => { + const roomId = "!room:example.com"; + let client: MatrixClient; + let utdEvent: MatrixEvent; + let autoRageshakeStore: AutoRageshakeStore; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + + client = stubClient(); + + // @ts-ignore bypass private ctor for tests + autoRageshakeStore = new AutoRageshakeStore(); + autoRageshakeStore.start(); + + utdEvent = mkEvent({ + event: true, + content: {}, + room: roomId, + user: client.getSafeUserId(), + type: EventType.RoomMessage, + }); + jest.spyOn(utdEvent, "isDecryptionFailure").mockReturnValue(true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("when the initial sync completed", () => { + beforeEach(() => { + client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Stopped, { nextSyncToken: "abc123" }); + }); + + describe("and an undecryptable event occurs", () => { + beforeEach(() => { + client.emit(MatrixEventEvent.Decrypted, utdEvent); + // simulate event grace period + jest.advanceTimersByTime(5500); + }); + + it("should send a rageshake", () => { + expect(mocked(client).sendToDevice.mock.calls).toMatchInlineSnapshot(` + [ + [ + "im.vector.auto_rs_request", + { + "@userId:matrix.org": { + "undefined": { + "device_id": undefined, + "event_id": "${utdEvent.getId()}", + "recipient_rageshake": undefined, + "room_id": "!room:example.com", + "sender_key": undefined, + "session_id": undefined, + "user_id": "@userId:matrix.org", + }, + }, + }, + ], + ] + `); + }); + }); + }); +}); diff --git a/test/stores/WidgetLayoutStore-test.ts b/test/stores/WidgetLayoutStore-test.ts index 54d40c52b7..81f373d0f6 100644 --- a/test/stores/WidgetLayoutStore-test.ts +++ b/test/stores/WidgetLayoutStore-test.ts @@ -14,11 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; import WidgetStore, { IApp } from "../../src/stores/WidgetStore"; import { Container, WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore"; import { stubClient } from "../test-utils"; +import defaultDispatcher from "../../src/dispatcher/dispatcher"; // setup test env values const roomId = "!room:server"; @@ -34,31 +36,54 @@ const mockRoom = { }, }; -const mockApps = [ - { roomId: roomId, id: "1" }, - { roomId: roomId, id: "2" }, - { roomId: roomId, id: "3" }, - { roomId: roomId, id: "4" }, -]; - -// fake the WidgetStore.instance to just return an object with `getApps` -jest.spyOn(WidgetStore, "instance", "get").mockReturnValue({ getApps: (_room) => mockApps }); - describe("WidgetLayoutStore", () => { - // we need to init a client so it does not error, when asking for DeviceStorage handlers (SettingsStore.setValue("Widgets.layout")) - stubClient(); + let client: MatrixClient; + let store: WidgetLayoutStore; + let roomUpdateListener: (event: string) => void; + let mockApps: IApp[]; - const store = WidgetLayoutStore.instance; + beforeEach(() => { + mockApps = [ + { roomId: roomId, id: "1" }, + { roomId: roomId, id: "2" }, + { roomId: roomId, id: "3" }, + { roomId: roomId, id: "4" }, + ]; - it("all widgets should be in the right container by default", async () => { + // fake the WidgetStore.instance to just return an object with `getApps` + jest.spyOn(WidgetStore, "instance", "get").mockReturnValue({ + on: jest.fn(), + off: jest.fn(), + getApps: () => mockApps, + } as unknown as WidgetStore); + }); + + beforeAll(() => { + // we need to init a client so it does not error, when asking for DeviceStorage handlers (SettingsStore.setValue("Widgets.layout")) + client = stubClient(); + + roomUpdateListener = jest.fn(); + // @ts-ignore bypass private ctor for tests + store = new WidgetLayoutStore(); + store.addListener(`update_${roomId}`, roomUpdateListener); + }); + + afterAll(() => { + store.removeListener(`update_${roomId}`, roomUpdateListener); + }); + + it("all widgets should be in the right container by default", () => { store.recalculateRoom(mockRoom); expect(store.getContainerWidgets(mockRoom, Container.Right).length).toStrictEqual(mockApps.length); }); + it("add widget to top container", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0]]); + expect(store.getContainerHeight(mockRoom, Container.Top)).toBeNull(); }); + it("add three widgets to top container", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); @@ -68,6 +93,7 @@ describe("WidgetLayoutStore", () => { new Set([mockApps[0], mockApps[1], mockApps[2]]), ); }); + it("cannot add more than three widgets to top container", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); @@ -75,6 +101,7 @@ describe("WidgetLayoutStore", () => { store.moveToContainer(mockRoom, mockApps[2], Container.Top); expect(store.canAddToContainer(mockRoom, Container.Top)).toEqual(false); }); + it("remove pins when maximising (other widget)", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); @@ -87,6 +114,7 @@ describe("WidgetLayoutStore", () => { ); expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([mockApps[3]]); }); + it("remove pins when maximising (one of the pinned widgets)", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); @@ -99,6 +127,7 @@ describe("WidgetLayoutStore", () => { new Set([mockApps[1], mockApps[2], mockApps[3]]), ); }); + it("remove maximised when pinning (other widget)", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Center); @@ -109,6 +138,7 @@ describe("WidgetLayoutStore", () => { new Set([mockApps[2], mockApps[3], mockApps[0]]), ); }); + it("remove maximised when pinning (same widget)", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Center); @@ -119,4 +149,118 @@ describe("WidgetLayoutStore", () => { new Set([mockApps[2], mockApps[3], mockApps[1]]), ); }); + + it("should recalculate all rooms when the client is ready", async () => { + mocked(client.getVisibleRooms).mockReturnValue([mockRoom]); + await store.start(); + + expect(roomUpdateListener).toHaveBeenCalled(); + expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([mockApps[0]]); + expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]); + expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([mockApps[1], mockApps[2], mockApps[3]]); + }); + + it("should clear the layout and emit an update if there are no longer apps in the room", () => { + store.recalculateRoom(mockRoom); + mocked(roomUpdateListener).mockClear(); + + jest.spyOn(WidgetStore, "instance", "get").mockReturnValue(( + ({ getApps: (): IApp[] => [] } as unknown as WidgetStore) + )); + store.recalculateRoom(mockRoom); + expect(roomUpdateListener).toHaveBeenCalled(); + expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]); + expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]); + expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]); + }); + + it("should clear the layout if the client is not viable", () => { + store.recalculateRoom(mockRoom); + defaultDispatcher.dispatch( + { + action: "on_client_not_viable", + }, + true, + ); + + expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]); + expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]); + expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]); + }); + + it("should return the expected resizer distributions", () => { + // this only works for top widgets + store.recalculateRoom(mockRoom); + store.moveToContainer(mockRoom, mockApps[0], Container.Top); + store.moveToContainer(mockRoom, mockApps[1], Container.Top); + expect(store.getResizerDistributions(mockRoom, Container.Top)).toEqual(["50.0%"]); + }); + + it("should set and return container height", () => { + store.recalculateRoom(mockRoom); + store.moveToContainer(mockRoom, mockApps[0], Container.Top); + store.moveToContainer(mockRoom, mockApps[1], Container.Top); + store.setContainerHeight(mockRoom, Container.Top, 23); + expect(store.getContainerHeight(mockRoom, Container.Top)).toBe(23); + }); + + it("should move a widget within a container", () => { + store.recalculateRoom(mockRoom); + store.moveToContainer(mockRoom, mockApps[0], Container.Top); + store.moveToContainer(mockRoom, mockApps[1], Container.Top); + store.moveToContainer(mockRoom, mockApps[2], Container.Top); + expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([ + mockApps[0], + mockApps[1], + mockApps[2], + ]); + store.moveWithinContainer(mockRoom, Container.Top, mockApps[0], 1); + expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([ + mockApps[1], + mockApps[0], + mockApps[2], + ]); + }); + + it("should copy the layout to the room", async () => { + await store.start(); + store.recalculateRoom(mockRoom); + store.moveToContainer(mockRoom, mockApps[0], Container.Top); + store.copyLayoutToRoom(mockRoom); + + expect(mocked(client.sendStateEvent).mock.calls).toMatchInlineSnapshot(` + [ + [ + "!room:server", + "io.element.widgets.layout", + { + "widgets": { + "1": { + "container": "top", + "height": 23, + "index": 2, + "width": 64, + }, + "2": { + "container": "top", + "height": 23, + "index": 0, + "width": 10, + }, + "3": { + "container": "top", + "height": 23, + "index": 1, + "width": 26, + }, + "4": { + "container": "right", + }, + }, + }, + "", + ], + ] + `); + }); }); From 4d2b27a96d19e9c206912213f43379ff77c08f48 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 18 Jan 2023 15:56:43 +0100 Subject: [PATCH 48/92] Fix broken threads list timestamp layout (#9922) * Add option to show full identifier as tooltip on sender profiles * Show full user id as tooltip on threads list entries * Fix broken threads list timestamp layout Previously, thread list timestamps would overflow into the unread messages bubble on the right. This is fixed by resetting the width of the timestamp and ensuring both the timestamp and the display name can shrink if necessary. Both now also use ellipses if necessary. --- res/css/views/rooms/_EventTile.pcss | 11 +++++- .../views/messages/DisambiguatedProfile.tsx | 34 ++++++++++++------- .../views/messages/SenderProfile.tsx | 5 ++- src/components/views/rooms/EventTile.tsx | 4 ++- src/i18n/strings/en_EN.json | 1 + 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 3beaeacf72..da77396469 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -884,6 +884,7 @@ $left-gutter: 64px; &::before { inset: 0; + pointer-events: none; /* ensures the title for the sender name can be correctly displayed */ } /* Display notification dot */ @@ -927,8 +928,14 @@ $left-gutter: 64px; inset: $padding auto auto $padding; } + .mx_EventTile_details { + overflow: hidden; + } + .mx_DisambiguatedProfile { display: inline-flex; + align-items: center; + flex: 1; .mx_DisambiguatedProfile_displayName, .mx_DisambiguatedProfile_mxid { @@ -979,7 +986,9 @@ $left-gutter: 64px; .mx_MessageTimestamp { font-size: $font-12px; - max-width: var(--MessageTimestamp-max-width); + width: unset; /* Cancel the default width */ + overflow: hidden; /* ensure correct overflow behavior */ + text-overflow: ellipsis; position: initial; margin-left: auto; /* to ensure it's end-aligned even if it's the only element of its parent */ } diff --git a/src/components/views/messages/DisambiguatedProfile.tsx b/src/components/views/messages/DisambiguatedProfile.tsx index 942eaa0eee..2fd47e107b 100644 --- a/src/components/views/messages/DisambiguatedProfile.tsx +++ b/src/components/views/messages/DisambiguatedProfile.tsx @@ -1,6 +1,6 @@ /* Copyright 2021 Šimon Brandner -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022-2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import React from "react"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import classNames from "classnames"; +import { _t } from "../../../languageHandler"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; import UserIdentifier from "../../../customisations/UserIdentifier"; @@ -28,35 +29,44 @@ interface IProps { onClick?(): void; colored?: boolean; emphasizeDisplayName?: boolean; + withTooltip?: boolean; } export default class DisambiguatedProfile extends React.Component { public render(): JSX.Element { - const { fallbackName, member, colored, emphasizeDisplayName, onClick } = this.props; + const { fallbackName, member, colored, emphasizeDisplayName, withTooltip, onClick } = this.props; const rawDisplayName = member?.rawDisplayName || fallbackName; const mxid = member?.userId; - let colorClass; + let colorClass: string | undefined; if (colored) { colorClass = getUserNameColorClass(fallbackName); } let mxidElement; - if (member?.disambiguate && mxid) { - mxidElement = ( - - {UserIdentifier.getDisplayUserIdentifier(mxid, { withDisplayName: true, roomId: member.roomId })} - - ); + let title: string | undefined; + + if (mxid) { + const identifier = + UserIdentifier.getDisplayUserIdentifier?.(mxid, { + withDisplayName: true, + roomId: member.roomId, + }) ?? mxid; + if (member?.disambiguate) { + mxidElement = {identifier}; + } + title = _t("%(displayName)s (%(matrixId)s)", { + displayName: rawDisplayName, + matrixId: identifier, + }); } - const displayNameClasses = classNames({ + const displayNameClasses = classNames(colorClass, { mx_DisambiguatedProfile_displayName: emphasizeDisplayName, - [colorClass]: true, }); return ( -
+
{rawDisplayName} diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 6280b04008..4027c1ded4 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -1,4 +1,5 @@ /* + Copyright 2023 The Matrix.org Foundation C.I.C. Copyright 2015, 2016 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,9 +25,10 @@ import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile"; interface IProps { mxEvent: MatrixEvent; onClick?(): void; + withTooltip?: boolean; } -export default function SenderProfile({ mxEvent, onClick }: IProps): JSX.Element { +export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps): JSX.Element { const member = useRoomMemberProfile({ userId: mxEvent.getSender(), member: mxEvent.sender, @@ -39,6 +41,7 @@ export default function SenderProfile({ mxEvent, onClick }: IProps): JSX.Element member={member} colored={true} emphasizeDisplayName={true} + withTooltip={withTooltip} /> ) : null; } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 351f4df215..5620ab9356 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2023 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -1091,6 +1091,8 @@ export class UnwrappedEventTile extends React.Component this.context.timelineRenderingType === TimelineRenderingType.Thread ) { sender = ; + } else if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { + sender = ; } else { sender = ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c3f7e3613c..f3768067cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2316,6 +2316,7 @@ "Last month": "Last month", "The beginning of the room": "The beginning of the room", "Jump to date": "Jump to date", + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", "Downloading": "Downloading", "Decrypting": "Decrypting", "Download": "Download", From f0a7b2886ec1058741d45696787ce0302ea42fa6 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 18 Jan 2023 16:43:18 +0000 Subject: [PATCH 49/92] replace .at() with array.length-1 --- src/Unread.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Unread.ts b/src/Unread.ts index cbd30b2bb8..6e7218cad1 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -85,7 +85,7 @@ export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread): // https://github.com/vector-im/element-web/issues/2427 // ...and possibly some of the others at // https://github.com/vector-im/element-web/issues/3363 - if (roomOrThread.timeline.at(-1)?.getSender() === myUserId) { + if (roomOrThread.timeline[roomOrThread.timeline.length - 1]?.getSender() === myUserId) { return false; } From 21f0825703337fa5c5018588e70df92cb6a0b773 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Jan 2023 17:19:12 +0000 Subject: [PATCH 50/92] refactor: sliding sync: convert to lists-as-keys rather than indexes Sister PR to https://github.com/matrix-org/matrix-js-sdk/pull/3076 --- src/SlidingSyncManager.ts | 47 +++++--------------- src/components/views/rooms/RoomSublist.tsx | 6 +-- src/hooks/useSlidingSyncRoomSearch.ts | 7 ++- src/stores/room-list/SlidingRoomListStore.ts | 20 +++------ 4 files changed, 24 insertions(+), 56 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 75a796df19..7f47e41728 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -137,10 +137,10 @@ export class SlidingSyncManager { this.listIdToIndex = {}; // by default use the encrypted subscription as that gets everything, which is a safer // default than potentially missing member events. - this.slidingSync = new SlidingSync(proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS); + this.slidingSync = new SlidingSync(proxyUrl, new Map(), ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS); this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION); // set the space list - this.slidingSync.setList(this.getOrAllocateListIndex(SlidingSyncManager.ListSpaces), { + this.slidingSync.setList(SlidingSyncManager.ListSpaces, { ranges: [[0, 20]], sort: ["by_name"], slow_get_all_rooms: true, @@ -182,38 +182,16 @@ export class SlidingSyncManager { return null; } - /** - * Allocate or retrieve the list index for an arbitrary list ID. For example SlidingSyncManager.ListSpaces - * @param listId A string which represents the list. - * @returns The index to use when registering lists or listening for callbacks. - */ - public getOrAllocateListIndex(listId: string): number { - let index = this.listIdToIndex[listId]; - if (index === undefined) { - // assign next highest index - index = -1; - for (const id in this.listIdToIndex) { - const listIndex = this.listIdToIndex[id]; - if (listIndex > index) { - index = listIndex; - } - } - index++; - this.listIdToIndex[listId] = index; - } - return index; - } - /** * Ensure that this list is registered. - * @param listIndex The list index to register + * @param listKey The list key to register * @param updateArgs The fields to update on the list. * @returns The complete list request params */ - public async ensureListRegistered(listIndex: number, updateArgs: PartialSlidingSyncRequest): Promise { - logger.debug("ensureListRegistered:::", listIndex, updateArgs); + public async ensureListRegistered(listKey: string, updateArgs: PartialSlidingSyncRequest): Promise { + logger.debug("ensureListRegistered:::", listKey, updateArgs); await this.configureDefer.promise; - let list = this.slidingSync.getList(listIndex); + let list = this.slidingSync.getListParams(listKey); if (!list) { list = { ranges: [[0, 20]], @@ -252,14 +230,14 @@ export class SlidingSyncManager { try { // if we only have range changes then call a different function so we don't nuke the list from before if (updateArgs.ranges && Object.keys(updateArgs).length === 1) { - await this.slidingSync.setListRanges(listIndex, updateArgs.ranges); + await this.slidingSync.setListRanges(listKey, updateArgs.ranges); } else { - await this.slidingSync.setList(listIndex, list); + await this.slidingSync.setList(listKey, list); } } catch (err) { logger.debug("ensureListRegistered: update failed txn_id=", err); } - return this.slidingSync.getList(listIndex); + return this.slidingSync.getListParams(listKey); } public async setRoomVisible(roomId: string, visible: boolean): Promise { @@ -304,7 +282,6 @@ export class SlidingSyncManager { */ public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load - const listIndex = this.getOrAllocateListIndex(SlidingSyncManager.ListSearch); let startIndex = batchSize; let hasMore = true; let firstTime = true; @@ -316,7 +293,7 @@ export class SlidingSyncManager { [startIndex, endIndex], ]; if (firstTime) { - await this.slidingSync.setList(listIndex, { + await this.slidingSync.setList(SlidingSyncManager.ListSearch, { // e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure // any changes to the list whilst spidering are caught. ranges: ranges, @@ -342,7 +319,7 @@ export class SlidingSyncManager { }, }); } else { - await this.slidingSync.setListRanges(listIndex, ranges); + await this.slidingSync.setListRanges(SlidingSyncManager.ListSearch, ranges); } // gradually request more over time await sleep(gapBetweenRequestsMs); @@ -350,7 +327,7 @@ export class SlidingSyncManager { // do nothing, as we reject only when we get interrupted but that's fine as the next // request will include our data } - hasMore = endIndex + 1 < this.slidingSync.getListData(listIndex)?.joinedCount; + hasMore = endIndex + 1 < this.slidingSync.getListData(SlidingSyncManager.ListSearch)?.joinedCount; startIndex += batchSize; firstTime = false; } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 11efcda896..7674916d70 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -340,9 +340,8 @@ export default class RoomSublist extends React.Component { private onShowAllClick = async (): Promise => { if (this.slidingSyncMode) { - const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId); const count = RoomListStore.instance.getCount(this.props.tagId); - await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, { + await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, { ranges: [[0, count]], }); } @@ -566,8 +565,7 @@ export default class RoomSublist extends React.Component { let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; if (this.slidingSyncMode) { - const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId); - const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex); + const slidingList = SlidingSyncManager.instance.slidingSync.getListParams(this.props.tagId); isAlphabetical = slidingList.sort[0] === "by_name"; isUnreadFirst = slidingList.sort[0] === "by_notification_level"; } diff --git a/src/hooks/useSlidingSyncRoomSearch.ts b/src/hooks/useSlidingSyncRoomSearch.ts index 3713ab7618..dc1caba917 100644 --- a/src/hooks/useSlidingSyncRoomSearch.ts +++ b/src/hooks/useSlidingSyncRoomSearch.ts @@ -34,7 +34,6 @@ export const useSlidingSyncRoomSearch = (): { const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(false); - const listIndex = SlidingSyncManager.instance.getOrAllocateListIndex(SlidingSyncManager.ListSearch); const [updateQuery, updateResult] = useLatestResult<{ term: string; limit?: number }, Room[]>(setRooms); @@ -50,14 +49,14 @@ export const useSlidingSyncRoomSearch = (): { try { setLoading(true); - await SlidingSyncManager.instance.ensureListRegistered(listIndex, { + await SlidingSyncManager.instance.ensureListRegistered(SlidingSyncManager.ListSearch, { ranges: [[0, limit]], filters: { room_name_like: term, }, }); const rooms = []; - const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(listIndex); + const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(SlidingSyncManager.ListSearch); let i = 0; while (roomIndexToRoomId[i]) { const roomId = roomIndexToRoomId[i]; @@ -78,7 +77,7 @@ export const useSlidingSyncRoomSearch = (): { // TODO: delete the list? } }, - [updateQuery, updateResult, listIndex], + [updateQuery, updateResult, SlidingSyncManager.ListSearch], ); return { diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts index 3807dd6559..b54dd3913d 100644 --- a/src/stores/room-list/SlidingRoomListStore.ts +++ b/src/stores/room-list/SlidingRoomListStore.ts @@ -89,15 +89,14 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl public async setTagSorting(tagId: TagID, sort: SortAlgorithm): Promise { logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort); this.tagIdToSortAlgo[tagId] = sort; - const slidingSyncIndex = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); switch (sort) { case SortAlgorithm.Alphabetic: - await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, { + await this.context.slidingSyncManager.ensureListRegistered(tagId, { sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], }); break; case SortAlgorithm.Recent: - await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, { + await this.context.slidingSyncManager.ensureListRegistered(tagId, { sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], }); break; @@ -164,8 +163,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl // check all lists for each tag we know about and see if the room is there const tags: TagID[] = []; for (const tagId in this.tagIdToSortAlgo) { - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); - const listData = this.context.slidingSyncManager.slidingSync.getListData(index); + const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId); if (!listData) { continue; } @@ -259,11 +257,10 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl } private onSlidingSyncListUpdate( - listIndex: number, + tagId: string, joinCount: number, roomIndexToRoomId: Record, ): void { - const tagId = this.context.slidingSyncManager.listIdForIndex(listIndex); this.counts[tagId] = joinCount; this.refreshOrderedLists(tagId, roomIndexToRoomId); // let the UI update @@ -295,8 +292,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl if (room) { // resort it based on the slidingSync view of the list. This may cause this old sticky // room to cease to exist. - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); - const listData = this.context.slidingSyncManager.slidingSync.getListData(index); + const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId); if (!listData) { continue; } @@ -334,9 +330,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config this.tagIdToSortAlgo[tagId] = sort; this.emit(LISTS_LOADING_EVENT, tagId, true); - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); this.context.slidingSyncManager - .ensureListRegistered(index, { + .ensureListRegistered(tagId, { filters: filter, sort: SlidingSyncSortToFilter[sort], }) @@ -367,9 +362,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl ); this.emit(LISTS_LOADING_EVENT, tagId, true); - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); this.context.slidingSyncManager - .ensureListRegistered(index, { + .ensureListRegistered(tagId, { filters: filters, }) .then(() => { From c34df2bf968c7896dba261736016f063717d4f1e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Jan 2023 17:24:31 +0000 Subject: [PATCH 51/92] Remove const from hook --- src/hooks/useSlidingSyncRoomSearch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSlidingSyncRoomSearch.ts b/src/hooks/useSlidingSyncRoomSearch.ts index dc1caba917..c029d8c21b 100644 --- a/src/hooks/useSlidingSyncRoomSearch.ts +++ b/src/hooks/useSlidingSyncRoomSearch.ts @@ -77,7 +77,7 @@ export const useSlidingSyncRoomSearch = (): { // TODO: delete the list? } }, - [updateQuery, updateResult, SlidingSyncManager.ListSearch], + [updateQuery, updateResult], ); return { From 576d10315996c0b69d21965d06f2d069491f6d5a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 18 Jan 2023 21:57:43 +0000 Subject: [PATCH 52/92] Reset matrix-js-sdk back to develop branch --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 62d88362d3..c90986604e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6493,7 +6493,7 @@ matrix-events-sdk@0.0.1: "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "23.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2fcc4811dd913bb774dd1c7f67cb693c4456d71e" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/83563c7a01bbeaf7f83f4b7feccc03647b536e7c" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2" From eb43f3449eeda289352061b36081bec1bfaf4cc2 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Thu, 19 Jan 2023 09:03:48 +0100 Subject: [PATCH 53/92] Fix the problem that the password reset email has to be confirmed twice (#9926) --- .../structures/auth/ForgotPassword.tsx | 2 ++ .../structures/auth/ForgotPassword-test.tsx | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 2c8f922e39..94399bf88c 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -262,6 +262,8 @@ export default class ForgotPassword extends React.Component { try { await this.reset.setNewPassword(this.state.password); + this.setState({ phase: Phase.Done }); + return; } catch (err: any) { if (err.httpStatus !== 401) { // 401 = waiting for email verification, else unknown error diff --git a/test/components/structures/auth/ForgotPassword-test.tsx b/test/components/structures/auth/ForgotPassword-test.tsx index 2e1742fcee..5a8dbc2359 100644 --- a/test/components/structures/auth/ForgotPassword-test.tsx +++ b/test/components/structures/auth/ForgotPassword-test.tsx @@ -288,6 +288,37 @@ describe("", () => { }); }); + describe("and confirm the email link and submitting the new password", () => { + beforeEach(async () => { + // fake link confirmed by resolving client.setPassword instead of raising an error + mocked(client.setPassword).mockResolvedValue({}); + await click(screen.getByText("Reset password")); + }); + + it("should send the new password (once)", () => { + expect(client.setPassword).toHaveBeenCalledWith( + { + type: "m.login.email.identity", + threepid_creds: { + client_secret: expect.any(String), + sid: testSid, + }, + threepidCreds: { + client_secret: expect.any(String), + sid: testSid, + }, + }, + testPassword, + false, + ); + + // be sure that the next attempt to set the password would have been sent + jest.advanceTimersByTime(3000); + // it should not retry to set the password + expect(client.setPassword).toHaveBeenCalledTimes(1); + }); + }); + describe("and submitting it", () => { beforeEach(async () => { await click(screen.getByText("Reset password")); From b47588fc5cfd86afc0753a37941b2a43c5ac0baf Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 19 Jan 2023 10:17:18 +0100 Subject: [PATCH 54/92] Fix {enter} press in RTE (#9927) Fix enter combination in RTE --- .../hooks/useInputEventProcessor.ts | 16 +++++--- .../components/WysiwygComposer-test.tsx | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index fb36bbf2d1..3a223fb6b1 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -19,6 +19,12 @@ import { useCallback } from "react"; import { useSettingValue } from "../../../../../hooks/useSettings"; +function isEnterPressed(event: KeyboardEvent): boolean { + // Ugly but here we need to send the message only if Enter is pressed + // And we need to stop the event propagation on enter to avoid the composer to grow + return event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey; +} + export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent) => WysiwygEvent | null { const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend"); return useCallback( @@ -28,12 +34,12 @@ export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent } const isKeyboardEvent = event instanceof KeyboardEvent; - const isEnterPress = - !isCtrlEnter && (isKeyboardEvent ? event.key === "Enter" : event.inputType === "insertParagraph"); - // sendMessage is sent when ctrl+enter is pressed - const isSendMessage = !isKeyboardEvent && event.inputType === "sendMessage"; + const isEnterPress = !isCtrlEnter && isKeyboardEvent && isEnterPressed(event); + const isInsertParagraph = !isCtrlEnter && !isKeyboardEvent && event.inputType === "insertParagraph"; + // sendMessage is sent when cmd+enter is pressed + const isSendMessage = isCtrlEnter && !isKeyboardEvent && event.inputType === "sendMessage"; - if (isEnterPress || isSendMessage) { + if (isEnterPress || isInsertParagraph || isSendMessage) { event.stopPropagation?.(); event.preventDefault?.(); onSend(); diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index 43dce76c7f..7353acb6b2 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -17,6 +17,7 @@ limitations under the License. import "@testing-library/jest-dom"; import React from "react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; @@ -87,6 +88,45 @@ describe("WysiwygComposer", () => { // Then it sends a message await waitFor(() => expect(onSend).toBeCalledTimes(1)); }); + + it("Should not call onSend when Shift+Enter is pressed ", async () => { + //When + await userEvent.type(screen.getByRole("textbox"), "{shift>}{enter}"); + + // Then it sends a message + await waitFor(() => expect(onSend).toBeCalledTimes(0)); + }); + + it("Should not call onSend when ctrl+Enter is pressed ", async () => { + //When + // Using userEvent.type or .keyboard wasn't working as expected in the case of ctrl+enter + fireEvent( + screen.getByRole("textbox"), + new KeyboardEvent("keydown", { + ctrlKey: true, + code: "Enter", + }), + ); + + // Then it sends a message + await waitFor(() => expect(onSend).toBeCalledTimes(0)); + }); + + it("Should not call onSend when alt+Enter is pressed ", async () => { + //When + await userEvent.type(screen.getByRole("textbox"), "{alt>}{enter}"); + + // Then it sends a message + await waitFor(() => expect(onSend).toBeCalledTimes(0)); + }); + + it("Should not call onSend when meta+Enter is pressed ", async () => { + //When + await userEvent.type(screen.getByRole("textbox"), "{meta>}{enter}"); + + // Then it sends a message + await waitFor(() => expect(onSend).toBeCalledTimes(0)); + }); }); describe("When settings require Ctrl+Enter to send", () => { From 8a2e3865316636a47974b1c9595f1e0034ada63a Mon Sep 17 00:00:00 2001 From: alunturner <56027671+alunturner@users.noreply.github.com> Date: Thu, 19 Jan 2023 09:24:29 +0000 Subject: [PATCH 55/92] Add disabled button state to rich text editor (#9930) * add disabled css state * conditionally apply disabled css state * hides disabled tooltips --- .../components/_FormattingButtons.pcss | 6 ++++ .../components/FormattingButtons.tsx | 28 ++++++++++--------- .../components/FormattingButtons-test.tsx | 27 ++++++++++++++++-- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss b/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss index fa8078279f..8e3dd22c99 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss @@ -50,6 +50,12 @@ limitations under the License. } } + .mx_FormattingButtons_disabled { + .mx_FormattingButtons_Icon { + color: $quinary-content; + } + } + .mx_FormattingButtons_Icon { --size: 16px; height: var(--size); diff --git a/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx index 7c1601b441..11c797d646 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { MouseEventHandler, ReactNode } from "react"; -import { FormattingFunctions, AllActionStates } from "@matrix-org/matrix-wysiwyg"; +import { FormattingFunctions, AllActionStates, ActionState } from "@matrix-org/matrix-wysiwyg"; import classNames from "classnames"; import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg"; @@ -53,21 +53,23 @@ function Tooltip({ label, keyCombo }: TooltipProps): JSX.Element { interface ButtonProps extends TooltipProps { icon: ReactNode; - isActive: boolean; + actionState: ActionState; onClick: MouseEventHandler; } -function Button({ label, keyCombo, onClick, isActive, icon }: ButtonProps): JSX.Element { +function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): JSX.Element { return ( void} title={label} className={classNames("mx_FormattingButtons_Button", { - mx_FormattingButtons_active: isActive, - mx_FormattingButtons_Button_hover: !isActive, + mx_FormattingButtons_active: actionState === "reversed", + mx_FormattingButtons_Button_hover: actionState === "enabled", + mx_FormattingButtons_disabled: actionState === "disabled", })} tooltip={keyCombo && } + forceHide={actionState === "disabled"} alignment={Alignment.Top} > {icon} @@ -85,53 +87,53 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP return (