{previewBar} @@ -2616,3 +2652,7 @@ export class RoomView extends React.Component { ); } } + +class CannotDetermineUserError extends Error { + public name = "CannotDetermineUserError"; +} diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index d1f3677816..0d41e56c92 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021-2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { + type JSX, type Dispatch, type KeyboardEvent, type KeyboardEventHandler, @@ -16,6 +17,7 @@ import React, { useCallback, useContext, useEffect, + useId, useMemo, useRef, useState, @@ -116,6 +118,7 @@ const Tile: React.FC = ({ const [showChildren, toggleShowChildren] = useStateToggle(true); const [onFocus, isActive, ref, nodeRef] = useRovingTabIndex(); const [busy, setBusy] = useState(false); + const checkboxLabelId = useId(); const onPreviewClick = (ev: ButtonEvent): void => { ev.preventDefault(); @@ -172,7 +175,14 @@ const Tile: React.FC = ({ let checkbox: ReactElement | undefined; if (onToggleClick) { if (hasPermissions) { - checkbox = ; + checkbox = ( + + ); } else { checkbox = ( = ({ ev.stopPropagation(); }} > - + ); } @@ -248,7 +263,7 @@ const Tile: React.FC = ({
{avatar}
- {name} + {name} {joinedSection} {suggestedSection}
@@ -330,11 +345,14 @@ const Tile: React.FC = ({ }; } + const shouldToggle = hasPermissions && onToggleClick; + return (
  • = ({ mx_SpaceHierarchy_subspace: room.room_type === RoomType.Space, mx_SpaceHierarchy_joining: busy, })} - onClick={hasPermissions && onToggleClick ? onToggleClick : onPreviewClick} + onClick={shouldToggle ? onToggleClick : onPreviewClick} onKeyDown={onKeyDown} ref={ref} onFocus={onFocus} @@ -619,7 +637,7 @@ const useIntersectionObserver = (callback: () => void): ((element: HTMLDivElemen } }; - const observerRef = useRef(); + const observerRef = useRef(undefined); return (element: HTMLDivElement) => { if (observerRef.current) { observerRef.current.disconnect(); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 170cb4615d..59ec657b02 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import { EventType, RoomType, JoinRule, Preset, type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; -import React, { useCallback, useContext, useRef, useState } from "react"; +import React, { type JSX, useCallback, useContext, useRef, useState } from "react"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import createRoom, { type IOpts } from "../../createRoom"; @@ -117,7 +117,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { <> => { e.preventDefault(); e.stopPropagation(); @@ -132,7 +132,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { {videoRoomsEnabled && ( => { e.preventDefault(); e.stopPropagation(); @@ -157,7 +157,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { )} { e.preventDefault(); e.stopPropagation(); @@ -168,7 +168,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { {canCreateSpace && ( { e.preventDefault(); e.stopPropagation(); diff --git a/src/components/structures/SplashPage.tsx b/src/components/structures/SplashPage.tsx index 7b419bbf47..86d78fcecc 100644 --- a/src/components/structures/SplashPage.tsx +++ b/src/components/structures/SplashPage.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react"; +import React, { type JSX, type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react"; interface Props extends DetailedHTMLProps, HTMLElement> { className?: string; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 0778d7c92f..b01f160551 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React, { type JSX } from "react"; import classNames from "classnames"; import { _t, type TranslationKey } from "../../languageHandler"; @@ -27,14 +27,14 @@ export class Tab { * @param {string} id The tab's ID. * @param {string} label The untranslated tab label. * @param {string|JSX.Element} icon An SVG element to use for the tab icon. Can also be a string for legacy icons, in which case it is the class for the tab icon. This should be a simple mask. - * @param {React.ReactNode} body The JSX for the tab container. + * @param {JSX.Element} body The JSX for the tab container. * @param {string} screenName The screen name to report to Posthog. */ public constructor( public readonly id: T, public readonly label: TranslationKey, public readonly icon: string | JSX.Element | null, - public readonly body: React.ReactNode, + public readonly body: JSX.Element, public readonly screenName?: ScreenName, ) {} } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 512a1156a5..c1c4b3ff57 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type KeyboardEvent } from "react"; +import React, { type JSX, createRef, type KeyboardEvent } from "react"; import { type Thread, THREAD_RELATION_TYPE, @@ -86,8 +86,8 @@ export default class ThreadView extends React.Component { // Set by setEventId in ctor. private eventId!: string; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.setEventId(this.props.mxEvent); const thread = this.props.room.getThread(this.eventId) ?? undefined; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index c41240079a..8346a0ab31 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -30,7 +30,7 @@ import { ThreadEvent, ReceiptType, } from "matrix-js-sdk/src/matrix"; -import { debounce, findLastIndex } from "lodash"; +import { debounce } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -73,25 +73,12 @@ const debuglog = (...args: any[]): void => { } }; -const overlaysBefore = (overlayEvent: MatrixEvent, mainEvent: MatrixEvent): boolean => - overlayEvent.localTimestamp < mainEvent.localTimestamp; - -const overlaysAfter = (overlayEvent: MatrixEvent, mainEvent: MatrixEvent): boolean => - overlayEvent.localTimestamp >= mainEvent.localTimestamp; - interface IProps { // The js-sdk EventTimelineSet object for the timeline sequence we are // representing. This may or may not have a room, depending on what it's // a timeline representing. If it has a room, we maintain RRs etc for // that room. timelineSet: EventTimelineSet; - // overlay events from a second timelineset on the main timeline - // added to support virtual rooms - // events from the overlay timeline set will be added by localTimestamp - // into the main timeline - overlayTimelineSet?: EventTimelineSet; - // filter events from overlay timeline - overlayTimelineSetFilter?: (event: MatrixEvent) => boolean; showReadReceipts?: boolean; // Enable managing RRs and RMs. These require the timelineSet to have a room. manageReadReceipts?: boolean; @@ -251,7 +238,6 @@ class TimelinePanel extends React.Component { private readonly messagePanel = createRef(); private dispatcherRef?: string; private timelineWindow?: TimelineWindow; - private overlayTimelineWindow?: TimelineWindow; private unmounted = false; private readReceiptActivityTimer: Timer | null = null; private readMarkerActivityTimer: Timer | null = null; @@ -349,16 +335,12 @@ class TimelinePanel extends React.Component { const differentEventId = prevProps.eventId != this.props.eventId; const differentHighlightedEventId = prevProps.highlightedEventId != this.props.highlightedEventId; const differentAvoidJump = prevProps.eventScrollIntoView && !this.props.eventScrollIntoView; - const differentOverlayTimeline = prevProps.overlayTimelineSet !== this.props.overlayTimelineSet; if (differentEventId || differentHighlightedEventId || differentAvoidJump) { logger.log( `TimelinePanel switching to eventId ${this.props.eventId} (was ${prevProps.eventId}), ` + `scrollIntoView: ${this.props.eventScrollIntoView} (was ${prevProps.eventScrollIntoView})`, ); this.initTimeline(this.props); - } else if (differentOverlayTimeline) { - logger.log(`TimelinePanel updating overlay timeline.`); - this.initTimeline(this.props); } } @@ -509,24 +491,9 @@ class TimelinePanel extends React.Component { // this particular event should be the first or last to be unpaginated. const eventId = scrollToken; - // The event in question could belong to either the main timeline or - // overlay timeline; let's check both const mainEvents = this.timelineWindow!.getEvents(); - const overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; - let marker = mainEvents.findIndex((ev) => ev.getId() === eventId); - let overlayMarker: number; - if (marker === -1) { - // The event must be from the overlay timeline instead - overlayMarker = overlayEvents.findIndex((ev) => ev.getId() === eventId); - marker = backwards - ? findLastIndex(mainEvents, (ev) => overlaysAfter(overlayEvents[overlayMarker], ev)) - : mainEvents.findIndex((ev) => overlaysBefore(overlayEvents[overlayMarker], ev)); - } else { - overlayMarker = backwards - ? findLastIndex(overlayEvents, (ev) => overlaysBefore(ev, mainEvents[marker])) - : overlayEvents.findIndex((ev) => overlaysAfter(ev, mainEvents[marker])); - } + const marker = mainEvents.findIndex((ev) => ev.getId() === eventId); // The number of events to unpaginate from the main timeline let count: number; @@ -536,24 +503,11 @@ class TimelinePanel extends React.Component { count = backwards ? marker + 1 : mainEvents.length - marker; } - // The number of events to unpaginate from the overlay timeline - let overlayCount: number; - if (overlayMarker === -1) { - overlayCount = 0; - } else { - overlayCount = backwards ? overlayMarker + 1 : overlayEvents.length - overlayMarker; - } - if (count > 0) { debuglog("Unpaginating", count, "in direction", dir); this.timelineWindow!.unpaginate(count, backwards); } - if (overlayCount > 0) { - debuglog("Unpaginating", count, "from overlay timeline in direction", dir); - this.overlayTimelineWindow!.unpaginate(overlayCount, backwards); - } - const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); this.setState({ @@ -610,10 +564,6 @@ class TimelinePanel extends React.Component { return false; } - if (this.overlayTimelineWindow) { - await this.extendOverlayWindowToCoverMainWindow(); - } - debuglog("paginate complete backwards:" + backwards + "; success:" + r); const { events, liveEvents } = this.getEvents(); @@ -705,10 +655,7 @@ class TimelinePanel extends React.Component { data: IRoomTimelineData, ): void => { // ignore events for other timeline sets - if ( - data.timeline.getTimelineSet() !== this.props.timelineSet && - data.timeline.getTimelineSet() !== this.props.overlayTimelineSet - ) { + if (data.timeline.getTimelineSet() !== this.props.timelineSet) { return; } @@ -748,69 +695,60 @@ class TimelinePanel extends React.Component { // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false) - .then(() => { - if (this.overlayTimelineWindow) { - return this.overlayTimelineWindow.paginate(EventTimeline.FORWARDS, 1, false); + this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false).then(() => { + if (this.unmounted) { + return; + } + + const { events, liveEvents } = this.getEvents(); + this.buildLegacyCallEventGroupers(events); + const lastLiveEvent = liveEvents[liveEvents.length - 1]; + + const updatedState: Partial = { + events, + liveEvents, + }; + + let callRMUpdated = false; + if (this.props.manageReadMarkers) { + // when a new event arrives when the user is not watching the + // window, but the window is in its auto-scroll mode, make sure the + // read marker is visible. + // + // We ignore events we have sent ourselves; we don't want to see the + // read-marker when a remote echo of an event we have just sent takes + // more than the timeout on userActiveRecently. + // + const myUserId = MatrixClientPeg.safeGet().credentials.userId; + callRMUpdated = false; + if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) { + updatedState.readMarkerVisible = true; + } else if (lastLiveEvent && this.getReadMarkerPosition() === 0) { + // we know we're stuckAtBottom, so we can advance the RM + // immediately, to save a later render cycle + + this.setReadMarker(lastLiveEvent.getId() ?? null, lastLiveEvent.getTs(), true); + updatedState.readMarkerVisible = false; + updatedState.readMarkerEventId = lastLiveEvent.getId(); + callRMUpdated = true; } - }) - .then(() => { - if (this.unmounted) { - return; + } + + this.setState(updatedState as IState, () => { + this.messagePanel.current?.updateTimelineMinHeight(); + if (callRMUpdated) { + this.props.onReadMarkerUpdated?.(); } - - const { events, liveEvents } = this.getEvents(); - this.buildLegacyCallEventGroupers(events); - const lastLiveEvent = liveEvents[liveEvents.length - 1]; - - const updatedState: Partial = { - events, - liveEvents, - }; - - let callRMUpdated = false; - if (this.props.manageReadMarkers) { - // when a new event arrives when the user is not watching the - // window, but the window is in its auto-scroll mode, make sure the - // read marker is visible. - // - // We ignore events we have sent ourselves; we don't want to see the - // read-marker when a remote echo of an event we have just sent takes - // more than the timeout on userActiveRecently. - // - const myUserId = MatrixClientPeg.safeGet().credentials.userId; - callRMUpdated = false; - if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) { - updatedState.readMarkerVisible = true; - } else if (lastLiveEvent && this.getReadMarkerPosition() === 0) { - // we know we're stuckAtBottom, so we can advance the RM - // immediately, to save a later render cycle - - this.setReadMarker(lastLiveEvent.getId() ?? null, lastLiveEvent.getTs(), true); - updatedState.readMarkerVisible = false; - updatedState.readMarkerEventId = lastLiveEvent.getId(); - callRMUpdated = true; - } - } - - this.setState(updatedState as IState, () => { - this.messagePanel.current?.updateTimelineMinHeight(); - if (callRMUpdated) { - this.props.onReadMarkerUpdated?.(); - } - }); }); + }); }; private hasTimelineSetFor(roomId: string | undefined): boolean { - return ( - (roomId !== undefined && roomId === this.props.timelineSet.room?.roomId) || - roomId === this.props.overlayTimelineSet?.room?.roomId - ); + return roomId !== undefined && roomId === this.props.timelineSet.room?.roomId; } private onRoomTimelineReset = (room: Room | undefined, timelineSet: EventTimelineSet): void => { - if (timelineSet !== this.props.timelineSet && timelineSet !== this.props.overlayTimelineSet) return; + if (timelineSet !== this.props.timelineSet) return; if (this.canResetTimeline()) { this.loadTimeline(); @@ -1475,48 +1413,6 @@ class TimelinePanel extends React.Component { }); } - private async extendOverlayWindowToCoverMainWindow(): Promise { - const mainWindow = this.timelineWindow!; - const overlayWindow = this.overlayTimelineWindow!; - const mainEvents = mainWindow.getEvents(); - - if (mainEvents.length > 0) { - let paginationRequests: Promise[]; - - // Keep paginating until the main window is covered - do { - paginationRequests = []; - const overlayEvents = overlayWindow.getEvents(); - - if ( - overlayWindow.canPaginate(EventTimeline.BACKWARDS) && - (overlayEvents.length === 0 || - overlaysAfter(overlayEvents[0], mainEvents[0]) || - !mainWindow.canPaginate(EventTimeline.BACKWARDS)) - ) { - // Paginating backwards could reveal more events to be overlaid in the main window - paginationRequests.push( - this.onPaginationRequest(overlayWindow, EventTimeline.BACKWARDS, PAGINATE_SIZE), - ); - } - - if ( - overlayWindow.canPaginate(EventTimeline.FORWARDS) && - (overlayEvents.length === 0 || - overlaysBefore(overlayEvents.at(-1)!, mainEvents.at(-1)!) || - !mainWindow.canPaginate(EventTimeline.FORWARDS)) - ) { - // Paginating forwards could reveal more events to be overlaid in the main window - paginationRequests.push( - this.onPaginationRequest(overlayWindow, EventTimeline.FORWARDS, PAGINATE_SIZE), - ); - } - - await Promise.all(paginationRequests); - } while (paginationRequests.length > 0); - } - } - /** * (re)-load the event timeline, and initialise the scroll state, centered * around the given event. @@ -1536,9 +1432,6 @@ class TimelinePanel extends React.Component { private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void { const cli = MatrixClientPeg.safeGet(); this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap }); - this.overlayTimelineWindow = this.props.overlayTimelineSet - ? new TimelineWindow(cli, this.props.overlayTimelineSet, { windowLimit: this.props.timelineCap }) - : undefined; const onLoaded = (): void => { if (this.unmounted) return; @@ -1554,14 +1447,8 @@ class TimelinePanel extends React.Component { this.setState( { - canBackPaginate: - (this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS) || - this.overlayTimelineWindow?.canPaginate(EventTimeline.BACKWARDS)) ?? - false, - canForwardPaginate: - (this.timelineWindow?.canPaginate(EventTimeline.FORWARDS) || - this.overlayTimelineWindow?.canPaginate(EventTimeline.FORWARDS)) ?? - false, + canBackPaginate: this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS) ?? false, + canForwardPaginate: this.timelineWindow?.canPaginate(EventTimeline.FORWARDS) ?? false, timelineLoading: false, }, () => { @@ -1618,11 +1505,13 @@ class TimelinePanel extends React.Component { description = _t("timeline|load_error|unable_to_find"); } - Modal.createDialog(ErrorDialog, { + const { finished } = Modal.createDialog(ErrorDialog, { title: _t("timeline|load_error|title"), description, - onFinished, }); + if (onFinished) { + finished.then(onFinished); + } }; // if we already have the event in question, TimelineWindow.load @@ -1636,7 +1525,7 @@ class TimelinePanel extends React.Component { // This is a hot-path optimization by skipping a promise tick // by repeating a no-op sync branch in // TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline - if (this.props.timelineSet.getTimelineForEvent(eventId) && !this.overlayTimelineWindow) { + if (this.props.timelineSet.getTimelineForEvent(eventId)) { // if we've got an eventId, and the timeline exists, we can skip // the promise tick. this.timelineWindow.load(eventId, INITIAL_SIZE); @@ -1645,14 +1534,7 @@ class TimelinePanel extends React.Component { return; } - const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise => { - if (this.overlayTimelineWindow) { - // TODO: use timestampToEvent to load the overlay timeline - // with more correct position when main TL eventId is truthy - await this.overlayTimelineWindow.load(undefined, INITIAL_SIZE); - await this.extendOverlayWindowToCoverMainWindow(); - } - }); + const prom = this.timelineWindow.load(eventId, INITIAL_SIZE); this.buildLegacyCallEventGroupers(); this.setState({ events: [], @@ -1683,38 +1565,9 @@ class TimelinePanel extends React.Component { this.reloadEvents(); } - // get the list of events from the timeline windows and the pending event list + // get the list of events from the timeline window and the pending event list private getEvents(): Pick { - const mainEvents = this.timelineWindow!.getEvents(); - let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; - if (this.props.overlayTimelineSetFilter !== undefined) { - overlayEvents = overlayEvents.filter(this.props.overlayTimelineSetFilter); - } - - // maintain the main timeline event order as returned from the HS - // merge overlay events at approximately the right position based on local timestamp - const events = overlayEvents.reduce( - (acc: MatrixEvent[], overlayEvent: MatrixEvent) => { - // find the first main tl event with a later timestamp - const index = acc.findIndex((event) => overlaysBefore(overlayEvent, event)); - // insert overlay event into timeline at approximately the right place - // if it's beyond the edge of the main window, hide it so that expanding - // the main window doesn't cause new events to pop in and change its position - if (index === -1) { - if (!this.timelineWindow!.canPaginate(EventTimeline.FORWARDS)) { - acc.push(overlayEvent); - } - } else if (index === 0) { - if (!this.timelineWindow!.canPaginate(EventTimeline.BACKWARDS)) { - acc.unshift(overlayEvent); - } - } else { - acc.splice(index, 0, overlayEvent); - } - return acc; - }, - [...mainEvents], - ); + const events = this.timelineWindow!.getEvents(); // We want the last event to be decrypted first const client = MatrixClientPeg.safeGet(); diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 43c12d9cf6..649cd1dc59 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React from "react"; import classNames from "classnames"; import { Text } from "@vector-im/compound-web"; import { type EmptyObject } from "matrix-js-sdk/src/matrix"; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 08a519e182..67af61f7ac 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type ReactNode } from "react"; +import React, { type JSX, createRef, type ReactNode } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -81,10 +81,10 @@ export default class UserMenu extends React.Component { private dispatcherRef?: string; private themeWatcherRef?: string; private readonly dndWatcherRef?: string; - private buttonRef: React.RefObject = createRef(); + private buttonRef = createRef(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { contextMenuPosition: null, @@ -370,6 +370,13 @@ export default class UserMenu extends React.Component { ? toRightOf(this.state.contextMenuPosition) : below(this.state.contextMenuPosition); + const userIdentifierString = UserIdentifierCustomisations.getDisplayUserIdentifier( + MatrixClientPeg.safeGet().getSafeUserId(), + { + withDisplayName: true, + }, + ); + return (
    @@ -377,13 +384,8 @@ export default class UserMenu extends React.Component { {OwnProfileStore.instance.displayName} - - {UserIdentifierCustomisations.getDisplayUserIdentifier( - MatrixClientPeg.safeGet().getSafeUserId(), - { - withDisplayName: true, - }, - )} + + {userIdentifierString}
    diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index be947d9a4b..58aed9932b 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -34,8 +34,8 @@ export default class UserView extends React.Component { public static contextType = MatrixClientContext; declare public context: React.ContextType; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { loading: true, }; diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index 66bbbe4c0b..b2e003284b 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import SyntaxHighlight from "../views/elements/SyntaxHighlight"; diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 0112da4cc0..3983b286f1 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -21,7 +21,7 @@ import SdkConfig from "../../SdkConfig"; import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface Props { - roomView: RefObject; + roomView: RefObject; resizeNotifier: ResizeNotifier; inviteEvent: MatrixEvent; } diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index a0f2be8836..5bade7b24a 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -78,9 +78,6 @@ export default class CompleteSecurity extends React.Component { } else if (phase === Phase.Busy) { icon = ; title = _t("encryption|verification|after_new_login|verify_this_device"); - } else if (phase === Phase.ConfirmReset) { - icon = ; - title = _t("encryption|verification|after_new_login|reset_confirmation"); } else if (phase === Phase.Finished) { // SetupEncryptionBody will take care of calling onFinished, we don't need to do anything } else { @@ -90,7 +87,7 @@ export default class CompleteSecurity extends React.Component { const forceVerification = SdkConfig.get("force_verification"); let skipButton; - if (!forceVerification && (phase === Phase.Intro || phase === Phase.ConfirmReset)) { + if (!forceVerification && phase === Phase.Intro) { skipButton = ( { label={_td("auth|change_password_new_label")} value={this.state.password} minScore={PASSWORD_MIN_SCORE} - fieldRef={(field) => (this.fieldPassword = field)} + fieldRef={(field) => { + this.fieldPassword = field; + }} onChange={this.onInputChanged.bind(this, "password")} autoComplete="new-password" /> @@ -399,7 +401,9 @@ export default class ForgotPassword extends React.Component { labelInvalid={_td("auth|reset_password|passwords_mismatch")} value={this.state.password2} password={this.state.password} - fieldRef={(field) => (this.fieldPasswordConfirm = field)} + fieldRef={(field) => { + this.fieldPasswordConfirm = field; + }} onChange={this.onInputChanged.bind(this, "password2")} autoComplete="new-password" /> diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 894799b566..9aca0046f2 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix"; diff --git a/src/components/structures/auth/LoginSplashView.tsx b/src/components/structures/auth/LoginSplashView.tsx index 62e5734e19..aae7980185 100644 --- a/src/components/structures/auth/LoginSplashView.tsx +++ b/src/components/structures/auth/LoginSplashView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; @@ -43,13 +43,13 @@ type MigrationState = { /** * The view that is displayed after we have logged in, before the first /sync is completed. */ -export function LoginSplashView(props: Props): React.JSX.Element { +export function LoginSplashView(props: Props): JSX.Element { const migrationState = useTypedEventEmitterState( props.matrixClient, CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress?: number, total?: number): MigrationState => ({ progress: progress ?? -1, totalSteps: total ?? -1 }), ); - let errorBox: React.JSX.Element | undefined; + let errorBox: JSX.Element | undefined; if (props.syncError) { errorBox =
    {messageForSyncError(props.syncError)}
    ; } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 496c687bbf..a5c713b3ea 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -20,7 +20,7 @@ import { SSOAction, type RegisterResponse, } from "matrix-js-sdk/src/matrix"; -import React, { Fragment, type ReactNode } from "react"; +import React, { type JSX, Fragment, type ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/structures/auth/SessionLockStolenView.tsx b/src/components/structures/auth/SessionLockStolenView.tsx index 01193440fc..bb95f1b44a 100644 --- a/src/components/structures/auth/SessionLockStolenView.tsx +++ b/src/components/structures/auth/SessionLockStolenView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import SplashPage from "../SplashPage"; import { _t } from "../../../languageHandler"; diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index e290292ea6..b76a623e2c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -1,12 +1,12 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2020, 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; @@ -19,6 +19,7 @@ import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStor import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; import Spinner from "../../views/elements/Spinner"; +import { ResetIdentityDialog } from "../../views/dialogs/ResetIdentityDialog"; function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean { return Boolean(keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations); @@ -89,14 +90,15 @@ export default class SetupEncryptionBody extends React.Component // We need to call onFinished now to close this dialog, and // again later to signal that the verification is complete. this.props.onFinished(); - Modal.createDialog(VerificationRequestDialog, { + const { finished: verificationFinished } = Modal.createDialog(VerificationRequestDialog, { verificationRequestPromise: requestPromise, member: cli.getUser(userId) ?? undefined, - onFinished: async (): Promise => { - const request = await requestPromise; - request.cancel(); - this.props.onFinished(); - }, + }); + + verificationFinished.then(async () => { + const request = await requestPromise; + request.cancel(); + this.props.onFinished(); }); }; @@ -112,19 +114,15 @@ export default class SetupEncryptionBody extends React.Component private onResetClick = (ev: ButtonEvent): void => { ev.preventDefault(); - const store = SetupEncryptionStore.sharedInstance(); - store.reset(); - }; - - private onResetConfirmClick = (): void => { - this.props.onFinished(); - const store = SetupEncryptionStore.sharedInstance(); - store.resetConfirm(); - }; - - private onResetBackClick = (): void => { - const store = SetupEncryptionStore.sharedInstance(); - store.returnAfterReset(); + Modal.createDialog(ResetIdentityDialog, { + onReset: () => { + // The user completed the reset process - close this dialog + this.props.onFinished(); + const store = SetupEncryptionStore.sharedInstance(); + store.done(); + }, + variant: "confirm", + }); }; private onDoneClick = (): void => { @@ -157,7 +155,7 @@ export default class SetupEncryptionBody extends React.Component

    {_t("encryption|verification|no_key_or_device")}

    - + {_t("encryption|verification|reset_proceed_prompt")}
    @@ -246,22 +244,6 @@ export default class SetupEncryptionBody extends React.Component