{previewBar} diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 413d081746..a92b24fc55 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -6,14 +6,14 @@ 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, CSSProperties, ReactNode } from "react"; +import React, { createRef, type CSSProperties, type ReactNode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; import Timer from "../../utils/Timer"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; -import ResizeNotifier from "../../utils/ResizeNotifier"; +import type ResizeNotifier from "../../utils/ResizeNotifier"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; // The amount of extra scroll distance to allow prior to unfilling. diff --git a/src/components/structures/SearchBox.tsx b/src/components/structures/SearchBox.tsx index a5bbb7865e..ec85314349 100644 --- a/src/components/structures/SearchBox.tsx +++ b/src/components/structures/SearchBox.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, { createRef, HTMLProps } from "react"; +import React, { createRef, type HTMLProps } from "react"; import { throttle } from "lodash"; import classNames from "classnames"; diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 362fe82dce..b0ee9e99d2 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,42 +7,43 @@ Please see LICENSE files in the repository root for full details. */ import React, { - Dispatch, - KeyboardEvent, - KeyboardEventHandler, - ReactElement, - ReactNode, - SetStateAction, + type Dispatch, + type KeyboardEvent, + type KeyboardEventHandler, + type ReactElement, + type ReactNode, + type SetStateAction, useCallback, useContext, useEffect, + useId, useMemo, useRef, useState, } from "react"; import { - Room, + type Room, RoomEvent, ClientEvent, - MatrixClient, + type MatrixClient, MatrixError, EventType, RoomType, GuestAccess, HistoryVisibility, - HierarchyRelation, - HierarchyRoom, + type HierarchyRelation, + type HierarchyRoom, JoinRule, } from "matrix-js-sdk/src/matrix"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import classNames from "classnames"; import { sortBy, uniqBy } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; -import { KnownMembership, SpaceChildEventContent } from "matrix-js-sdk/src/types"; +import { KnownMembership, type SpaceChildEventContent } from "matrix-js-sdk/src/types"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; -import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import SearchBox from "./SearchBox"; import RoomAvatar from "../views/avatars/RoomAvatar"; @@ -56,13 +57,13 @@ import { getChildOrder } from "../../stores/spaces/SpaceStore"; import { Linkify, topicToHtml } from "../../HtmlUtils"; import { useDispatcher } from "../../hooks/useDispatcher"; import { Action } from "../../dispatcher/actions"; -import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex"; +import { type IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { IOOBData } from "../../stores/ThreepidInviteStore"; +import { type IOOBData } from "../../stores/ThreepidInviteStore"; import { awaitRoomDownSync } from "../../utils/RoomUpgrade"; -import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; -import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload"; +import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import { type JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { getTopic } from "../../hooks/room/useTopic"; @@ -116,6 +117,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 +174,14 @@ const Tile: React.FC = ({ let checkbox: ReactElement | undefined; if (onToggleClick) { if (hasPermissions) { - checkbox = ; + checkbox = ( + + ); } else { checkbox = ( = ({ ev.stopPropagation(); }} > - + ); } @@ -248,7 +262,7 @@ const Tile: React.FC = ({
{avatar}
- {name} + {name} {joinedSection} {suggestedSection}
@@ -330,11 +344,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} diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 1424df2c98..bb4fe19520 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -6,18 +6,18 @@ 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 { EventType, RoomType, JoinRule, Preset, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +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 MatrixClientContext from "../../contexts/MatrixClientContext"; -import createRoom, { IOpts } from "../../createRoom"; +import createRoom, { type IOpts } from "../../createRoom"; import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; import { Action } from "../../dispatcher/actions"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import { type ActionPayload } from "../../dispatcher/payloads"; +import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import * as Email from "../../email"; import { useEventEmitterState } from "../../hooks/useEventEmitter"; import { useMyRoomMembership } from "../../hooks/useRoomMembers"; @@ -30,7 +30,7 @@ import { UIComponent } from "../../settings/UIFeature"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; -import ResizeNotifier from "../../utils/ResizeNotifier"; +import type ResizeNotifier from "../../utils/ResizeNotifier"; import { shouldShowSpaceInvite, shouldShowSpaceSettings, @@ -51,7 +51,7 @@ import { defaultDmsRenderer, defaultRoomsRenderer, } from "../views/dialogs/AddExistingToSpaceDialog"; -import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../views/elements/AccessibleButton"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import Field from "../views/elements/Field"; import RoomFacePile from "../views/elements/RoomFacePile"; @@ -65,7 +65,7 @@ import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu"; import MainSplit from "./MainSplit"; import RightPanel from "./RightPanel"; import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; -import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; +import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; interface IProps { space: Room; @@ -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 1ce0724dab..7b419bbf47 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, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react"; +import React, { 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 71161fec52..0778d7c92f 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -11,10 +11,10 @@ Please see LICENSE files in the repository root for full details. import * as React from "react"; import classNames from "classnames"; -import { _t, TranslationKey } from "../../languageHandler"; +import { _t, type TranslationKey } from "../../languageHandler"; import AutoHideScrollbar from "./AutoHideScrollbar"; -import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers"; -import { NonEmptyArray } from "../../@types/common"; +import { PosthogScreenTracker, type ScreenName } from "../../PosthogTrackers"; +import { type NonEmptyArray } from "../../@types/common"; import { RovingAccessibleButton, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; import { useWindowWidth } from "../../hooks/useWindowWidth"; diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 7aee8554b1..892dc82c75 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -6,16 +6,16 @@ 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 { Optional } from "matrix-events-sdk"; +import { type Optional } from "matrix-events-sdk"; import React, { useContext, useEffect, useRef, useState } from "react"; -import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix"; +import { type EventTimelineSet, type Room, Thread } from "matrix-js-sdk/src/matrix"; import { IconButton, Tooltip } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads"; import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg"; import BaseCard from "../views/right_panel/BaseCard"; -import ResizeNotifier from "../../utils/ResizeNotifier"; +import type ResizeNotifier from "../../utils/ResizeNotifier"; import MatrixClientContext, { useMatrixClientContext } from "../../contexts/MatrixClientContext"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; @@ -23,10 +23,10 @@ import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./Conte import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import TimelinePanel from "./TimelinePanel"; import { Layout } from "../../settings/enums/Layout"; -import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; +import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import Measured from "../views/elements/Measured"; import PosthogTrackers from "../../PosthogTrackers"; -import { ButtonEvent } from "../views/elements/AccessibleButton"; +import { type ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import { clearRoomNotification } from "../../utils/notifications"; import EmptyState from "../views/right_panel/EmptyState"; @@ -129,10 +129,10 @@ export const ThreadPanelHeader: React.FC<{ ); return ( -
    +
    - - + +
    @@ -192,9 +192,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => narrow={narrow} > - } + header={_t("common|threads")} id="thread-panel" className="mx_ThreadPanel" ariaLabelledBy="thread-panel-tab" @@ -204,7 +202,8 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => ref={card} closeButtonRef={closeButonRef} > - {card.current && } + {hasThreads && } + {timelineSet ? ( { // 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; @@ -443,7 +443,7 @@ export default class ThreadView extends React.Component { PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev); }} > - {this.card.current && } +
    {timeline}
    {ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && ( diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 31ce65cfa7..c41240079a 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -6,25 +6,25 @@ 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, ReactNode } from "react"; +import React, { createRef, type ReactNode } from "react"; import { - Room, + type Room, RoomEvent, - RoomMember, + type RoomMember, RoomMemberEvent, - MatrixEvent, + type MatrixEvent, MatrixEventEvent, - EventTimelineSet, - IRoomTimelineData, + type EventTimelineSet, + type IRoomTimelineData, Direction, EventTimeline, EventType, - RelationType, + type RelationType, ClientEvent, - MatrixClient, - Relations, - MatrixError, - SyncState, + type MatrixClient, + type Relations, + type MatrixError, + type SyncState, TimelineWindow, Thread, ThreadEvent, @@ -34,7 +34,7 @@ import { debounce, findLastIndex } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; -import { Layout } from "../../settings/enums/Layout"; +import { type Layout } from "../../settings/enums/Layout"; import { _t } from "../../languageHandler"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; @@ -45,15 +45,16 @@ import { Action } from "../../dispatcher/actions"; import Timer from "../../utils/Timer"; import shouldHideEvent from "../../shouldHideEvent"; import MessagePanel from "./MessagePanel"; -import { IScrollState } from "./ScrollPanel"; -import { ActionPayload } from "../../dispatcher/payloads"; -import ResizeNotifier from "../../utils/ResizeNotifier"; -import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; +import { type IScrollState } from "./ScrollPanel"; +import { type ActionPayload } from "../../dispatcher/payloads"; +import type ResizeNotifier from "../../utils/ResizeNotifier"; +import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import Spinner from "../views/elements/Spinner"; -import EditorStateTransfer from "../../utils/EditorStateTransfer"; +import type EditorStateTransfer from "../../utils/EditorStateTransfer"; import ErrorDialog from "../views/dialogs/ErrorDialog"; -import LegacyCallEventGrouper, { buildLegacyCallEventGroupers } from "./LegacyCallEventGrouper"; -import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import type LegacyCallEventGrouper from "./LegacyCallEventGrouper"; +import { buildLegacyCallEventGroupers } from "./LegacyCallEventGrouper"; +import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { haveRendererForEvent } from "../../events/EventTileFactory"; diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 02db99a0e0..649cd1dc59 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -6,19 +6,20 @@ 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"; -import ToastStore, { IToast } from "../../stores/ToastStore"; +import ToastStore, { type IToast } from "../../stores/ToastStore"; interface IState { toasts: IToast[]; countSeen: number; } -export default class ToastContainer extends React.Component<{}, IState> { - public constructor(props: {}) { +export default class ToastContainer extends React.Component { + public constructor(props: EmptyObject) { super(props); this.state = { toasts: ToastStore.sharedInstance().getToasts(), diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index 7618862798..a28ff2ba44 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -7,18 +7,18 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { Room, IEventRelation } from "matrix-js-sdk/src/matrix"; -import { Optional } from "matrix-events-sdk"; +import { type Room, type IEventRelation } from "matrix-js-sdk/src/matrix"; +import { type Optional } from "matrix-events-sdk"; import ContentMessages from "../../ContentMessages"; import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import { Action } from "../../dispatcher/actions"; import ProgressBar from "../views/elements/ProgressBar"; -import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import { RoomUpload } from "../../models/RoomUpload"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { UploadPayload } from "../../dispatcher/payloads/UploadPayload"; +import AccessibleButton, { type ButtonEvent } from "../views/elements/AccessibleButton"; +import { type RoomUpload } from "../../models/RoomUpload"; +import { type ActionPayload } from "../../dispatcher/payloads"; +import { type UploadPayload } from "../../dispatcher/payloads/UploadPayload"; import { fileSize } from "../../utils/FileUtils"; interface IProps { diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index e266352393..e2dc5bc5b8 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -6,24 +6,24 @@ 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, ReactNode } from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; +import React, { createRef, type ReactNode } from "react"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import { ActionPayload } from "../../dispatcher/payloads"; +import { type ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; -import { ChevronFace, ContextMenuButton, MenuProps } from "./ContextMenu"; +import { ChevronFace, ContextMenuButton, type MenuProps } from "./ContextMenu"; import { UserTab } from "../views/dialogs/UserTab"; -import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; +import { type OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; import LogoutDialog, { shouldShowLogoutDialog } from "../views/dialogs/LogoutDialog"; import SettingsStore from "../../settings/SettingsStore"; import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme"; import { RovingAccessibleButton } from "../../accessibility/RovingTabIndex"; -import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../views/elements/AccessibleButton"; import SdkConfig from "../../SdkConfig"; import { getHomePageUrl } from "../../utils/pages"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; @@ -39,7 +39,7 @@ import SpaceStore from "../../stores/spaces/SpaceStore"; import { UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import PosthogTrackers from "../../PosthogTrackers"; -import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; +import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { SDKContext } from "../../contexts/SDKContext"; import { shouldShowFeedback } from "../../utils/Feedback"; import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg"; @@ -83,8 +83,8 @@ export default class UserMenu extends React.Component { private readonly dndWatcherRef?: string; private buttonRef: React.RefObject = 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 f71a6331d9..58aed9932b 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixEvent, RoomMember, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, RoomMember, type MatrixClient } from "matrix-js-sdk/src/matrix"; import Modal from "../../Modal"; import { _t } from "../../languageHandler"; @@ -15,9 +15,9 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import MainSplit from "./MainSplit"; import RightPanel from "./RightPanel"; import Spinner from "../views/elements/Spinner"; -import ResizeNotifier from "../../utils/ResizeNotifier"; +import type ResizeNotifier from "../../utils/ResizeNotifier"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; -import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage"; +import HomePage from "./HomePage.tsx"; import MatrixClientContext from "../../contexts/MatrixClientContext"; interface IProps { @@ -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, }; @@ -93,7 +93,7 @@ export default class UserView extends React.Component { defaultSize={420} analyticsRoomType="user_profile" > - + ); } else { diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index eca37842d7..66bbbe4c0b 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import SyntaxHighlight from "../views/elements/SyntaxHighlight"; import { _t } from "../../languageHandler"; diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 7787a03bf2..0112da4cc0 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -6,12 +6,12 @@ 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, { RefObject } from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import React, { type RefObject } from "react"; +import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import ResizeNotifier from "../../utils/ResizeNotifier"; +import type ResizeNotifier from "../../utils/ResizeNotifier"; import ErrorBoundary from "../views/elements/ErrorBoundary"; -import RoomHeader from "../views/rooms/RoomHeader"; +import RoomHeader from "../views/rooms/RoomHeader/RoomHeader.tsx"; import ScrollPanel from "./ScrollPanel"; import EventTileBubble from "../views/messages/EventTileBubble"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index ecf888f6e9..9e25f3349c 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.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 React, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { sleep } from "matrix-js-sdk/src/utils"; import { LockSolidIcon, CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -23,11 +23,11 @@ import AuthHeader from "../../views/auth/AuthHeader"; import AuthBody from "../../views/auth/AuthBody"; import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField"; import StyledCheckbox from "../../views/elements/StyledCheckbox"; -import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; +import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import QuestionDialog from "../../views/dialogs/QuestionDialog"; import { EnterEmail } from "./forgot-password/EnterEmail"; import { CheckEmail } from "./forgot-password/CheckEmail"; -import Field from "../../views/elements/Field"; +import type Field from "../../views/elements/Field"; import { ErrorMessage } from "../ErrorMessage"; import { VerifyEmailModal } from "./forgot-password/VerifyEmailModal"; import Spinner from "../../views/elements/Spinner"; diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index b4b41f0515..894799b566 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -6,20 +6,20 @@ 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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix"; +import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix"; import { _t, UserFriendlyError } from "../../../languageHandler"; -import Login, { ClientLoginFlow, OidcNativeFlow } from "../../../Login"; +import Login, { type ClientLoginFlow, type OidcNativeFlow } from "../../../Login"; import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import AuthPage from "../../views/auth/AuthPage"; import PlatformPeg from "../../../PlatformPeg"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; -import { IMatrixClientCreds } from "../../../MatrixClientPeg"; +import { type IMatrixClientCreds } from "../../../MatrixClientPeg"; import PasswordLogin from "../../views/auth/PasswordLogin"; import InlineSpinner from "../../views/elements/InlineSpinner"; import Spinner from "../../views/elements/Spinner"; @@ -27,8 +27,8 @@ import SSOButtons from "../../views/elements/SSOButtons"; import ServerPicker from "../../views/elements/ServerPicker"; import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; -import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; -import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; +import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; +import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { filterBoolean } from "../../../utils/arrays"; import { startOidcLogin } from "../../../utils/oidc/authorize"; diff --git a/src/components/structures/auth/LoginSplashView.tsx b/src/components/structures/auth/LoginSplashView.tsx index 3d68a12e8d..62e5734e19 100644 --- a/src/components/structures/auth/LoginSplashView.tsx +++ b/src/components/structures/auth/LoginSplashView.tsx @@ -7,13 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { messageForSyncError } from "../../../utils/ErrorUtils"; import Spinner from "../../views/elements/Spinner"; import ProgressBar from "../../views/elements/ProgressBar"; -import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; import { _t } from "../../../languageHandler"; import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; import SdkConfig from "../../../SdkConfig"; diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 1dc6f57bc7..496c687bbf 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -9,18 +9,18 @@ Please see LICENSE files in the repository root for full details. import { AuthType, createClient, - IAuthData, - AuthDict, - IInputs, + type IAuthData, + type AuthDict, + type IInputs, MatrixError, - IRegisterRequestParams, - IRequestTokenResponse, - MatrixClient, - SSOFlow, + type IRegisterRequestParams, + type IRequestTokenResponse, + type MatrixClient, + type SSOFlow, SSOAction, - RegisterResponse, + type RegisterResponse, } from "matrix-js-sdk/src/matrix"; -import React, { Fragment, ReactNode } from "react"; +import React, { Fragment, type ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; @@ -28,22 +28,22 @@ import { _t } from "../../../languageHandler"; import { adminContactStrings, messageForResourceLimitError, resourceLimitStrings } from "../../../utils/ErrorUtils"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import * as Lifecycle from "../../../Lifecycle"; -import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; +import { type IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; import AuthPage from "../../views/auth/AuthPage"; -import Login, { OidcNativeFlow } from "../../../Login"; +import Login, { type OidcNativeFlow } from "../../../Login"; import dis from "../../../dispatcher/dispatcher"; import SSOButtons from "../../views/elements/SSOButtons"; import ServerPicker from "../../views/elements/ServerPicker"; import RegistrationForm from "../../views/auth/RegistrationForm"; -import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; -import InteractiveAuth, { InteractiveAuthCallback } from "../InteractiveAuth"; +import InteractiveAuth, { type InteractiveAuthCallback } from "../InteractiveAuth"; import Spinner from "../../views/elements/Spinner"; import { AuthHeaderDisplay } from "./header/AuthHeaderDisplay"; import { AuthHeaderProvider } from "./header/AuthHeaderProvider"; import SettingsStore from "../../../settings/SettingsStore"; -import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; +import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { startOidcLogin } from "../../../utils/oidc/authorize"; const debuglog = (...args: any[]): void => { diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 1ff0ad4120..e290292ea6 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { KeyBackupInfo, VerificationRequest } from "matrix-js-sdk/src/crypto-api"; +import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; -import { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; +import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -17,7 +17,7 @@ import Modal from "../../../Modal"; import VerificationRequestDialog from "../../views/dialogs/VerificationRequestDialog"; import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStore"; import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; -import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; import Spinner from "../../views/elements/Spinner"; function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean { diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 696edc0ad2..4990a978b4 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -6,16 +6,16 @@ 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, { ChangeEvent, SyntheticEvent } from "react"; +import React, { type ChangeEvent, type SyntheticEvent } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { Optional } from "matrix-events-sdk"; -import { LoginFlow, MatrixError, SSOAction, SSOFlow } from "matrix-js-sdk/src/matrix"; +import { type Optional } from "matrix-events-sdk"; +import { type LoginFlow, MatrixError, SSOAction, type SSOFlow } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; import * as Lifecycle from "../../../Lifecycle"; import Modal from "../../../Modal"; -import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; +import { type IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; import { sendLoginRequest } from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform"; @@ -66,8 +66,8 @@ export default class SoftLogout extends React.Component { public static contextType = SDKContext; declare public context: React.ContextType; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { loginView: LoginView.Loading, diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index 428bc17026..64036507de 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import { Tooltip } from "@vector-im/compound-web"; import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; diff --git a/src/components/structures/auth/forgot-password/EnterEmail.tsx b/src/components/structures/auth/forgot-password/EnterEmail.tsx index d50040552d..9e7d6ae5a6 100644 --- a/src/components/structures/auth/forgot-password/EnterEmail.tsx +++ b/src/components/structures/auth/forgot-password/EnterEmail.tsx @@ -6,15 +6,15 @@ 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, { ReactNode, useRef } from "react"; +import React, { type ReactNode, useRef } from "react"; import { EmailSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t, _td } from "../../../../languageHandler"; import EmailField from "../../../views/auth/EmailField"; import { ErrorMessage } from "../../ErrorMessage"; import Spinner from "../../../views/elements/Spinner"; -import Field from "../../../views/elements/Field"; -import AccessibleButton, { ButtonEvent } from "../../../views/elements/AccessibleButton"; +import type Field from "../../../views/elements/Field"; +import AccessibleButton, { type ButtonEvent } from "../../../views/elements/AccessibleButton"; interface EnterEmailProps { email: string; diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index cb2c5b3b85..5f57146fe5 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import { Tooltip } from "@vector-im/compound-web"; import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; diff --git a/src/components/structures/auth/header/AuthHeaderContext.tsx b/src/components/structures/auth/header/AuthHeaderContext.tsx index 4c9d436f0c..768060b654 100644 --- a/src/components/structures/auth/header/AuthHeaderContext.tsx +++ b/src/components/structures/auth/header/AuthHeaderContext.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 { createContext, Dispatch, ReducerAction, ReducerState } from "react"; +import { createContext, type Dispatch, type ReducerAction, type ReducerState } from "react"; import type { AuthHeaderReducer } from "./AuthHeaderProvider"; diff --git a/src/components/structures/auth/header/AuthHeaderDisplay.tsx b/src/components/structures/auth/header/AuthHeaderDisplay.tsx index f1289a1c99..17f4c91b8c 100644 --- a/src/components/structures/auth/header/AuthHeaderDisplay.tsx +++ b/src/components/structures/auth/header/AuthHeaderDisplay.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, { Fragment, PropsWithChildren, ReactNode, useContext } from "react"; +import React, { Fragment, type PropsWithChildren, type ReactNode, useContext } from "react"; import { AuthHeaderContext } from "./AuthHeaderContext"; diff --git a/src/components/structures/auth/header/AuthHeaderModifier.tsx b/src/components/structures/auth/header/AuthHeaderModifier.tsx index d3b3d648e7..afe5a4b7ce 100644 --- a/src/components/structures/auth/header/AuthHeaderModifier.tsx +++ b/src/components/structures/auth/header/AuthHeaderModifier.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 { ReactNode, useContext, useEffect } from "react"; +import { type ReactNode, useContext, useEffect } from "react"; import { AuthHeaderContext } from "./AuthHeaderContext"; import { AuthHeaderActionType } from "./AuthHeaderProvider"; diff --git a/src/components/structures/auth/header/AuthHeaderProvider.tsx b/src/components/structures/auth/header/AuthHeaderProvider.tsx index 0189b69212..af48364bd7 100644 --- a/src/components/structures/auth/header/AuthHeaderProvider.tsx +++ b/src/components/structures/auth/header/AuthHeaderProvider.tsx @@ -7,10 +7,10 @@ Please see LICENSE files in the repository root for full details. */ import { isEqual } from "lodash"; -import React, { ComponentProps, PropsWithChildren, Reducer, useReducer } from "react"; +import React, { type ComponentProps, type PropsWithChildren, type Reducer, useReducer } from "react"; import { AuthHeaderContext } from "./AuthHeaderContext"; -import { AuthHeaderModifier } from "./AuthHeaderModifier"; +import { type AuthHeaderModifier } from "./AuthHeaderModifier"; export enum AuthHeaderActionType { Add, @@ -24,7 +24,7 @@ interface AuthHeaderAction { export type AuthHeaderReducer = Reducer[], AuthHeaderAction>; -export function AuthHeaderProvider({ children }: PropsWithChildren<{}>): JSX.Element { +export function AuthHeaderProvider({ children }: PropsWithChildren): JSX.Element { const [state, dispatch] = useReducer( (state: ComponentProps[], action: AuthHeaderAction) => { switch (action.type) { diff --git a/src/components/structures/grouper/BaseGrouper.ts b/src/components/structures/grouper/BaseGrouper.ts index a685582a2c..c0fc83080e 100644 --- a/src/components/structures/grouper/BaseGrouper.ts +++ b/src/components/structures/grouper/BaseGrouper.ts @@ -6,10 +6,11 @@ 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 { ReactNode } from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { type ReactNode } from "react"; +import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import MessagePanel, { WrappedEvent } from "../MessagePanel"; +import { type WrappedEvent } from "../MessagePanel"; +import type MessagePanel from "../MessagePanel"; /* Grouper classes determine when events can be grouped together in a summary. * Groupers should have the following methods: diff --git a/src/components/structures/grouper/CreationGrouper.tsx b/src/components/structures/grouper/CreationGrouper.tsx index 5009b14baf..009f5bdc26 100644 --- a/src/components/structures/grouper/CreationGrouper.tsx +++ b/src/components/structures/grouper/CreationGrouper.tsx @@ -6,12 +6,13 @@ 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, { ReactNode } from "react"; -import { EventType, M_BEACON_INFO, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import React, { type ReactNode } from "react"; +import { EventType, M_BEACON_INFO, type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { BaseGrouper } from "./BaseGrouper"; -import MessagePanel, { WrappedEvent } from "../MessagePanel"; +import { type WrappedEvent } from "../MessagePanel"; +import type MessagePanel from "../MessagePanel"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t } from "../../../languageHandler"; import DateSeparator from "../../views/messages/DateSeparator"; diff --git a/src/components/structures/grouper/LateEventGrouper.ts b/src/components/structures/grouper/LateEventGrouper.ts index 87cf6549b2..73e858b340 100644 --- a/src/components/structures/grouper/LateEventGrouper.ts +++ b/src/components/structures/grouper/LateEventGrouper.ts @@ -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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; const UNSIGNED_KEY = "io.element.late_event"; diff --git a/src/components/structures/grouper/MainGrouper.tsx b/src/components/structures/grouper/MainGrouper.tsx index 84d0be2674..e686f1aa81 100644 --- a/src/components/structures/grouper/MainGrouper.tsx +++ b/src/components/structures/grouper/MainGrouper.tsx @@ -6,8 +6,8 @@ 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, { ReactNode } from "react"; -import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import React, { type ReactNode } from "react"; +import { EventType, type MatrixEvent } from "matrix-js-sdk/src/matrix"; import type MessagePanel from "../MessagePanel"; import type { WrappedEvent } from "../MessagePanel"; diff --git a/src/components/utils/Box.tsx b/src/components/utils/Box.tsx index c81c9bafed..2de64ba075 100644 --- a/src/components/utils/Box.tsx +++ b/src/components/utils/Box.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { useEffect, useRef } from "react"; +import React, { useMemo } from "react"; type FlexProps = { /** @@ -40,25 +40,6 @@ type FlexProps = { grow?: string | null; }; -/** - * Set or remove a CSS property - * @param ref the reference - * @param name the CSS property name - * @param value the CSS property value - */ -function addOrRemoveProperty( - ref: React.MutableRefObject, - name: string, - value?: string | null, -): void { - const style = ref.current!.style; - if (value) { - style.setProperty(name, value); - } else { - style.removeProperty(name); - } -} - /** * A flex child helper */ @@ -71,12 +52,12 @@ export function Box({ children, ...props }: React.PropsWithChildren): JSX.Element { - const ref = useRef(); - - useEffect(() => { - addOrRemoveProperty(ref, `--mx-box-flex`, flex); - addOrRemoveProperty(ref, `--mx-box-shrink`, shrink); - addOrRemoveProperty(ref, `--mx-box-grow`, grow); + const style = useMemo(() => { + const style: Record = {}; + if (flex) style["--mx-box-flex"] = flex; + if (shrink) style["--mx-box-shrink"] = shrink; + if (grow) style["--mx-box-grow"] = grow; + return style; }, [flex, grow, shrink]); return React.createElement( @@ -88,7 +69,7 @@ export function Box({ "mx_Box--shrink": !!shrink, "mx_Box--grow": !!grow, }), - ref, + style, }, children, ); diff --git a/src/components/utils/Flex.tsx b/src/components/utils/Flex.tsx index ae5704d247..c4e2f90066 100644 --- a/src/components/utils/Flex.tsx +++ b/src/components/utils/Flex.tsx @@ -7,14 +7,14 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { useEffect, useRef } from "react"; +import React, { type ComponentProps, type JSXElementConstructor, useMemo } from "react"; -type FlexProps = { +type FlexProps> = { /** * The type of the HTML element * @default div */ - as?: string; + as?: T; /** * The CSS class name. */ @@ -30,15 +30,20 @@ type FlexProps = { */ direction?: "row" | "column" | "row-reverse" | "column-reverse"; /** - * The alingment of the flex children + * The alignment of the flex children * @default start */ - align?: "start" | "center" | "end" | "baseline" | "stretch"; + align?: "start" | "center" | "end" | "baseline" | "stretch" | "normal"; /** * The justification of the flex children * @default start */ justify?: "start" | "center" | "end" | "space-between"; + /** + * The wrapping of the flex children + * @default nowrap + */ + wrap?: "wrap" | "nowrap" | "wrap-reverse"; /** * The spacing between the flex children, expressed with the CSS unit * @default 0 @@ -48,31 +53,34 @@ type FlexProps = { * the on click event callback */ onClick?: (e: React.MouseEvent) => void; -}; +} & ComponentProps; /** * A flexbox container helper */ -export function Flex({ +export function Flex = "div">({ as = "div", display = "flex", direction = "row", align = "start", justify = "start", gap = "0", + wrap = "nowrap", className, children, ...props -}: React.PropsWithChildren): JSX.Element { - const ref = useRef(); +}: React.PropsWithChildren>): JSX.Element { + const style = useMemo( + () => ({ + "--mx-flex-display": display, + "--mx-flex-direction": direction, + "--mx-flex-align": align, + "--mx-flex-justify": justify, + "--mx-flex-gap": gap, + "--mx-flex-wrap": wrap, + }), + [align, direction, display, gap, justify, wrap], + ); - useEffect(() => { - ref.current!.style.setProperty(`--mx-flex-display`, display); - ref.current!.style.setProperty(`--mx-flex-direction`, direction); - ref.current!.style.setProperty(`--mx-flex-align`, align); - ref.current!.style.setProperty(`--mx-flex-justify`, justify); - ref.current!.style.setProperty(`--mx-flex-gap`, gap); - }, [align, direction, display, gap, justify]); - - return React.createElement(as, { ...props, className: classNames("mx_Flex", className), ref }, children); + return React.createElement(as, { ...props, className: classNames("mx_Flex", className), style }, children); } diff --git a/src/components/viewmodels/memberlist/MemberListViewModel.tsx b/src/components/viewmodels/memberlist/MemberListViewModel.tsx index 4a1a2d59f1..a03f703511 100644 --- a/src/components/viewmodels/memberlist/MemberListViewModel.tsx +++ b/src/components/viewmodels/memberlist/MemberListViewModel.tsx @@ -8,35 +8,35 @@ Please see LICENSE files in the repository root for full details. import { ClientEvent, EventType, - MatrixEvent, - Room, + type MatrixEvent, + type Room, RoomEvent, RoomMemberEvent, - RoomState, + type RoomState, RoomStateEvent, - RoomMember as SdkRoomMember, - User, + type RoomMember as SdkRoomMember, + type User, UserEvent, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { throttle } from "lodash"; -import { RoomMember } from "../../../models/rooms/RoomMember"; +import { type RoomMember } from "../../../models/rooms/RoomMember"; import { mediaFromMxc } from "../../../customisations/Media"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; -import { PresenceState } from "../../../models/rooms/PresenceState"; +import { type PresenceState } from "../../../models/rooms/PresenceState"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { SDKContext } from "../../../contexts/SDKContext"; import PosthogTrackers from "../../../PosthogTrackers"; -import { ButtonEvent } from "../../views/elements/AccessibleButton"; +import { type ButtonEvent } from "../../views/elements/AccessibleButton"; import { inviteToRoom } from "../../../utils/room/inviteToRoom"; import { canInviteTo } from "../../../utils/room/canInviteTo"; import { isValid3pidInvite } from "../../../RoomInvite"; -import { ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite"; -import { XOR } from "../../../@types/common"; +import { type ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite"; +import { type XOR } from "../../../@types/common"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; type Member = XOR<{ member: RoomMember }, { threePidInvite: ThreePIDInvite }>; @@ -99,8 +99,12 @@ export function sdkRoomMemberToRoomMember(member: SdkRoomMember): Member { }; } +export const SEPARATOR = "SEPARATOR"; +export type MemberWithSeparator = Member | typeof SEPARATOR; + export interface MemberListViewState { - members: Member[]; + members: MemberWithSeparator[]; + memberCount: number; search: (searchQuery: string) => void; isPresenceEnabled: boolean; shouldShowInvite: boolean; @@ -118,60 +122,65 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { } const sdkContext = useContext(SDKContext); - const [memberMap, setMemberMap] = useState>(new Map()); + const [memberMap, setMemberMap] = useState>(new Map()); const [isLoading, setIsLoading] = useState(true); - // This is the last known total number of members in this room. - const totalMemberCount = useRef(0); - - const searchQuery = useRef(""); + const [totalMemberCount, setTotalMemberCount] = useState(0); + /** + * This is the current number of members in the list. + * This number will be less than the total number of members + * in the room when the search functionality is used. + */ + const [memberCount, setMemberCount] = useState(0); const loadMembers = useMemo( () => throttle( - async (): Promise => { + async (searchQuery?: string): Promise => { const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList( roomId, - searchQuery.current, + searchQuery, ); - const newMemberMap = new Map(); - // First add the invited room members - for (const member of invitedSdk) { - const roomMember = sdkRoomMemberToRoomMember(member); - newMemberMap.set(member.userId, roomMember); - } - // Then add the third party invites - const threePidInvited = getPending3PidInvites(room, searchQuery.current); - for (const invited of threePidInvited) { - const key = invited.threePidInvite!.event.getContent().display_name; - newMemberMap.set(key, invited); - } - // Finally add the joined room members + const threePidInvited = getPending3PidInvites(room, searchQuery); + + const newMemberMap = new Map(); + + // First add the joined room members for (const member of joinedSdk) { const roomMember = sdkRoomMemberToRoomMember(member); newMemberMap.set(member.userId, roomMember); } + + // Then a separator if needed + if (joinedSdk.length > 0 && (invitedSdk.length > 0 || threePidInvited.length > 0)) + newMemberMap.set(SEPARATOR, SEPARATOR); + + // Then add the invited room members + for (const member of invitedSdk) { + const roomMember = sdkRoomMemberToRoomMember(member); + newMemberMap.set(member.userId, roomMember); + } + + // Finally add the third party invites + for (const invited of threePidInvited) { + const key = invited.threePidInvite!.event.getContent().display_name; + newMemberMap.set(key, invited); + } + setMemberMap(newMemberMap); - if (!searchQuery.current) { + setMemberCount(joinedSdk.length + invitedSdk.length + threePidInvited.length); + if (!searchQuery) { /** * Since searching for members only gives you the relevant * members matching the query, do not update the totalMemberCount! **/ - totalMemberCount.current = newMemberMap.size; + setTotalMemberCount(newMemberMap.size); } }, 500, { leading: true, trailing: true }, ), - [roomId, sdkContext.memberListStore, room], - ); - - const search = useCallback( - (query: string) => { - searchQuery.current = query; - loadMembers(); - }, - [loadMembers], + [sdkContext.memberListStore, roomId, room], ); const isPresenceEnabled = useMemo( @@ -252,12 +261,13 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { return { members: Array.from(memberMap.values()), - search, + memberCount, + search: loadMembers, shouldShowInvite, isPresenceEnabled, isLoading, onInviteButtonClick, - shouldShowSearch: totalMemberCount.current >= 20, + shouldShowSearch: totalMemberCount >= 20, canInvite, }; } diff --git a/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx b/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx index ade40e9b97..4f6814caae 100644 --- a/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx +++ b/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx @@ -6,18 +6,18 @@ Please see LICENSE files in the repository root for full details. */ import { useEffect, useMemo, useState } from "react"; -import { RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; -import { UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; +import { RoomStateEvent, type MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { type UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import dis from "../../../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { Action } from "../../../../dispatcher/actions"; import { asyncSome } from "../../../../utils/arrays"; import { getUserDeviceIds } from "../../../../utils/crypto/deviceInfo"; -import { RoomMember } from "../../../../models/rooms/RoomMember"; -import { E2EState } from "../../../views/rooms/E2EIcon"; -import { _t, _td, TranslationKey } from "../../../../languageHandler"; +import { type RoomMember } from "../../../../models/rooms/RoomMember"; +import { _t, _td, type TranslationKey } from "../../../../languageHandler"; import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier"; +import { E2EStatus } from "../../../../utils/ShieldUtils"; interface MemberTileViewModelProps { member: RoomMember; @@ -25,7 +25,7 @@ interface MemberTileViewModelProps { } export interface MemberTileViewState extends MemberTileViewModelProps { - e2eStatus?: E2EState; + e2eStatus?: E2EStatus; name: string; onClick: () => void; title?: string; @@ -43,7 +43,7 @@ const PowerLabel: Record = { }; export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState { - const [e2eStatus, setE2eStatus] = useState(); + const [e2eStatus, setE2eStatus] = useState(); useEffect(() => { const cli = MatrixClientPeg.safeGet(); @@ -53,7 +53,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT const isMe = userId === cli.getUserId(); const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId); if (!userTrust?.isCrossSigningVerified()) { - setE2eStatus(userTrust?.wasCrossSigningVerified() ? E2EState.Warning : E2EState.Normal); + setE2eStatus(userTrust?.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal); return; } @@ -67,7 +67,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified()); }); - setE2eStatus(anyDeviceUnverified ? E2EState.Warning : E2EState.Verified); + setE2eStatus(anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified); }; const onRoomStateEvents = (ev: MatrixEvent): void => { @@ -145,7 +145,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT userLabel = _t(PowerLabel[powerStatus]); } if (props.member.isInvite) { - userLabel = `(${_t("member_list|invited_label")})`; + userLabel = _t("member_list|invited_label"); } return { diff --git a/src/components/viewmodels/memberlist/tiles/ThreePidTileViewModel.tsx b/src/components/viewmodels/memberlist/tiles/ThreePidTileViewModel.tsx index daeb8d899f..f1e32692c0 100644 --- a/src/components/viewmodels/memberlist/tiles/ThreePidTileViewModel.tsx +++ b/src/components/viewmodels/memberlist/tiles/ThreePidTileViewModel.tsx @@ -7,7 +7,8 @@ Please see LICENSE files in the repository root for full details. import dis from "../../../../dispatcher/dispatcher"; import { Action } from "../../../../dispatcher/actions"; -import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite"; +import { type ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite"; +import { _t } from "../../../../languageHandler"; interface ThreePidTileViewModelProps { threePidInvite: ThreePIDInvite; @@ -16,6 +17,7 @@ interface ThreePidTileViewModelProps { export interface ThreePidTileViewState { name: string; onClick: () => void; + userLabel?: string; } export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState { @@ -28,8 +30,11 @@ export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): Thr }); }; + const userLabel = _t("member_list|invited_label"); + return { name, onClick, + userLabel, }; } diff --git a/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx b/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx new file mode 100644 index 0000000000..9e141c1379 --- /dev/null +++ b/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * 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 { useCallback, useEffect, useState } from "react"; + +import type { Room } from "matrix-js-sdk/src/matrix"; +import { type MessagePreview, MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; + +interface MessagePreviewViewState { + /** + * A string representation of the message preview if available. + */ + message?: string; +} + +/** + * View model for rendering a message preview for a given room list item. + * @param room The room for which we're rendering the message preview. + * @see {@link MessagePreviewViewState} for what this view model returns. + */ +export function useMessagePreviewViewModel(room: Room): MessagePreviewViewState { + const [messagePreview, setMessagePreview] = useState(null); + + const updatePreview = useCallback(async (): Promise => { + /** + * The second argument to getPreviewForRoom is a tag id which doesn't really make + * much sense within the context of the new room list. We can pass an empty string + * to match all tags for now but we should remember to actually change the implementation + * in the store once we remove the legacy room list. + */ + const newPreview = await MessagePreviewStore.instance.getPreviewForRoom(room, ""); + setMessagePreview(newPreview); + }, [room]); + + /** + * Update when the message preview has changed for this room. + */ + useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => { + updatePreview(); + }); + + /** + * Do an initial fetch of the message preview. + */ + useEffect(() => { + updatePreview(); + }, [updatePreview]); + + return { + message: messagePreview?.text, + }; +} diff --git a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx new file mode 100644 index 0000000000..8a1fdb1fe7 --- /dev/null +++ b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx @@ -0,0 +1,211 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * 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 { useCallback } from "react"; +import { JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix"; + +import { useFeatureEnabled } from "../../../hooks/useSettings"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import PosthogTrackers from "../../../PosthogTrackers"; +import { Action } from "../../../dispatcher/actions"; +import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; +import { + getMetaSpaceName, + type MetaSpace, + type SpaceKey, + UPDATE_HOME_BEHAVIOUR, + UPDATE_SELECTED_SPACE, +} from "../../../stores/spaces"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; +import { + shouldShowSpaceSettings, + showCreateNewRoom, + showSpaceInvite, + showSpacePreferences, + showSpaceSettings, +} from "../../../utils/space"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { createRoom, hasCreateRoomRights } from "./utils"; + +/** + * Hook to get the active space and its title. + */ +function useSpace(): { activeSpace: Room | null; title: string } { + const [spaceKey, activeSpace] = useEventEmitterState<[SpaceKey, Room | null]>( + SpaceStore.instance, + UPDATE_SELECTED_SPACE, + () => [SpaceStore.instance.activeSpace, SpaceStore.instance.activeSpaceRoom], + ); + const spaceName = useTypedEventEmitterState(activeSpace ?? undefined, RoomEvent.Name, () => activeSpace?.name); + const allRoomsInHome = useEventEmitterState( + SpaceStore.instance, + UPDATE_HOME_BEHAVIOUR, + () => SpaceStore.instance.allRoomsInHome, + ); + + const title = spaceName ?? getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome); + + return { + activeSpace, + title, + }; +} + +export interface RoomListHeaderViewState { + /** + * The title of the room list + */ + title: string; + /** + * Whether to display the compose menu + * True if the user can create rooms + */ + displayComposeMenu: boolean; + /** + * Whether to display the space menu + * True if there is an active space + */ + displaySpaceMenu: boolean; + /** + * Whether the user can create rooms + */ + canCreateRoom: boolean; + /** + * Whether the user can create video rooms + */ + canCreateVideoRoom: boolean; + /** + * Whether the user can invite in the active space + */ + canInviteInSpace: boolean; + /** + * Whether the user can access space settings + */ + canAccessSpaceSettings: boolean; + /** + * Create a chat room + * @param e - The click event + */ + createChatRoom: (e: Event) => void; + /** + * Create a room + * @param e - The click event + */ + createRoom: (e: Event) => void; + /** + * Create a video room + */ + createVideoRoom: () => void; + /** + * Open the active space home + */ + openSpaceHome: () => void; + /** + * Display the space invite dialog + */ + inviteInSpace: () => void; + /** + * Open the space preferences + */ + openSpacePreferences: () => void; + /** + * Open the space settings + */ + openSpaceSettings: () => void; +} + +/** + * View model for the RoomListHeader. + */ +export function useRoomListHeaderViewModel(): RoomListHeaderViewState { + const matrixClient = useMatrixClientContext(); + const { activeSpace, title } = useSpace(); + const isSpaceRoom = Boolean(activeSpace); + + const canCreateRoom = hasCreateRoomRights(matrixClient, activeSpace); + const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms"); + const displayComposeMenu = canCreateRoom || canCreateVideoRoom; + const displaySpaceMenu = isSpaceRoom; + const canInviteInSpace = Boolean( + activeSpace?.getJoinRule() === JoinRule.Public || activeSpace?.canInvite(matrixClient.getSafeUserId()), + ); + const canAccessSpaceSettings = Boolean(activeSpace && shouldShowSpaceSettings(activeSpace)); + + /* Actions */ + + const createChatRoom = useCallback((e: Event) => { + defaultDispatcher.fire(Action.CreateChat); + PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e); + }, []); + + const createRoomMemoized = useCallback( + (e: Event) => { + createRoom(activeSpace); + PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e); + }, + [activeSpace], + ); + + const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); + const createVideoRoom = useCallback(() => { + const type = elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo; + if (activeSpace) { + showCreateNewRoom(activeSpace, type); + } else { + defaultDispatcher.dispatch({ + action: Action.CreateRoom, + type, + }); + } + }, [activeSpace, elementCallVideoRoomsEnabled]); + + const openSpaceHome = useCallback(() => { + // openSpaceHome is only available when there is an active space + if (!activeSpace) return; + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: activeSpace.roomId, + metricsTrigger: undefined, + }); + }, [activeSpace]); + + const inviteInSpace = useCallback(() => { + // inviteInSpace is only available when there is an active space + if (!activeSpace) return; + showSpaceInvite(activeSpace); + }, [activeSpace]); + + const openSpacePreferences = useCallback(() => { + // openSpacePreferences is only available when there is an active space + if (!activeSpace) return; + showSpacePreferences(activeSpace); + }, [activeSpace]); + + const openSpaceSettings = useCallback(() => { + // openSpaceSettings is only available when there is an active space + if (!activeSpace) return; + showSpaceSettings(activeSpace); + }, [activeSpace]); + + return { + title, + displayComposeMenu, + displaySpaceMenu, + canCreateRoom, + canCreateVideoRoom, + canInviteInSpace, + canAccessSpaceSettings, + createChatRoom, + createRoom: createRoomMemoized, + createVideoRoom, + openSpaceHome, + inviteInSpace, + openSpacePreferences, + openSpaceSettings, + }; +} diff --git a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx new file mode 100644 index 0000000000..6b089495a0 --- /dev/null +++ b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx @@ -0,0 +1,180 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * 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 { useCallback } from "react"; +import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; + +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; +import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; +import { hasAccessToOptionsMenu } from "./utils"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import { DefaultTagID } from "../../../stores/room-list/models"; +import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; +import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../../settings/UIFeature"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications"; +import PosthogTrackers from "../../../PosthogTrackers"; +import { tagRoom } from "../../../utils/room/tagRoom"; + +export interface RoomListItemMenuViewState { + /** + * Whether the more options menu should be shown. + */ + showMoreOptionsMenu: boolean; + /** + * Whether the room is a favourite room. + */ + isFavourite: boolean; + /** + * Can invite other user's in the room. + */ + canInvite: boolean; + /** + * Can copy the room link. + */ + canCopyRoomLink: boolean; + /** + * Can mark the room as read. + */ + canMarkAsRead: boolean; + /** + * Can mark the room as unread. + */ + canMarkAsUnread: boolean; + /** + * Mark the room as read. + * @param evt + */ + markAsRead: (evt: Event) => void; + /** + * Mark the room as unread. + * @param evt + */ + markAsUnread: (evt: Event) => void; + /** + * Toggle the room as favourite. + * @param evt + */ + toggleFavorite: (evt: Event) => void; + /** + * Toggle the room as low priority. + */ + toggleLowPriority: () => void; + /** + * Invite other users in the room. + * @param evt + */ + invite: (evt: Event) => void; + /** + * Copy the room link in the clipboard. + * @param evt + */ + copyRoomLink: (evt: Event) => void; + /** + * Leave the room. + * @param evt + */ + leaveRoom: (evt: Event) => void; +} + +export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewState { + const matrixClient = useMatrixClientContext(); + const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); + const { level: notificationLevel } = useUnreadNotifications(room); + + const showMoreOptionsMenu = hasAccessToOptionsMenu(room); + + const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); + const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]); + const isArchived = Boolean(roomTags[DefaultTagID.Archived]); + + const canMarkAsRead = notificationLevel > NotificationLevel.None; + const canMarkAsUnread = !canMarkAsRead && !isArchived; + + const canInvite = + room.canInvite(matrixClient.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers); + const canCopyRoomLink = !isDm; + + // Actions + + const markAsRead = useCallback( + async (evt: Event): Promise => { + await clearRoomNotification(room, matrixClient); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", evt); + }, + [room, matrixClient], + ); + + const markAsUnread = useCallback( + async (evt: Event): Promise => { + await setMarkedUnreadState(room, matrixClient, true); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", evt); + }, + [room, matrixClient], + ); + + const toggleFavorite = useCallback( + (evt: Event): void => { + tagRoom(room, DefaultTagID.Favourite); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); + }, + [room], + ); + + const toggleLowPriority = useCallback((): void => tagRoom(room, DefaultTagID.LowPriority), [room]); + + const invite = useCallback( + (evt: Event): void => { + dispatcher.dispatch({ + action: "view_invite", + roomId: room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", evt); + }, + [room], + ); + + const copyRoomLink = useCallback( + (evt: Event): void => { + dispatcher.dispatch({ + action: "copy_room", + room_id: room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); + }, + [room], + ); + + const leaveRoom = useCallback( + (evt: Event): void => { + dispatcher.dispatch({ + action: isArchived ? "forget_room" : "leave_room", + room_id: room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", evt); + }, + [room, isArchived], + ); + + return { + showMoreOptionsMenu, + isFavourite, + canInvite, + canCopyRoomLink, + canMarkAsRead, + canMarkAsUnread, + markAsRead, + markAsUnread, + toggleFavorite, + toggleLowPriority, + invite, + copyRoomLink, + leaveRoom, + }; +} diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx new file mode 100644 index 0000000000..9e38e6e8d8 --- /dev/null +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * 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 { useCallback } from "react"; +import { type Room } from "matrix-js-sdk/src/matrix"; + +import dispatcher from "../../../dispatcher/dispatcher"; +import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { Action } from "../../../dispatcher/actions"; +import { hasAccessToOptionsMenu } from "./utils"; + +export interface RoomListItemViewState { + /** + * Whether the hover menu should be shown. + */ + showHoverMenu: boolean; + /** + * Open the room having given roomId. + */ + openRoom: () => void; +} + +/** + * View model for the room list item + * @see {@link RoomListItemViewState} for more information about what this view model returns. + */ +export function useRoomListItemViewModel(room: Room): RoomListItemViewState { + // incoming: Check notification menu rights + const showHoverMenu = hasAccessToOptionsMenu(room); + + // Actions + + const openRoom = useCallback((): void => { + dispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + metricsTrigger: "RoomList", + }); + }, [room]); + + return { + showHoverMenu, + openRoom, + }; +} diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx new file mode 100644 index 0000000000..217eaefbd9 --- /dev/null +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -0,0 +1,137 @@ +/* +Copyright 2025 New Vector Ltd. + +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 { useCallback } from "react"; + +import type { Room } from "matrix-js-sdk/src/matrix"; +import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms"; +import { type SortOption, useSorter } from "./useSorter"; +import { useMessagePreviewToggle } from "./useMessagePreviewToggle"; +import { createRoom as createRoomFunc, hasCreateRoomRights } from "./utils"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; +import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { useStickyRoomList } from "./useStickyRoomList"; + +export interface RoomListViewState { + /** + * A list of rooms to be displayed in the left panel. + */ + rooms: Room[]; + + /** + * Create a chat room + * @param e - The click event + */ + createChatRoom: () => void; + + /** + * Whether the user can create a room in the current space + */ + canCreateRoom: boolean; + + /** + * Create a room + * @param e - The click event + */ + createRoom: () => void; + + /** + * A list of objects that provide the view enough information + * to render primary room filters. + */ + primaryFilters: PrimaryFilter[]; + + /** + * The currently active primary filter. + * If no primary filter is active, this will be undefined. + */ + activePrimaryFilter?: PrimaryFilter; + + /** + * A function to activate a given secondary filter. + */ + activateSecondaryFilter: (filter: SecondaryFilters) => void; + + /** + * The currently active secondary filter. + */ + activeSecondaryFilter: SecondaryFilters; + + /** + * Change the sort order of the room-list. + */ + sort: (option: SortOption) => void; + + /** + * The currently active sort option. + */ + activeSortOption: SortOption; + + /** + * Whether message previews must be shown or not. + */ + shouldShowMessagePreview: boolean; + + /** + * A function to turn on/off message previews. + */ + toggleMessagePreview: () => void; + + /** + * The index of the active room in the room list. + */ + activeIndex: number | undefined; +} + +/** + * View model for the new room list + * @see {@link RoomListViewState} for more information about what this view model returns. + */ +export function useRoomListViewModel(): RoomListViewState { + const matrixClient = useMatrixClientContext(); + const { + primaryFilters, + activePrimaryFilter, + rooms: filteredRooms, + activateSecondaryFilter, + activeSecondaryFilter, + } = useFilteredRooms(); + const { activeIndex, rooms } = useStickyRoomList(filteredRooms); + + const currentSpace = useEventEmitterState( + SpaceStore.instance, + UPDATE_SELECTED_SPACE, + () => SpaceStore.instance.activeSpaceRoom, + ); + const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace); + + const { activeSortOption, sort } = useSorter(); + const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle(); + + const createChatRoom = useCallback(() => dispatcher.fire(Action.CreateChat), []); + const createRoom = useCallback(() => createRoomFunc(currentSpace), [currentSpace]); + + return { + rooms, + canCreateRoom, + createRoom, + createChatRoom, + primaryFilters, + activePrimaryFilter, + activateSecondaryFilter, + activeSecondaryFilter, + activeSortOption, + sort, + shouldShowMessagePreview, + toggleMessagePreview, + activeIndex, + }; +} diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx new file mode 100644 index 0000000000..5e1554fcdc --- /dev/null +++ b/src/components/viewmodels/roomlist/useFilteredRooms.tsx @@ -0,0 +1,190 @@ +/* +Copyright 2025 New Vector Ltd. + +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 { useCallback, useMemo, useState } from "react"; + +import type { Room } from "matrix-js-sdk/src/matrix"; +import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters"; +import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; +import { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; + +/** + * Provides information about a primary filter. + * A primary filter is a commonly used filter that is given + * more precedence in the UI. For eg, primary filters may be + * rendered as pills above the room list. + */ +export interface PrimaryFilter { + // A function to toggle this filter on and off. + toggle: () => void; + // Whether this filter is currently applied + active: boolean; + // Text that can be used in the UI to represent this filter. + name: string; + // The key of the filter + key: FilterKey; +} + +interface FilteredRooms { + primaryFilters: PrimaryFilter[]; + rooms: Room[]; + activateSecondaryFilter: (filter: SecondaryFilters) => void; + activeSecondaryFilter: SecondaryFilters; + /** + * The currently active primary filter. + * If no primary filter is active, this will be undefined. + */ + activePrimaryFilter?: PrimaryFilter; +} + +const filterKeyToNameMap: Map = new Map([ + [FilterKey.UnreadFilter, _td("room_list|filters|unread")], + [FilterKey.FavouriteFilter, _td("room_list|filters|favourite")], + [FilterKey.PeopleFilter, _td("room_list|filters|people")], + [FilterKey.RoomsFilter, _td("room_list|filters|rooms")], +]); + +/** + * These are the secondary filters which are not prominently shown + * in the UI. + */ +export const enum SecondaryFilters { + AllActivity, + MentionsOnly, + InvitesOnly, + LowPriority, +} + +/** + * A map from {@link SecondaryFilters} which the UI understands to + * {@link FilterKey} which the store understands. + */ +const secondaryFiltersToFilterKeyMap = new Map([ + [SecondaryFilters.AllActivity, undefined], + [SecondaryFilters.MentionsOnly, FilterKey.MentionsFilter], + [SecondaryFilters.InvitesOnly, FilterKey.InvitesFilter], + [SecondaryFilters.LowPriority, FilterKey.LowPriorityFilter], +]); + +/** + * Use this function to determine if a given primary filter is compatible with + * a given secondary filter. Practically, this determines whether it makes sense + * to expose two filters together in the UI - for eg, it does not make sense to show the + * favourite primary filter if the active secondary filter is low priority. + * @param primary Primary filter key + * @param secondary Secondary filter key + * @returns true if compatible, false otherwise + */ +function isPrimaryFilterCompatible(primary: FilterKey, secondary: FilterKey): boolean { + if (secondary === FilterKey.MentionsFilter) { + if (primary === FilterKey.UnreadFilter) return false; + } else if (secondary === FilterKey.InvitesFilter) { + if (primary === FilterKey.UnreadFilter || primary === FilterKey.FavouriteFilter) return false; + } else if (secondary === FilterKey.LowPriorityFilter) { + if (primary === FilterKey.FavouriteFilter) return false; + } + return true; +} + +/** + * Track available filters and provide a filtered list of rooms. + */ +export function useFilteredRooms(): FilteredRooms { + /** + * Primary filter refers to the pill based filters + * rendered above the room list. + */ + const [primaryFilter, setPrimaryFilter] = useState(); + /** + * Secondary filters are also filters but they are hidden + * away in a popup menu. + */ + const [activeSecondaryFilter, setActiveSecondaryFilter] = useState(SecondaryFilters.AllActivity); + + const secondaryFilter = useMemo( + () => secondaryFiltersToFilterKeyMap.get(activeSecondaryFilter), + [activeSecondaryFilter], + ); + + const [rooms, setRooms] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace()); + + const updateRoomsFromStore = useCallback((filters: FilterKey[] = []): void => { + const newRooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filters); + setRooms(newRooms); + }, []); + + const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] => + array.filter((f) => f !== undefined) as FilterKey[]; + + const getAppliedFilters = (): FilterKey[] => { + return filterUndefined([primaryFilter, secondaryFilter]); + }; + + useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => { + const filters = getAppliedFilters(); + updateRoomsFromStore(filters); + }); + + /** + * Secondary filters are activated using this function. + * This is different to how primary filters work because the secondary + * filters are static i.e they are always available and don't need to be + * hidden. + */ + const activateSecondaryFilter = useCallback( + (filter: SecondaryFilters): void => { + // If the filter is already active, just return. + if (filter === activeSecondaryFilter) return; + + // SecondaryFilter is an enum for the UI, let's convert it to something + // that the store will understand. + const secondary = secondaryFiltersToFilterKeyMap.get(filter); + setActiveSecondaryFilter(filter); + + // Reset any active primary filters. + setPrimaryFilter(undefined); + + updateRoomsFromStore(filterUndefined([secondary])); + }, + [activeSecondaryFilter, updateRoomsFromStore], + ); + + /** + * This tells the view which primary filters are available, how to toggle them + * and whether a given primary filter is active. @see {@link PrimaryFilter} + */ + const primaryFilters = useMemo(() => { + const createPrimaryFilter = (key: FilterKey, name: string): PrimaryFilter => { + return { + toggle: () => { + setPrimaryFilter((currentFilter) => { + const filter = currentFilter === key ? undefined : key; + updateRoomsFromStore(filterUndefined([filter, secondaryFilter])); + return filter; + }); + }, + active: primaryFilter === key, + name, + key, + }; + }; + const filters: PrimaryFilter[] = []; + for (const [key, name] of filterKeyToNameMap.entries()) { + if (secondaryFilter && !isPrimaryFilterCompatible(key, secondaryFilter)) { + continue; + } + filters.push(createPrimaryFilter(key, _t(name))); + } + return filters; + }, [primaryFilter, updateRoomsFromStore, secondaryFilter]); + + const activePrimaryFilter = useMemo(() => primaryFilters.find((filter) => filter.active), [primaryFilters]); + + return { primaryFilters, activePrimaryFilter, rooms, activateSecondaryFilter, activeSecondaryFilter }; +} diff --git a/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx b/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx new file mode 100644 index 0000000000..27ccd40106 --- /dev/null +++ b/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * 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 { useCallback, useState } from "react"; + +import SettingsStore from "../../../settings/SettingsStore"; +import { SettingLevel } from "../../../settings/SettingLevel"; + +interface MessagePreviewToggleState { + shouldShowMessagePreview: boolean; + toggleMessagePreview: () => void; +} + +/** + * This hook: + * - Provides a state that tracks whether message previews are turned on or off. + * - Provides a function to toggle message previews. + */ +export function useMessagePreviewToggle(): MessagePreviewToggleState { + const [shouldShowMessagePreview, setShouldShowMessagePreview] = useState(() => + SettingsStore.getValue("RoomList.showMessagePreview"), + ); + + const toggleMessagePreview = useCallback((): void => { + setShouldShowMessagePreview((current) => { + const toggled = !current; + SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled); + return toggled; + }); + }, []); + + return { toggleMessagePreview, shouldShowMessagePreview }; +} diff --git a/src/components/viewmodels/roomlist/useSorter.ts b/src/components/viewmodels/roomlist/useSorter.ts new file mode 100644 index 0000000000..c7a880d430 --- /dev/null +++ b/src/components/viewmodels/roomlist/useSorter.ts @@ -0,0 +1,62 @@ +/* +Copyright 2025 New Vector Ltd. + +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 { useState } from "react"; + +import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; +import { SortingAlgorithm } from "../../../stores/room-list-v3/skip-list/sorters"; +import SettingsStore from "../../../settings/SettingsStore"; + +/** + * Sorting options made available to the view. + */ +export const enum SortOption { + Activity = SortingAlgorithm.Recency, + AToZ = SortingAlgorithm.Alphabetic, +} + +/** + * {@link SortOption} holds almost the same information as + * {@link SortingAlgorithm}. This is done intentionally to + * prevent the view from having a dependence on the + * model (which is the store in this case). + */ +const sortingAlgorithmToSortingOption = { + [SortingAlgorithm.Alphabetic]: SortOption.AToZ, + [SortingAlgorithm.Recency]: SortOption.Activity, +}; + +const sortOptionToSortingAlgorithm = { + [SortOption.AToZ]: SortingAlgorithm.Alphabetic, + [SortOption.Activity]: SortingAlgorithm.Recency, +}; + +interface SortState { + sort: (option: SortOption) => void; + activeSortOption: SortOption; +} + +/** + * This hook does two things: + * - Provides a way to track the currently active sort option. + * - Provides a function to resort the room list. + */ +export function useSorter(): SortState { + const [activeSortingAlgorithm, setActiveSortingAlgorithm] = useState(() => + SettingsStore.getValue("RoomList.preferredSorting"), + ); + + const sort = (option: SortOption): void => { + const sortingAlgorithm = sortOptionToSortingAlgorithm[option]; + RoomListStoreV3.instance.resort(sortingAlgorithm); + setActiveSortingAlgorithm(sortingAlgorithm); + }; + + return { + sort, + activeSortOption: sortingAlgorithmToSortingOption[activeSortingAlgorithm!], + }; +} diff --git a/src/components/viewmodels/roomlist/useStickyRoomList.tsx b/src/components/viewmodels/roomlist/useStickyRoomList.tsx new file mode 100644 index 0000000000..e8234d14ae --- /dev/null +++ b/src/components/viewmodels/roomlist/useStickyRoomList.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * 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 { useCallback, useEffect, useState } from "react"; + +import { SdkContextClass } from "../../../contexts/SDKContext"; +import { useDispatcher } from "../../../hooks/useDispatcher"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import type { Room } from "matrix-js-sdk/src/matrix"; +import type { Optional } from "matrix-events-sdk"; + +function getIndexByRoomId(rooms: Room[], roomId: Optional): number | undefined { + const index = rooms.findIndex((room) => room.roomId === roomId); + return index === -1 ? undefined : index; +} + +function getRoomsWithStickyRoom( + rooms: Room[], + oldIndex: number | undefined, + newIndex: number | undefined, + isRoomChange: boolean, +): { newRooms: Room[]; newIndex: number | undefined } { + const updated = { newIndex, newRooms: rooms }; + if (isRoomChange) { + /* + * When opening another room, the index should obviously change. + */ + return updated; + } + if (newIndex === undefined || oldIndex === undefined) { + /* + * If oldIndex is undefined, then there was no active room before. + * So nothing to do in regards to sticky room. + * Similarly, if newIndex is undefined, there's no active room anymore. + */ + return updated; + } + if (newIndex === oldIndex) { + /* + * If the index hasn't changed, we have nothing to do. + */ + return updated; + } + if (oldIndex > rooms.length - 1) { + /* + * If the old index falls out of the bounds of the rooms array + * (usually because rooms were removed), we can no longer place + * the active room in the same old index. + */ + return updated; + } + + /* + * Making the active room sticky is as simple as removing it from + * its new index and placing it in the old index. + */ + const newRooms = [...rooms]; + const [newRoom] = newRooms.splice(newIndex, 1); + newRooms.splice(oldIndex, 0, newRoom); + + return { newIndex: oldIndex, newRooms }; +} + +interface StickyRoomListResult { + /** + * List of rooms with sticky active room. + */ + rooms: Room[]; + /** + * Index of the active room in the room list. + */ + activeIndex: number | undefined; +} + +/** + * - Provides a list of rooms such that the active room is sticky i.e the active room is kept + * in the same index even when the order of rooms in the list changes. + * - Provides the index of the active room. + * @param rooms list of rooms + * @see {@link StickyRoomListResult} details what this hook returns.. + */ +export function useStickyRoomList(rooms: Room[]): StickyRoomListResult { + const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({ + index: undefined, + roomsWithStickyRoom: rooms, + }); + + const updateRoomsAndIndex = useCallback( + (newRoomId?: string, isRoomChange: boolean = false) => { + setListState((current) => { + const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); + const newActiveIndex = getIndexByRoomId(rooms, activeRoomId); + const oldIndex = current.index; + const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange); + return { index: newIndex, roomsWithStickyRoom: newRooms }; + }); + }, + [rooms], + ); + + // Re-calculate the index when the active room has changed. + useDispatcher(dispatcher, (payload) => { + if (payload.action === Action.ActiveRoomChanged) updateRoomsAndIndex(payload.newRoomId, true); + }); + + // Re-calculate the index when the list of rooms has changed. + useEffect(() => { + updateRoomsAndIndex(); + }, [rooms, updateRoomsAndIndex]); + + return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom }; +} diff --git a/src/components/viewmodels/roomlist/utils.ts b/src/components/viewmodels/roomlist/utils.ts new file mode 100644 index 0000000000..6220c3b961 --- /dev/null +++ b/src/components/viewmodels/roomlist/utils.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * 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 { type Room, KnownMembership, EventTimeline, EventType, type MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { isKnockDenied } from "../../../utils/membership"; +import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../../settings/UIFeature"; +import { showCreateNewRoom } from "../../../utils/space"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; + +/** + * Check if the user has access to the options menu. + * @param room + */ +export function hasAccessToOptionsMenu(room: Room): boolean { + return ( + room.getMyMembership() === KnownMembership.Invite || + (room.getMyMembership() !== KnownMembership.Knock && + !isKnockDenied(room) && + shouldShowComponent(UIComponent.RoomOptionsMenu)) + ); +} + +/** + * Create a room + * @param space - The space to create the room in + */ +export async function createRoom(space?: Room | null): Promise { + if (space) { + await showCreateNewRoom(space); + } else { + dispatcher.fire(Action.CreateRoom); + } +} + +/** + * Check if the user has the rights to create a room in the given space + * If the space is not provided, it will check if the user has the rights to create a room in general + * @param matrixClient + * @param space + */ +export function hasCreateRoomRights(matrixClient: MatrixClient, space?: Room | null): boolean { + const hasUIRight = shouldShowComponent(UIComponent.CreateRooms); + if (!space || !hasUIRight) return hasUIRight; + + return Boolean( + space + ?.getLiveTimeline() + .getState(EventTimeline.FORWARDS) + ?.maySendStateEvent(EventType.RoomAvatar, matrixClient.getSafeUserId()), + ); +} diff --git a/src/components/viewmodels/rooms/UserIdentityWarningViewModel.tsx b/src/components/viewmodels/rooms/UserIdentityWarningViewModel.tsx new file mode 100644 index 0000000000..9e9f0f7b3a --- /dev/null +++ b/src/components/viewmodels/rooms/UserIdentityWarningViewModel.tsx @@ -0,0 +1,192 @@ +/* +Copyright 2025 New Vector Ltd. + +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 { useCallback, useEffect, useMemo, useState } from "react"; +import { EventType, type MatrixEvent, type Room, type RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { type CryptoApi, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; +import { throttle } from "lodash"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx"; +import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts"; + +export type ViolationType = "PinViolation" | "VerificationViolation"; + +/** + * Represents a prompt to the user about a violation in the room. + * The type of violation and the member it relates to are included. + * If the type is "VerificationViolation", the warning is critical and should be reported with more urgency. + */ +export type ViolationPrompt = { + member: RoomMember; + type: ViolationType; +}; + +/** + * The state of the UserIdentityWarningViewModel. + * This includes the current prompt to show to the user and a callback to handle button clicks. + * If currentPrompt is undefined, there are no violations to show. + */ +export interface UserIdentityWarningState { + currentPrompt?: ViolationPrompt; + dispatchAction: (action: UserIdentityWarningViewModelAction) => void; +} + +/** + * List of actions that can be dispatched to the UserIdentityWarningViewModel. + */ +export type UserIdentityWarningViewModelAction = + | { type: "PinUserIdentity"; userId: string } + | { type: "WithdrawVerification"; userId: string }; + +/** + * Maps a list of room members to a list of violations. + * Checks for all members in the room to see if they have any violations. + * If no violations are found, an empty list is returned. + * + * @param cryptoApi + * @param members - The list of room members to check for violations. + */ +async function mapToViolations(cryptoApi: CryptoApi, members: RoomMember[]): Promise { + const violationList = new Array(); + for (const member of members) { + const verificationStatus = await cryptoApi.getUserVerificationStatus(member.userId); + if (verificationStatus.wasCrossSigningVerified() && !verificationStatus.isCrossSigningVerified()) { + violationList.push({ member, type: "VerificationViolation" }); + } else if (verificationStatus.needsUserApproval) { + violationList.push({ member, type: "PinViolation" }); + } + } + return violationList; +} + +export function useUserIdentityWarningViewModel(room: Room, key: string): UserIdentityWarningState { + const cli = useMatrixClientContext(); + const crypto = cli.getCrypto(); + + const [members, setMembers] = useState([]); + const [currentPrompt, setCurrentPrompt] = useState(undefined); + + const loadViolations = useMemo( + () => + throttle(async (): Promise => { + const isEncrypted = crypto && (await crypto.isEncryptionEnabledInRoom(room.roomId)); + if (!isEncrypted) { + setMembers([]); + setCurrentPrompt(undefined); + return; + } + + const targetMembers = await room.getEncryptionTargetMembers(); + setMembers(targetMembers); + const violations = await mapToViolations(crypto, targetMembers); + + let candidatePrompt: ViolationPrompt | undefined; + if (violations.length > 0) { + // sort by user ID to ensure consistent ordering + const sortedViolations = violations.sort((a, b) => a.member.userId.localeCompare(b.member.userId)); + candidatePrompt = sortedViolations[0]; + } else { + candidatePrompt = undefined; + } + + // is the current prompt still valid? + setCurrentPrompt((existingPrompt): ViolationPrompt | undefined => { + if (existingPrompt && violations.includes(existingPrompt)) { + return existingPrompt; + } else if (candidatePrompt) { + return candidatePrompt; + } else { + return undefined; + } + }); + }), + [crypto, room], + ); + + // We need to listen for changes to the members list + useTypedEventEmitter( + cli, + RoomStateEvent.Events, + useCallback( + async (event: MatrixEvent): Promise => { + if (!crypto || event.getRoomId() !== room.roomId) { + return; + } + let shouldRefresh = false; + + const eventType = event.getType(); + + if (eventType === EventType.RoomEncryption && event.getStateKey() === "") { + // Room is now encrypted, so we can initialise the component. + shouldRefresh = true; + } else if (eventType == EventType.RoomMember) { + // We're processing an m.room.member event + // Something has changed in membership, someone joined or someone left or + // someone changed their display name. Anyhow let's refresh. + const userId = event.getStateKey(); + shouldRefresh = !!userId; + } + + if (shouldRefresh) { + loadViolations().catch((e) => { + logger.error("Error refreshing UserIdentityWarningViewModel:", e); + }); + } + }, + [crypto, room, loadViolations], + ), + ); + + // We need to listen for changes to the verification status of the members to refresh violations + useTypedEventEmitter( + cli, + CryptoEvent.UserTrustStatusChanged, + useCallback( + (userId: string): void => { + if (members.find((m) => m.userId == userId)) { + // This member is tracked, we need to refresh. + // refresh all for now? + // As a later optimisation we could store the current violations and only update the relevant one. + loadViolations().catch((e) => { + logger.error("Error refreshing UserIdentityWarning:", e); + }); + } + }, + [loadViolations, members], + ), + ); + + useEffect(() => { + loadViolations().catch((e) => { + logger.error("Error initialising UserIdentityWarning:", e); + }); + }, [loadViolations]); + + const dispatchAction = useCallback( + (action: UserIdentityWarningViewModelAction): void => { + if (!crypto) { + return; + } + if (action.type === "PinUserIdentity") { + crypto.pinCurrentUserIdentity(action.userId).catch((e) => { + logger.error("Error pinning user identity:", e); + }); + } else if (action.type === "WithdrawVerification") { + crypto.withdrawVerificationRequirement(action.userId).catch((e) => { + logger.error("Error withdrawing verification requirement:", e); + }); + } + }, + [crypto], + ); + + return { + currentPrompt, + dispatchAction, + }; +} diff --git a/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts b/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts new file mode 100644 index 0000000000..ee301bd27f --- /dev/null +++ b/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts @@ -0,0 +1,116 @@ +/* +Copyright 2025 New Vector Ltd. + +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 { useCallback, useEffect, useState } from "react"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../../../DeviceListener"; + +interface KeyStoragePanelState { + /** + * Whether the app's "key storage" option should show as enabled to the user, + * or 'undefined' if the state is still loading. + */ + isEnabled: boolean | undefined; + + /** + * A function that can be called to enable or disable key storage. + * @param enable True to turn key storage on or false to turn it off + */ + setEnabled: (enable: boolean) => void; + + /** + * True if the state is still loading for the first time + */ + loading: boolean; + + /** + * True if the status is in the process of being changed + */ + busy: boolean; +} + +/** Returns a ViewModel for use in {@link KeyStoragePanel} and {@link DeleteKeyStoragePanel}. */ +export function useKeyStoragePanelViewModel(): KeyStoragePanelState { + const [isEnabled, setIsEnabled] = useState(undefined); + const [loading, setLoading] = useState(true); + // Whilst the change is being made, the toggle will reflect the pending value rather than the actual state + const [pendingValue, setPendingValue] = useState(undefined); + + const matrixClient = useMatrixClientContext(); + + const checkStatus = useCallback(async () => { + const crypto = matrixClient.getCrypto(); + if (!crypto) { + logger.error("Can't check key backup status: no crypto module available"); + return; + } + // The toggle is enabled only if this device will upload megolm keys to the backup. + // This is consistent with EX. + const activeBackupVersion = await crypto.getActiveSessionBackupVersion(); + setIsEnabled(activeBackupVersion !== null); + }, [matrixClient]); + + useEffect(() => { + (async () => { + await checkStatus(); + setLoading(false); + })(); + }, [checkStatus]); + + const setEnabled = useCallback( + async (enable: boolean) => { + setPendingValue(enable); + try { + // stop the device listener since enabling or (especially) disabling key storage must be + // done with a sequence of API calls that will put the account in a slightly different + // state each time, so suppress any warning toasts until the process is finished (when + // we'll turn it back on again.) + DeviceListener.sharedInstance().stop(); + + const crypto = matrixClient.getCrypto(); + if (!crypto) { + logger.error("Can't change key backup status: no crypto module available"); + return; + } + if (enable) { + // If there is no existing key backup on the server, create one. + // `resetKeyBackup` will delete any existing backup, so we only do this if there is no existing backup. + const currentKeyBackup = await crypto.checkKeyBackupAndEnable(); + if (currentKeyBackup === null) { + await crypto.resetKeyBackup(); + + // resetKeyBackup fires this off in the background without waiting, so we need to do it + // explicitly and wait for it, otherwise it won't be enabled yet when we check again. + await crypto.checkKeyBackupAndEnable(); + } + + // Set the flag so that EX no longer thinks the user wants backup disabled + await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: false }); + } else { + // This method will delete the key backup as well as server side recovery keys and other + // server-side crypto data. + await crypto.disableKeyStorage(); + + // Set a flag to say that the user doesn't want key backup. + // Element X uses this to determine whether to set up automatically, + // so this will stop EX turning it back on spontaneously. + await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true }); + } + + await checkStatus(); + } finally { + setPendingValue(undefined); + DeviceListener.sharedInstance().start(matrixClient); + } + }, + [setPendingValue, checkStatus, matrixClient], + ); + + return { isEnabled: pendingValue ?? isEnabled, setEnabled, loading, busy: pendingValue !== undefined }; +} diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index 63c77108e3..6f674e504d 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import PlayPauseButton from "./PlayPauseButton"; import { formatBytes } from "../../../utils/FormattingUtils"; diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx index 97cbad0fc2..97fc49b9db 100644 --- a/src/components/views/audio_messages/AudioPlayerBase.tsx +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -6,16 +6,16 @@ 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, ReactNode, RefObject } from "react"; +import React, { createRef, type ReactNode, type RefObject } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { Playback, PlaybackState } from "../../../audio/Playback"; +import { type Playback, type PlaybackState } from "../../../audio/Playback"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { _t } from "../../../languageHandler"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -import SeekBar from "./SeekBar"; -import PlayPauseButton from "./PlayPauseButton"; +import type SeekBar from "./SeekBar"; +import type PlayPauseButton from "./PlayPauseButton"; export interface IProps { // Playback instance to render. Cannot change during component lifecycle: create diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index c8f27c3f9c..d9f1a3d6c5 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.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, { HTMLProps } from "react"; +import React, { type HTMLProps } from "react"; import { Temporal } from "temporal-polyfill"; import { formatSeconds } from "../../../DateUtils"; diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index 1a84a15955..920baa99be 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import Clock from "./Clock"; -import { Playback } from "../../../audio/Playback"; +import { type Playback } from "../../../audio/Playback"; interface IProps { playback: Playback; diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index bd8b8d5d23..74415566a6 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details. import React from "react"; -import { IRecordingUpdate } from "../../../audio/VoiceRecording"; +import { type IRecordingUpdate } from "../../../audio/VoiceRecording"; import Clock from "./Clock"; import { MarkedExecution } from "../../../utils/MarkedExecution"; -import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; +import { type VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; interface IProps { recorder: VoiceMessageRecording; diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index 18c1ca1aa2..2c388fe867 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -8,11 +8,11 @@ Please see LICENSE files in the repository root for full details. import React from "react"; -import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES } from "../../../audio/VoiceRecording"; +import { type IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES } from "../../../audio/VoiceRecording"; import { arrayFastResample, arraySeed } from "../../../utils/arrays"; import Waveform from "./Waveform"; import { MarkedExecution } from "../../../utils/MarkedExecution"; -import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; +import { type VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; interface IProps { recorder: VoiceMessageRecording; diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index 1b197c6bad..a4457b2230 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -6,12 +6,12 @@ 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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; -import { Playback, PlaybackState } from "../../../audio/Playback"; -import AccessibleButton, { ButtonProps } from "../elements/AccessibleButton"; +import { type Playback, PlaybackState } from "../../../audio/Playback"; +import AccessibleButton, { type ButtonProps } from "../elements/AccessibleButton"; type Props = Omit, "title" | "onClick" | "disabled" | "element" | "ref"> & { // Playback instance to manipulate. Cannot change during the component lifecycle. diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 999b5398b1..0d69793379 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import Clock from "./Clock"; -import { Playback, PlaybackState } from "../../../audio/Playback"; +import { type Playback, PlaybackState } from "../../../audio/Playback"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; interface IProps { diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index a5113dd042..c1f470f4b1 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -10,7 +10,7 @@ import React from "react"; import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import Waveform from "./Waveform"; -import { Playback } from "../../../audio/Playback"; +import { type Playback } from "../../../audio/Playback"; import { percentageOf } from "../../../utils/numbers"; import { PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/consts"; diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx index 4c030f81ef..c0e3337787 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -6,11 +6,11 @@ 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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import PlayPauseButton from "./PlayPauseButton"; import PlaybackClock from "./PlaybackClock"; -import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase"; +import AudioPlayerBase, { type IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase"; import SeekBar from "./SeekBar"; import PlaybackWaveform from "./PlaybackWaveform"; import { PlaybackState } from "../../../audio/Playback"; diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index 1a79f5be06..587975ce1b 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -6,9 +6,9 @@ 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, { ChangeEvent, CSSProperties, ReactNode } from "react"; +import React, { type ChangeEvent, type CSSProperties, type ReactNode } from "react"; -import { PlaybackInterface } from "../../../audio/Playback"; +import { type PlaybackInterface } from "../../../audio/Playback"; import { MarkedExecution } from "../../../utils/MarkedExecution"; import { percentageOf } from "../../../utils/numbers"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index 83d02b81fd..115b310f4c 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.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, { CSSProperties } from "react"; +import React, { type CSSProperties } from "react"; import classNames from "classnames"; interface WaveformCSSProperties extends CSSProperties { @@ -18,8 +18,6 @@ interface IProps { progress: number; // percent complete, 0-1, default 100% } -interface IState {} - /** * A simple waveform component. This renders bars (centered vertically) for each * height provided in the component properties. Updating the properties will update @@ -28,7 +26,7 @@ interface IState {} * For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be * "filled", as a demonstration of the progress property. */ -export default class Waveform extends React.PureComponent { +export default class Waveform extends React.PureComponent { public static defaultProps = { progress: 1, }; diff --git a/src/components/views/auth/AuthBody.tsx b/src/components/views/auth/AuthBody.tsx index b83955fcd9..ac1950b4a1 100644 --- a/src/components/views/auth/AuthBody.tsx +++ b/src/components/views/auth/AuthBody.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { PropsWithChildren } from "react"; +import React, { type PropsWithChildren } from "react"; interface Props { className?: string; diff --git a/src/components/views/auth/AuthFooter.tsx b/src/components/views/auth/AuthFooter.tsx index 472ff53f09..f609b4792a 100644 --- a/src/components/views/auth/AuthFooter.tsx +++ b/src/components/views/auth/AuthFooter.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, { ReactElement } from "react"; +import React, { type ReactElement } from "react"; import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/auth/CompleteSecurityBody.tsx b/src/components/views/auth/CompleteSecurityBody.tsx index 77808ec71c..01eb0ec6f5 100644 --- a/src/components/views/auth/CompleteSecurityBody.tsx +++ b/src/components/views/auth/CompleteSecurityBody.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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; export default class CompleteSecurityBody extends React.PureComponent<{ children: ReactNode }> { public render(): React.ReactNode { diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index 7e8d669d15..f25ed95e96 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -6,13 +6,13 @@ 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, { ReactElement } from "react"; +import React, { type ReactElement } from "react"; -import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from "../../../phonenumber"; +import { COUNTRIES, getEmojiFlag, type PhoneNumberCountryDefinition } from "../../../phonenumber"; import SdkConfig from "../../../SdkConfig"; import { _t, getUserLanguage } from "../../../languageHandler"; import Dropdown from "../elements/Dropdown"; -import { NonEmptyArray } from "../../../@types/common"; +import { type NonEmptyArray } from "../../../@types/common"; interface InternationalisedCountry extends PhoneNumberCountryDefinition { name: string; // already translated to the user's locale diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index ddc20c0bd1..9b330a377d 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -6,11 +6,11 @@ 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, { ComponentProps, PureComponent, RefCallback, RefObject } from "react"; +import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react"; -import Field, { IInputProps } from "../elements/Field"; -import { _t, _td, TranslationKey } from "../../../languageHandler"; -import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; +import Field, { type IInputProps } from "../elements/Field"; +import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; import * as Email from "../../../email"; interface IProps extends Omit { diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index d493e5c3ca..cc22ba3c3a 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -7,23 +7,22 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth"; +import { type InternationalisedPolicy, type Terms, type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { AuthType, type AuthDict, type IInputs, type IStageStatus } from "matrix-js-sdk/src/interactive-auth"; import { logger } from "matrix-js-sdk/src/logger"; -import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react"; +import React, { type ChangeEvent, createRef, type FormEvent, Fragment } from "react"; import { Button, Text } from "@vector-im/compound-web"; import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg"; import { _t } from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; -import { LocalisedPolicy, Policies } from "../../../Terms"; import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier"; -import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { type AccessibleButtonKind, type ButtonEvent } from "../elements/AccessibleButton"; import Field from "../elements/Field"; import Spinner from "../elements/Spinner"; import CaptchaForm from "./CaptchaForm"; import { Flex } from "../../utils/Flex"; +import { pickBestPolicyLanguage } from "../../../Terms.ts"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -86,7 +85,6 @@ interface IAuthEntryProps { requestEmailToken?: () => Promise; fail: (error: Error) => void; clientSecret: string; - showContinue: boolean; } interface IPasswordAuthEntryState { @@ -235,12 +233,10 @@ export class RecaptchaAuthEntry extends React.Component; } -interface LocalisedPolicyWithId extends LocalisedPolicy { +interface LocalisedPolicyWithId extends InternationalisedPolicy { id: string; } @@ -278,7 +274,6 @@ export class TermsAuthEntry extends React.Component = {}; const pickedPolicies: { id: string; @@ -287,17 +282,7 @@ export class TermsAuthEntry extends React.Component e !== "version"); - langPolicy = firstLang ? policy[firstLang] : undefined; - } + const langPolicy = pickBestPolicyLanguage(policy); if (!langPolicy) throw new Error("Failed to find a policy to show the user"); initToggles[policyId] = false; @@ -375,9 +360,11 @@ export class TermsAuthEntry extends React.Component +

    {_t("auth|uia|terms")}

    + {checkboxes} + {errorSection} {_t("action|accept")} - ); - } - - return ( -
    -

    {_t("auth|uia|terms")}

    - {checkboxes} - {errorSection} - {submitButton}
    ); } @@ -908,7 +886,7 @@ export class SSOAuthEntry extends React.Component extends React.Component { +export class FallbackAuthEntry extends React.Component { protected popupWindow: Window | null; protected fallbackButton = createRef(); diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index ecf107cfbd..1519a1fb45 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -14,11 +14,11 @@ import { MSC4108SecureChannel, MSC4108SignInWithQR, RendezvousError, - RendezvousFailureReason, + type RendezvousFailureReason, RendezvousIntent, } from "matrix-js-sdk/src/rendezvous"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { Click, Mode, Phase } from "./LoginWithQR-types"; import LoginWithQRFlow from "./LoginWithQRFlow"; diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 663dc1acff..b246013421 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -6,11 +6,11 @@ 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, ReactNode } from "react"; +import React, { createRef, type ReactNode } from "react"; import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; import ChevronLeftIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-left"; import CheckCircleSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid"; -import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import { Heading, MFAInput, Text } from "@vector-im/compound-web"; import classNames from "classnames"; import { QrCodeIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -21,7 +21,7 @@ import QRCode from "../elements/QRCode"; import Spinner from "../elements/Spinner"; import { Click, Phase } from "./LoginWithQR-types"; import SdkConfig from "../../../SdkConfig"; -import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR"; +import { type FailureReason, LoginWithQRFailureReason } from "./LoginWithQR"; import { ErrorMessage } from "../../structures/ErrorMessage"; interface Props { diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index 0337c80359..bd20b59929 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -6,11 +6,11 @@ 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, { ComponentProps, PureComponent, RefCallback, RefObject } from "react"; +import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react"; -import Field, { IInputProps } from "../elements/Field"; -import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { _t, _td, TranslationKey } from "../../../languageHandler"; +import Field, { type IInputProps } from "../elements/Field"; +import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; +import { _t, _td, type TranslationKey } from "../../../languageHandler"; interface IProps extends Omit { id?: string; diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index 8fc56cc68c..ce251d4d9b 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -6,14 +6,14 @@ 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, { ComponentProps, PureComponent, RefCallback, RefObject } from "react"; +import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react"; import classNames from "classnames"; import type { ZxcvbnResult } from "@zxcvbn-ts/core"; import SdkConfig from "../../../SdkConfig"; -import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { _t, _td, TranslationKey } from "../../../languageHandler"; -import Field, { IInputProps } from "../elements/Field"; +import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; +import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import Field, { type IInputProps } from "../elements/Field"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; interface IProps extends Omit { diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index e13af99f6f..d394f475a6 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -6,18 +6,18 @@ 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, { SyntheticEvent } from "react"; +import React, { type SyntheticEvent } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; -import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; +import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; +import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; import Field from "../elements/Field"; import CountryDropdown from "./CountryDropdown"; import EmailField from "./EmailField"; -import { PhoneNumberCountryDefinition } from "../../../phonenumber"; +import { type PhoneNumberCountryDefinition } from "../../../phonenumber"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index ef2c83d3be..44dfa64ba5 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -7,18 +7,18 @@ 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, { BaseSyntheticEvent, ComponentProps, ReactNode } from "react"; -import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import React, { type BaseSyntheticEvent, type ComponentProps, type ReactNode } from "react"; +import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import * as Email from "../../../email"; -import { looksValid as phoneNumberLooksValid, PhoneNumberCountryDefinition } from "../../../phonenumber"; +import { looksValid as phoneNumberLooksValid, type PhoneNumberCountryDefinition } from "../../../phonenumber"; import Modal from "../../../Modal"; import { _t, _td } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; import { SAFE_LOCALPART_REGEX } from "../../../Registration"; -import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; +import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; +import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import EmailField from "./EmailField"; import PassphraseField from "./PassphraseField"; import Field from "../elements/Field"; diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx index 39d5f95a8d..59fd1533e3 100644 --- a/src/components/views/auth/Welcome.tsx +++ b/src/components/views/auth/Welcome.tsx @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; +import { type EmptyObject } from "matrix-js-sdk/src/matrix"; import SdkConfig from "../../../SdkConfig"; import AuthPage from "./AuthPage"; @@ -16,9 +17,7 @@ import LanguageSelector from "./LanguageSelector"; import EmbeddedPage from "../../structures/EmbeddedPage"; import { MATRIX_LOGO_HTML } from "../../structures/static-page-vars"; -interface IProps {} - -export default class Welcome extends React.PureComponent { +export default class Welcome extends React.PureComponent { public render(): React.ReactNode { const pagesConfig = SdkConfig.getObject("embedded_pages"); let pageUrl: string | undefined; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 942650c65a..8aabf60149 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -9,13 +9,13 @@ 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, { AriaRole, forwardRef, useCallback, useContext, useEffect, useState } from "react"; +import React, { type AriaRole, forwardRef, useCallback, useContext, useEffect, useState } from "react"; import classNames from "classnames"; -import { ClientEvent, SyncState } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, type SyncState } from "matrix-js-sdk/src/matrix"; import { Avatar } from "@vector-im/compound-web"; import SettingsStore from "../../../settings/SettingsStore"; -import { ButtonEvent } from "../elements/AccessibleButton"; +import { type ButtonEvent } from "../elements/AccessibleButton"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 5a11e0f6b2..89b89af12e 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -8,19 +8,27 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; -import { EventType, JoinRule, MatrixEvent, Room, RoomEvent, User, UserEvent } from "matrix-js-sdk/src/matrix"; +import { + EventType, + JoinRule, + type MatrixEvent, + type Room, + RoomEvent, + type User, + UserEvent, +} from "matrix-js-sdk/src/matrix"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { Tooltip } from "@vector-im/compound-web"; import RoomAvatar from "./RoomAvatar"; import NotificationBadge from "../rooms/NotificationBadge"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; -import { NotificationState } from "../../../stores/notifications/NotificationState"; +import { type NotificationState } from "../../../stores/notifications/NotificationState"; import { isPresenceEnabled } from "../../../utils/presence"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import DMRoomMap from "../../../utils/DMRoomMap"; -import { IOOBData } from "../../../stores/ThreepidInviteStore"; +import { type IOOBData } from "../../../stores/ThreepidInviteStore"; import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers"; interface IProps { diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 641fe4a783..a5bcc211e5 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -7,8 +7,8 @@ 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, { forwardRef, ReactNode, Ref, useContext } from "react"; -import { RoomMember, ResizeMethod } from "matrix-js-sdk/src/matrix"; +import React, { forwardRef, type ReactNode, type Ref, useContext } from "react"; +import { type RoomMember, type ResizeMethod } from "matrix-js-sdk/src/matrix"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index e1d71ac1aa..e1e3e9157d 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -1,13 +1,20 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2015, 2016 OpenMarket Ltd 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, { ComponentProps } from "react"; -import { Room, RoomStateEvent, MatrixEvent, EventType, RoomType } from "matrix-js-sdk/src/matrix"; +import React, { type ComponentProps } from "react"; +import { + type Room, + RoomStateEvent, + type MatrixEvent, + EventType, + RoomType, + KnownMembership, +} from "matrix-js-sdk/src/matrix"; import BaseAvatar from "./BaseAvatar"; import ImageView from "../elements/ImageView"; @@ -16,9 +23,10 @@ import Modal from "../../../Modal"; import * as Avatar from "../../../Avatar"; import DMRoomMap from "../../../utils/DMRoomMap"; import { mediaFromMxc } from "../../../customisations/Media"; -import { IOOBData } from "../../../stores/ThreepidInviteStore"; +import { type IOOBData } from "../../../stores/ThreepidInviteStore"; import { LocalRoom } from "../../../models/LocalRoom"; import { filterBoolean } from "../../../utils/arrays"; +import SettingsStore from "../../../settings/SettingsStore"; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, @@ -86,6 +94,13 @@ export default class RoomAvatar extends React.Component { }; private static getImageUrls(props: IProps): string[] { + const myMembership = props.room?.getMyMembership(); + if (myMembership === KnownMembership.Invite || !myMembership) { + if (SettingsStore.getValue("showAvatarsOnInvites") === false) { + // The user has opted out of showing avatars, so return no urls here. + return []; + } + } let oobAvatar: string | null = null; if (props.oobData.avatarUrl) { oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp( diff --git a/src/components/views/avatars/SearchResultAvatar.tsx b/src/components/views/avatars/SearchResultAvatar.tsx index c50c4d81b2..c17dd21736 100644 --- a/src/components/views/avatars/SearchResultAvatar.tsx +++ b/src/components/views/avatars/SearchResultAvatar.tsx @@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { RoomMember } from "matrix-js-sdk/src/matrix"; +import { type RoomMember } from "matrix-js-sdk/src/matrix"; import emailPillAvatar from "../../../../res/img/icon-email-pill-avatar.svg"; import { mediaFromMxc } from "../../../customisations/Media"; -import { Member, ThreepidMember } from "../../../utils/direct-messages"; +import { type Member, type ThreepidMember } from "../../../utils/direct-messages"; import BaseAvatar from "./BaseAvatar"; interface SearchResultAvatarProps { diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx index f6d73e7d1c..c43cd98216 100644 --- a/src/components/views/avatars/WidgetAvatar.tsx +++ b/src/components/views/avatars/WidgetAvatar.tsx @@ -6,12 +6,12 @@ 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, { ComponentProps } from "react"; -import { IWidget } from "matrix-widget-api"; +import React, { type ComponentProps } from "react"; +import { type IWidget } from "matrix-widget-api"; import classNames from "classnames"; -import { IApp, isAppWidget } from "../../../stores/WidgetStore"; -import BaseAvatar, { BaseAvatarType } from "./BaseAvatar"; +import { type IApp, isAppWidget } from "../../../stores/WidgetStore"; +import BaseAvatar, { type BaseAvatarType } from "./BaseAvatar"; import { mediaFromMxc } from "../../../customisations/Media"; interface IProps extends Omit, "name" | "url" | "urls"> { diff --git a/src/components/views/avatars/WithPresenceIndicator.tsx b/src/components/views/avatars/WithPresenceIndicator.tsx index 9d10f8dce6..4d60daf494 100644 --- a/src/components/views/avatars/WithPresenceIndicator.tsx +++ b/src/components/views/avatars/WithPresenceIndicator.tsx @@ -6,8 +6,8 @@ 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, { ReactNode, useEffect, useState } from "react"; -import { ClientEvent, Room, RoomMember, RoomStateEvent, UserEvent } from "matrix-js-sdk/src/matrix"; +import React, { type ReactNode, useEffect, useState } from "react"; +import { ClientEvent, type Room, type RoomMember, RoomStateEvent, UserEvent } from "matrix-js-sdk/src/matrix"; import { Tooltip } from "@vector-im/compound-web"; import { isPresenceEnabled } from "../../../utils/presence"; diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx index 01a7f9364a..ceedbf05fd 100644 --- a/src/components/views/beacon/BeaconListItem.tsx +++ b/src/components/views/beacon/BeaconListItem.tsx @@ -6,8 +6,8 @@ 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, { HTMLProps, useContext } from "react"; -import { Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix"; +import React, { type HTMLProps, useContext } from "react"; +import { type Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx index 01d74e72b1..262901233b 100644 --- a/src/components/views/beacon/BeaconMarker.tsx +++ b/src/components/views/beacon/BeaconMarker.tsx @@ -6,10 +6,10 @@ 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, { ReactNode, useContext } from "react"; -import * as maplibregl from "maplibre-gl"; -import { Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix"; +import React, { type ReactNode, useContext } from "react"; +import { type Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix"; +import type * as maplibregl from "maplibre-gl"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { SmartMarker } from "../location"; diff --git a/src/components/views/beacon/BeaconStatus.tsx b/src/components/views/beacon/BeaconStatus.tsx index 1415dc229c..0ed7ceb4b0 100644 --- a/src/components/views/beacon/BeaconStatus.tsx +++ b/src/components/views/beacon/BeaconStatus.tsx @@ -6,9 +6,9 @@ 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, { HTMLProps } from "react"; +import React, { type HTMLProps } from "react"; import classNames from "classnames"; -import { Beacon } from "matrix-js-sdk/src/matrix"; +import { type Beacon } from "matrix-js-sdk/src/matrix"; import StyledLiveBeaconIcon from "./StyledLiveBeaconIcon"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/beacon/BeaconStatusTooltip.tsx b/src/components/views/beacon/BeaconStatusTooltip.tsx index 1dc1b05e61..eb8a76f1c1 100644 --- a/src/components/views/beacon/BeaconStatusTooltip.tsx +++ b/src/components/views/beacon/BeaconStatusTooltip.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { useContext } from "react"; -import { Beacon, LocationAssetType } from "matrix-js-sdk/src/matrix"; +import { type Beacon, LocationAssetType } from "matrix-js-sdk/src/matrix"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import BeaconStatus from "./BeaconStatus"; diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 27f9f2e520..d779ebbf2e 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details. */ import React, { useState, useEffect } from "react"; -import { MatrixClient, Beacon, Room } from "matrix-js-sdk/src/matrix"; -import * as maplibregl from "maplibre-gl"; +import { type MatrixClient, type Beacon, type Room } from "matrix-js-sdk/src/matrix"; +import type * as maplibregl from "maplibre-gl"; import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-location.svg"; import { useLiveBeacons } from "../../../utils/beacon/useLiveBeacons"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -17,7 +17,7 @@ import BaseDialog from "../dialogs/BaseDialog"; import Map from "../location/Map"; import ZoomButtons from "../location/ZoomButtons"; import BeaconMarker from "./BeaconMarker"; -import { Bounds, getBeaconBounds } from "../../../utils/beacon/bounds"; +import { type Bounds, getBeaconBounds } from "../../../utils/beacon/bounds"; import { getGeoUri } from "../../../utils/beacon"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; @@ -26,7 +26,7 @@ import DialogOwnBeaconStatus from "./DialogOwnBeaconStatus"; import BeaconStatusTooltip from "./BeaconStatusTooltip"; import MapFallback from "../location/MapFallback"; import { MapError } from "../location/MapError"; -import { LocationShareError } from "../../../utils/location"; +import { type LocationShareError } from "../../../utils/location"; export interface IProps { roomId: Room["roomId"]; diff --git a/src/components/views/beacon/DialogOwnBeaconStatus.tsx b/src/components/views/beacon/DialogOwnBeaconStatus.tsx index e6b35d9247..fff6b0db4b 100644 --- a/src/components/views/beacon/DialogOwnBeaconStatus.tsx +++ b/src/components/views/beacon/DialogOwnBeaconStatus.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { useContext } from "react"; -import { Room, Beacon, LocationAssetType } from "matrix-js-sdk/src/matrix"; +import { type Room, type Beacon, LocationAssetType } from "matrix-js-sdk/src/matrix"; import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; diff --git a/src/components/views/beacon/DialogSidebar.tsx b/src/components/views/beacon/DialogSidebar.tsx index 9fcca26dde..ee86e6bcf0 100644 --- a/src/components/views/beacon/DialogSidebar.tsx +++ b/src/components/views/beacon/DialogSidebar.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { Beacon } from "matrix-js-sdk/src/matrix"; +import { type Beacon } from "matrix-js-sdk/src/matrix"; import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx index 0c3f26f91e..d5338cf28f 100644 --- a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx +++ b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx @@ -8,16 +8,16 @@ Please see LICENSE files in the repository root for full details. import classNames from "classnames"; import React, { useEffect } from "react"; -import { Beacon, BeaconIdentifier } from "matrix-js-sdk/src/matrix"; +import { type Beacon, type BeaconIdentifier } from "matrix-js-sdk/src/matrix"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { _t } from "../../../languageHandler"; import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore"; import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-location.svg"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; import dispatcher from "../../../dispatcher/dispatcher"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; interface Props { isMinimized?: boolean; diff --git a/src/components/views/beacon/LiveTimeRemaining.tsx b/src/components/views/beacon/LiveTimeRemaining.tsx index c0730938ee..d4d7ca262e 100644 --- a/src/components/views/beacon/LiveTimeRemaining.tsx +++ b/src/components/views/beacon/LiveTimeRemaining.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { useCallback, useEffect, useState } from "react"; -import { BeaconEvent, Beacon } from "matrix-js-sdk/src/matrix"; +import { BeaconEvent, type Beacon } from "matrix-js-sdk/src/matrix"; import { formatDuration } from "../../../DateUtils"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx index 7d0f277786..8ece201cbe 100644 --- a/src/components/views/beacon/OwnBeaconStatus.tsx +++ b/src/components/views/beacon/OwnBeaconStatus.tsx @@ -6,15 +6,15 @@ 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 { Beacon } from "matrix-js-sdk/src/matrix"; -import React, { HTMLProps } from "react"; +import { type Beacon } from "matrix-js-sdk/src/matrix"; +import React, { type HTMLProps } from "react"; import { _t } from "../../../languageHandler"; import { useOwnLiveBeacons } from "../../../utils/beacon"; import { preventDefaultWrapper } from "../../../utils/NativeEventUtils"; import BeaconStatus from "./BeaconStatus"; import { BeaconDisplayStatus } from "./displayStatus"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; interface Props { displayStatus: BeaconDisplayStatus; diff --git a/src/components/views/beacon/RoomCallBanner.tsx b/src/components/views/beacon/RoomCallBanner.tsx index a9f25e7e6c..e4f0dfa608 100644 --- a/src/components/views/beacon/RoomCallBanner.tsx +++ b/src/components/views/beacon/RoomCallBanner.tsx @@ -7,15 +7,15 @@ Please see LICENSE files in the repository root for full details. */ import React, { useCallback } from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; -import { ConnectionState, ElementCall } from "../../../models/Call"; +import { ConnectionState, type ElementCall } from "../../../models/Call"; import { useCall } from "../../../hooks/useCall"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore"; diff --git a/src/components/views/beacon/ShareLatestLocation.tsx b/src/components/views/beacon/ShareLatestLocation.tsx index 5300a81900..b18cdff839 100644 --- a/src/components/views/beacon/ShareLatestLocation.tsx +++ b/src/components/views/beacon/ShareLatestLocation.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { useEffect, useState } from "react"; -import { ContentHelpers } from "matrix-js-sdk/src/matrix"; +import { type ContentHelpers } from "matrix-js-sdk/src/matrix"; import { Tooltip } from "@vector-im/compound-web"; import { Icon as ExternalLinkIcon } from "../../../../res/img/external-link.svg"; diff --git a/src/components/views/beacon/displayStatus.ts b/src/components/views/beacon/displayStatus.ts index e11b0018ba..15320c2fa5 100644 --- a/src/components/views/beacon/displayStatus.ts +++ b/src/components/views/beacon/displayStatus.ts @@ -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 { ContentHelpers } from "matrix-js-sdk/src/matrix"; +import { type ContentHelpers } from "matrix-js-sdk/src/matrix"; export enum BeaconDisplayStatus { Loading = "Loading", diff --git a/src/components/views/beacon/index.tsx b/src/components/views/beacon/index.tsx index 871e7cb07e..7cbb6328bf 100644 --- a/src/components/views/beacon/index.tsx +++ b/src/components/views/beacon/index.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. // Exports beacon components which touch maplibre-gs wrapped in React Suspense to enable code splitting -import React, { ComponentProps, lazy, Suspense } from "react"; +import React, { type ComponentProps, lazy, Suspense } from "react"; import Spinner from "../elements/Spinner"; diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 93d69dffce..4421a9e50b 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.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, { ReactNode, useState } from "react"; +import React, { type ReactNode, useState } from "react"; import { sleep } from "matrix-js-sdk/src/utils"; import { _t } from "../../../languageHandler"; @@ -20,7 +20,7 @@ import SettingsFlag from "../elements/SettingsFlag"; import { useFeatureEnabled } from "../../../hooks/useSettings"; import InlineSpinner from "../elements/InlineSpinner"; import { shouldShowFeedback } from "../../../utils/Feedback"; -import { FeatureSettingKey } from "../../../settings/Settings.tsx"; +import { type FeatureSettingKey } from "../../../settings/Settings.tsx"; // XXX: Keep this around for re-use in future Betas diff --git a/src/components/views/context_menus/DeviceContextMenu.tsx b/src/components/views/context_menus/DeviceContextMenu.tsx index 5d71049fb4..b6646c05ec 100644 --- a/src/components/views/context_menus/DeviceContextMenu.tsx +++ b/src/components/views/context_menus/DeviceContextMenu.tsx @@ -10,8 +10,8 @@ import React, { useEffect, useState } from "react"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler"; import IconizedContextMenu, { IconizedContextMenuOptionList, IconizedContextMenuRadio } from "./IconizedContextMenu"; -import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; -import { _t, _td, TranslationKey } from "../../../languageHandler"; +import { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; +import { _t, _td, type TranslationKey } from "../../../languageHandler"; const SECTION_NAMES: Record = { [MediaDeviceKindEnum.AudioInput]: _td("voip|input_devices"), diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index c4d0d73786..d194d02656 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -6,12 +6,11 @@ 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 { createRef } from "react"; -import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import React, { createRef } from "react"; +import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; -import ContextMenu, { IProps as IContextMenuProps } from "../../structures/ContextMenu"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; +import ContextMenu, { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; import Field from "../elements/Field"; import DialPad from "../voip/DialPad"; diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index 17a3805513..4d74ed24b1 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -6,12 +6,12 @@ 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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import classNames from "classnames"; import ContextMenu, { ChevronFace, - IProps as IContextMenuProps, + type IProps as IContextMenuProps, MenuItem, MenuItemCheckbox, MenuItemRadio, diff --git a/src/components/views/context_menus/KebabContextMenu.tsx b/src/components/views/context_menus/KebabContextMenu.tsx index e65f0389ff..f6fc9972a0 100644 --- a/src/components/views/context_menus/KebabContextMenu.tsx +++ b/src/components/views/context_menus/KebabContextMenu.tsx @@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import ContextMenuIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; -import { ChevronFace, ContextMenuButton, MenuProps, useContextMenu } from "../../structures/ContextMenu"; -import { ButtonProps } from "../elements/AccessibleButton"; +import { ChevronFace, ContextMenuButton, type MenuProps, useContextMenu } from "../../structures/ContextMenu"; +import { type ButtonProps } from "../elements/AccessibleButton"; import IconizedContextMenu, { IconizedContextMenuOptionList } from "./IconizedContextMenu"; const contextMenuBelow = (elementRect: DOMRect): MenuProps => { diff --git a/src/components/views/context_menus/LegacyCallContextMenu.tsx b/src/components/views/context_menus/LegacyCallContextMenu.tsx index bc3deab7a1..3427ed183b 100644 --- a/src/components/views/context_menus/LegacyCallContextMenu.tsx +++ b/src/components/views/context_menus/LegacyCallContextMenu.tsx @@ -6,10 +6,10 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { _t } from "../../../languageHandler"; -import ContextMenu, { IProps as IContextMenuProps, MenuItem } from "../../structures/ContextMenu"; +import ContextMenu, { type IProps as IContextMenuProps, MenuItem } from "../../structures/ContextMenu"; import LegacyCallHandler from "../../../LegacyCallHandler"; interface IProps extends IContextMenuProps { diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 0af3604fca..82168a59bc 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -11,12 +11,12 @@ Please see LICENSE files in the repository root for full details. import React, { createRef, useContext } from "react"; import { EventStatus, - MatrixEvent, + type MatrixEvent, MatrixEventEvent, RoomMemberEvent, EventType, RelationType, - Relations, + type Relations, Thread, M_POLL_START, } from "matrix-js-sdk/src/matrix"; @@ -31,10 +31,10 @@ import { isUrlPermitted } from "../../../HtmlUtils"; import { canEditContent, editEvent, isContentActionable } from "../../../utils/EventUtils"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { Action } from "../../../dispatcher/actions"; -import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -import { ButtonEvent } from "../elements/AccessibleButton"; +import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { type ButtonEvent } from "../elements/AccessibleButton"; import { copyPlaintext, getSelectedText } from "../../../utils/strings"; -import ContextMenu, { toRightOf, MenuProps } from "../../structures/ContextMenu"; +import ContextMenu, { toRightOf, type MenuProps } from "../../structures/ContextMenu"; import ReactionPicker from "../emojipicker/ReactionPicker"; import ViewSource from "../../structures/ViewSource"; import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog"; @@ -42,14 +42,14 @@ import { ShareDialog } from "../dialogs/ShareDialog"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import EndPollDialog from "../dialogs/EndPollDialog"; import { isPollEnded } from "../messages/MPollBody"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; -import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload"; -import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload"; +import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTile"; +import { type OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload"; +import { type OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload"; import { createMapSiteLinkFromEvent } from "../../../utils/location"; import { getForwardableEvent } from "../../../events/forward/getForwardableEvent"; import { getShareableLocationEvent } from "../../../events/location/getShareableLocationEvent"; -import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; +import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { CardContext } from "../right_panel/context"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; @@ -130,8 +130,8 @@ export default class MessageContextMenu extends React.Component private reactButtonRef = createRef(); // XXX Ref to a functional component - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { canRedact: false, diff --git a/src/components/views/context_menus/RoomGeneralContextMenu.tsx b/src/components/views/context_menus/RoomGeneralContextMenu.tsx index c54aa1e465..e6ba9cfc11 100644 --- a/src/components/views/context_menus/RoomGeneralContextMenu.tsx +++ b/src/components/views/context_menus/RoomGeneralContextMenu.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { type Room } from "matrix-js-sdk/src/matrix"; import React, { useContext } from "react"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; @@ -19,17 +19,17 @@ import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { _t } from "../../../languageHandler"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; -import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import { DefaultTagID, type TagID } from "../../../stores/room-list/models"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import DMRoomMap from "../../../utils/DMRoomMap"; import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications"; -import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; +import { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; import IconizedContextMenu, { IconizedContextMenuCheckbox, IconizedContextMenuOption, IconizedContextMenuOptionList, } from "../context_menus/IconizedContextMenu"; -import { ButtonEvent } from "../elements/AccessibleButton"; +import { type ButtonEvent } from "../elements/AccessibleButton"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { DeveloperToolsOption } from "./DeveloperToolsOption"; diff --git a/src/components/views/context_menus/RoomNotificationContextMenu.tsx b/src/components/views/context_menus/RoomNotificationContextMenu.tsx index ea63880762..14e38f9e09 100644 --- a/src/components/views/context_menus/RoomNotificationContextMenu.tsx +++ b/src/components/views/context_menus/RoomNotificationContextMenu.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 { Room } from "matrix-js-sdk/src/matrix"; +import { type Room } from "matrix-js-sdk/src/matrix"; import React from "react"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; @@ -14,12 +14,12 @@ import { useNotificationState } from "../../../hooks/useRoomNotificationState"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { _t } from "../../../languageHandler"; import { RoomNotifState } from "../../../RoomNotifs"; -import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; +import { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; import IconizedContextMenu, { IconizedContextMenuOptionList, IconizedContextMenuRadio, } from "../context_menus/IconizedContextMenu"; -import { ButtonEvent } from "../elements/AccessibleButton"; +import { type ButtonEvent } from "../elements/AccessibleButton"; interface IProps extends IContextMenuProps { room: Room; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx index 11a1364d6d..17018da57c 100644 --- a/src/components/views/context_menus/SpaceContextMenu.tsx +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details. */ import React, { useContext } from "react"; -import { Room, EventType, RoomType } from "matrix-js-sdk/src/matrix"; +import { type Room, EventType, RoomType } from "matrix-js-sdk/src/matrix"; -import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; +import { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { _t } from "../../../languageHandler"; import { @@ -22,7 +22,7 @@ import { } from "../../../utils/space"; import { leaveSpace } from "../../../utils/leave-behaviour"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { ButtonEvent } from "../elements/AccessibleButton"; +import { type ButtonEvent } from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { BetaPill } from "../beta/BetaCard"; import SettingsStore from "../../../settings/SettingsStore"; @@ -31,7 +31,7 @@ import { Action } from "../../../dispatcher/actions"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import PosthogTrackers from "../../../PosthogTrackers"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; interface IProps extends IContextMenuProps { space?: Room; diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index eea9815954..ef32d62282 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -7,19 +7,19 @@ Please see LICENSE files in the repository root for full details. */ import React, { useCallback, useEffect } from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { ButtonEvent } from "../elements/AccessibleButton"; +import { type ButtonEvent } from "../elements/AccessibleButton"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; -import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../../utils/strings"; -import { ChevronFace, ContextMenuTooltipButton, MenuProps, useContextMenu } from "../../structures/ContextMenu"; +import { ChevronFace, ContextMenuTooltipButton, type MenuProps, useContextMenu } from "../../structures/ContextMenu"; import { _t } from "../../../languageHandler"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; export interface ThreadListContextMenuProps { mxEvent: MatrixEvent; diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 894e8f8c7d..2b13f636a6 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -6,11 +6,11 @@ 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, { ComponentProps, useContext } from "react"; -import { ClientWidgetApi, IWidget, MatrixCapabilities } from "matrix-widget-api"; +import React, { type ComponentProps, useContext } from "react"; +import { type ClientWidgetApi, type IWidget, MatrixCapabilities } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; -import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { type ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; +import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { ChevronFace } from "../../structures/ContextMenu"; diff --git a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx index d7311244b7..09a828194a 100644 --- a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { useState } from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 3cc62f4155..c247c3aea9 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -1,27 +1,27 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 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, { ReactElement, ReactNode, useContext, useMemo, useRef, useState } from "react"; +import React, { type ReactElement, type ReactNode, useContext, useId, useMemo, useRef, useState } from "react"; import classNames from "classnames"; -import { Room, EventType } from "matrix-js-sdk/src/matrix"; +import { type Room, EventType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { sleep } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { _t, _td, TranslationKey } from "../../../languageHandler"; +import { _t, _td, type TranslationKey } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import RoomAvatar from "../avatars/RoomAvatar"; import { getDisplayAliasForRoom } from "../../../Rooms"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import DMRoomMap from "../../../utils/DMRoomMap"; import { calculateRoomVia } from "../../../utils/permalinks/Permalinks"; @@ -34,7 +34,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher"; import LazyRenderList from "../elements/LazyRenderList"; import { useSettingValue } from "../../../hooks/useSettings"; import { filterBoolean } from "../../../utils/arrays"; -import { NonEmptyArray } from "../../../@types/common"; +import { type NonEmptyArray } from "../../../@types/common"; // These values match CSS const ROW_HEIGHT = 32 + 12; @@ -53,8 +53,9 @@ export const Entry: React.FC<{ checked: boolean; onChange?(value: boolean): void; }> = ({ room, checked, onChange }) => { + const id = useId(); return ( -
  • {room?.isSpaceRoom() ? ( ) : ( @@ -62,11 +63,12 @@ export const Entry: React.FC<{ )} {room.name} onChange(e.currentTarget.checked) : undefined} checked={checked} disabled={!onChange} /> - +
  • ); }; @@ -357,6 +359,7 @@ const defaultRendererFactory =

    {_t(title)}

    { - const desktopBuilds = SdkConfig.getObject("desktop_builds"); - const mobileBuilds = SdkConfig.getObject("mobile_builds"); - - return ( - !!desktopBuilds?.get("available") || - !!mobileBuilds?.get("ios") || - !!mobileBuilds?.get("android") || - !!mobileBuilds?.get("fdroid") - ); -}; - -export const AppDownloadDialog: FC = ({ onFinished }) => { - const brand = SdkConfig.get("brand"); - const desktopBuilds = SdkConfig.getObject("desktop_builds"); - const mobileBuilds = SdkConfig.getObject("mobile_builds"); - - const urlAppStore = mobileBuilds?.get("ios"); - - const urlGooglePlay = mobileBuilds?.get("android"); - const urlFDroid = mobileBuilds?.get("fdroid"); - const urlAndroid = urlGooglePlay ?? urlFDroid; - - return ( - - {desktopBuilds?.get("available") && ( -
    - {_t("onboarding|download_brand_desktop", { brand })} - {}} - > - {_t("onboarding|download_brand_desktop", { brand })} - -
    - )} -
    - {urlAppStore && ( -
    - {_t("common|ios")} - -
    - {_t("onboarding|qr_or_app_links", { - appLinks: "", - qrCode: "", - })} -
    -
    - {}} - > - - -
    -
    - )} - {urlAndroid && ( -
    - {_t("common|android")} - -
    - {_t("onboarding|qr_or_app_links", { - appLinks: "", - qrCode: "", - })} -
    -
    - {urlGooglePlay && ( - {}} - > - - - )} - {urlFDroid && ( - {}} - > - - - )} -
    -
    - )} -
    -
    -

    {_t("onboarding|apple_trademarks")}

    -

    {_t("onboarding|google_trademarks")}

    -
    -
    - ); -}; diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 39afb2e621..0638e7e8ab 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -11,14 +11,14 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import FocusLock from "react-focus-lock"; import classNames from "classnames"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import AccessibleButton from "../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import Heading from "../typography/Heading"; -import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers"; +import { PosthogScreenTracker, type ScreenName } from "../../../PosthogTrackers"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 2c13ae0006..081f8beaca 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -15,7 +15,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { UserTab } from "./UserTab"; import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog"; -import { SettingKey } from "../../../settings/Settings.tsx"; +import { type SettingKey } from "../../../settings/Settings.tsx"; // XXX: Keep this around for re-use in future Betas diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 013f9ecb06..e7f1b93cd4 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -9,12 +9,13 @@ 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 ReactNode } from "react"; +import { Link } from "@vector-im/compound-web"; import SdkConfig from "../../../SdkConfig"; import Modal from "../../../Modal"; import { _t } from "../../../languageHandler"; -import sendBugReport, { downloadBugReport } from "../../../rageshake/submit-rageshake"; +import sendBugReport, { downloadBugReport, RageshakeError } from "../../../rageshake/submit-rageshake"; import AccessibleButton from "../elements/AccessibleButton"; import QuestionDialog from "./QuestionDialog"; import BaseDialog from "./BaseDialog"; @@ -26,7 +27,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { getBrowserSupport } from "../../../SupportedBrowser"; -interface IProps { +export interface BugReportDialogProps { onFinished: (success: boolean) => void; initialText?: string; label?: string; @@ -36,7 +37,7 @@ interface IProps { interface IState { sendLogs: boolean; busy: boolean; - err: string | null; + err: ReactNode | null; issueUrl: string; text: string; progress: string | null; @@ -44,11 +45,11 @@ interface IState { downloadProgress: string | null; } -export default class BugReportDialog extends React.Component { +export default class BugReportDialog extends React.Component { private unmounted: boolean; private issueRef: React.RefObject; - public constructor(props: IProps) { + public constructor(props: BugReportDialogProps) { super(props); this.state = { @@ -89,6 +90,42 @@ export default class BugReportDialog extends React.Component { this.props.onFinished(false); }; + private getErrorText(error: Error | RageshakeError): ReactNode { + if (error instanceof RageshakeError) { + let errorText; + switch (error.errorcode) { + case "DISALLOWED_APP": + errorText = _t("bug_reporting|failed_send_logs_causes|disallowed_app"); + break; + case "REJECTED_BAD_VERSION": + errorText = _t("bug_reporting|failed_send_logs_causes|rejected_version"); + break; + case "REJECTED_UNEXPECTED_RECOVERY_KEY": + errorText = _t("bug_reporting|failed_send_logs_causes|rejected_recovery_key"); + break; + default: + if (error.errorcode?.startsWith("REJECTED")) { + errorText = _t("bug_reporting|failed_send_logs_causes|rejected_generic"); + } else { + errorText = _t("bug_reporting|failed_send_logs_causes|server_unknown_error"); + } + break; + } + return ( + <> +

    {errorText}

    + {error.policyURL && ( + + {_t("action|learn_more")} + + )} + + ); + } else { + return

    {_t("bug_reporting|failed_send_logs_causes|unknown_error")}

    ; + } + } + private onSubmit = (): void => { if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) { this.setState({ @@ -126,7 +163,7 @@ export default class BugReportDialog extends React.Component { this.setState({ busy: false, progress: null, - err: _t("bug_reporting|failed_send_logs") + `${err.message}`, + err: this.getErrorText(err), }); } }, @@ -155,7 +192,7 @@ export default class BugReportDialog extends React.Component { this.setState({ downloadBusy: false, downloadProgress: - _t("bug_reporting|failed_send_logs") + `${err instanceof Error ? err.message : ""}`, + _t("bug_reporting|failed_download_logs") + `${err instanceof Error ? err.message : ""}`, }); } } diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx index d766c6c973..1b8bf9b07e 100644 --- a/src/components/views/dialogs/BulkRedactDialog.tsx +++ b/src/components/views/dialogs/BulkRedactDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -8,7 +8,14 @@ Please see LICENSE files in the repository root for full details. import React, { useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient, RoomMember, Room, MatrixEvent, EventTimeline, EventType } from "matrix-js-sdk/src/matrix"; +import { + type MatrixClient, + type RoomMember, + type Room, + type MatrixEvent, + EventTimeline, + EventType, +} from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -106,12 +113,13 @@ const BulkRedactDialog: React.FC = (props) => {

    {_t("user_info|redact|confirm_description_1", { count, user })}

    {_t("user_info|redact|confirm_description_2")}

    - setKeepStateEvents(e.target.checked)}> + setKeepStateEvents(e.target.checked)} + > {_t("user_info|redact|confirm_keep_state_label")} -
    - {_t("user_info|redact|confirm_keep_state_explainer")} -
    = { [_td("devtools|explore_account_data"), AccountDataExplorer], [_td("devtools|settings_explorer"), SettingExplorer], [_td("devtools|server_info"), ServerInfo], + [_td("devtools|crypto|title"), Crypto], ], }; diff --git a/src/components/views/dialogs/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index 03d9c7f7f5..bb654fe607 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixEvent, MatrixClient, TimelineEvents } from "matrix-js-sdk/src/matrix"; +import { type MatrixEvent, type MatrixClient, type TimelineEvents } from "matrix-js-sdk/src/matrix"; import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent"; import { _t } from "../../../languageHandler"; @@ -15,7 +15,7 @@ import QuestionDialog from "./QuestionDialog"; import { findTopAnswer } from "../messages/MPollBody"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; -import { GetRelationsForEvent } from "../rooms/EventTile"; +import { type GetRelationsForEvent } from "../rooms/EventTile"; interface IProps { matrixClient: MatrixClient; diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index bd7ad24bca..cfb6cca886 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -6,8 +6,8 @@ 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, { useRef, useState, Dispatch, SetStateAction } from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; +import React, { useRef, useState, type Dispatch, type SetStateAction } from "react"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; @@ -18,18 +18,18 @@ import StyledRadioGroup from "../elements/StyledRadioGroup"; import StyledCheckbox from "../elements/StyledCheckbox"; import { ExportFormat, - ExportFormatKey, + type ExportFormatKey, ExportType, - ExportTypeKey, + type ExportTypeKey, textForFormat, textForType, } from "../../../utils/exportUtils/exportUtils"; -import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; +import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; import HTMLExporter from "../../../utils/exportUtils/HtmlExport"; import JSONExporter from "../../../utils/exportUtils/JSONExport"; import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport"; import { useStateCallback } from "../../../hooks/useStateCallback"; -import Exporter from "../../../utils/exportUtils/Exporter"; +import type Exporter from "../../../utils/exportUtils/Exporter"; import Spinner from "../elements/Spinner"; import InfoDialog from "./InfoDialog"; import ChatExport from "../../../customisations/ChatExport"; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index a831acc5d1..d18b1bac01 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -9,18 +9,18 @@ Please see LICENSE files in the repository root for full details. import React, { useEffect, useMemo, useState } from "react"; import classnames from "classnames"; import { - IContent, + type IContent, MatrixEvent, - Room, - RoomMember, + type Room, + type RoomMember, EventType, - MatrixClient, + type MatrixClient, ContentHelpers, - ILocationContent, + type ILocationContent, LocationAssetType, M_TIMESTAMP, M_BEACON, - TimelineEvents, + type TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; @@ -36,19 +36,19 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; -import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import { Action } from "../../../dispatcher/actions"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; import { isLocationEvent } from "../../../utils/EventUtils"; import { isSelfLocation, locationEventGeoUri } from "../../../utils/location"; import { RoomContextDetails } from "../rooms/RoomContextDetails"; import { filterBoolean } from "../../../utils/arrays"; import { - IState, + type IState, RovingTabIndexContext, RovingTabIndexProvider, Type, diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx index 1ea187361e..3967de7c4d 100644 --- a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -1,12 +1,12 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 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, { ReactNode, useState } from "react"; +import React, { type ReactNode, useState } from "react"; import QuestionDialog from "./QuestionDialog"; import { _t } from "../../../languageHandler"; @@ -78,7 +78,6 @@ const GenericFeatureFeedbackDialog: React.FC = ({ }} autoFocus={true} /> - setCanContact((e.target as HTMLInputElement).checked)} diff --git a/src/components/views/dialogs/IncomingSasDialog.tsx b/src/components/views/dialogs/IncomingSasDialog.tsx index e5df3a35b1..bb210d0d23 100644 --- a/src/components/views/dialogs/IncomingSasDialog.tsx +++ b/src/components/views/dialogs/IncomingSasDialog.tsx @@ -5,8 +5,8 @@ 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, { ReactNode } from "react"; -import { GeneratedSas, ShowSasCallbacks, Verifier, VerifierEvent } from "matrix-js-sdk/src/crypto-api"; +import React, { type ReactNode } from "react"; +import { type GeneratedSas, type ShowSasCallbacks, type Verifier, VerifierEvent } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; diff --git a/src/components/views/dialogs/InfoDialog.tsx b/src/components/views/dialogs/InfoDialog.tsx index 20fb51a7d0..febf6a3419 100644 --- a/src/components/views/dialogs/InfoDialog.tsx +++ b/src/components/views/dialogs/InfoDialog.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, { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index beb27573c6..5800153fed 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -9,17 +9,17 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixClient, UIAResponse } from "matrix-js-sdk/src/matrix"; -import { AuthType } from "matrix-js-sdk/src/interactive-auth"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type AuthType } from "matrix-js-sdk/src/interactive-auth"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import InteractiveAuth, { ERROR_USER_CANCELLED, - InteractiveAuthCallback, - InteractiveAuthProps, + type InteractiveAuthCallback, + type InteractiveAuthProps, } from "../../structures/InteractiveAuth"; -import { ContinueKind, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; +import { type ContinueKind, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import BaseDialog from "./BaseDialog"; import { Linkify } from "../../../Linkify"; @@ -63,7 +63,7 @@ export interface InteractiveAuthDialogProps // Default is defined in _getDefaultDialogAesthetics() aestheticsForStagePhases?: DialogAesthetics; - onFinished(success?: boolean, result?: UIAResponse | Error | null): void; + onFinished(success?: boolean, result?: T | Error | null): void; } interface IState { @@ -111,7 +111,7 @@ export default class InteractiveAuthDialog extends React.Component = async (success, result): Promise => { if (success) { - this.props.onFinished(true, result); + this.props.onFinished(true, result as T); } else { if (result === ERROR_USER_CANCELLED) { this.props.onFinished(false, null); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index d72baae523..d98069fcf9 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -6,11 +6,11 @@ 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, ReactNode, SyntheticEvent } from "react"; +import React, { createRef, type ReactNode, type SyntheticEvent } from "react"; import classNames from "classnames"; -import { RoomMember, Room, MatrixError, EventType } from "matrix-js-sdk/src/matrix"; +import { RoomMember, type Room, MatrixError, EventType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { uniqBy } from "lodash"; import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -26,7 +26,7 @@ import { buildActivityScores, buildMemberScores, compareMembers } from "../../.. import { abbreviateUrl } from "../../../utils/UrlUtils"; import IdentityAuthClient from "../../../IdentityAuthClient"; import { humanizeTime } from "../../../utils/humanize"; -import { IInviteResult, inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite"; +import { type IInviteResult, inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite"; import { Action } from "../../../dispatcher/actions"; import { DefaultTagID } from "../../../stores/room-list/models"; import RoomListStore from "../../../stores/room-list/RoomListStore"; @@ -35,7 +35,7 @@ import { UIFeature } from "../../../settings/UIFeature"; import { mediaFromMxc } from "../../../customisations/Media"; import BaseAvatar from "../avatars/BaseAvatar"; import { SearchResultAvatar } from "../avatars/SearchResultAvatar"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; import { selectText } from "../../../utils/strings"; import Field from "../elements/Field"; import TabbedView, { Tab, TabLocation } from "../../structures/TabbedView"; @@ -47,13 +47,13 @@ import DialPadBackspaceButton from "../elements/DialPadBackspaceButton"; import LegacyCallHandler from "../../../LegacyCallHandler"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import CopyableText from "../elements/CopyableText"; -import { ScreenName } from "../../../PosthogTrackers"; +import { type ScreenName } from "../../../PosthogTrackers"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { DirectoryMember, - IDMUserTileProps, - Member, + type IDMUserTileProps, + type Member, startDmOnFirstMessage, ThreepidMember, } from "../../../utils/direct-messages"; @@ -61,11 +61,11 @@ import { InviteKind } from "./InviteDialogTypes"; import Modal from "../../../Modal"; import dis from "../../../dispatcher/dispatcher"; import { privateShouldBeEncrypted } from "../../../utils/rooms"; -import { NonEmptyArray } from "../../../@types/common"; +import { type NonEmptyArray } from "../../../@types/common"; import { UNKNOWN_PROFILE_ERRORS } from "../../../utils/MultiInviter"; -import AskInviteAnywayDialog, { UnknownProfiles } from "./AskInviteAnywayDialog"; +import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDialog"; import { SdkContextClass } from "../../../contexts/SDKContext"; -import { UserProfilesStore } from "../../../stores/UserProfilesStore"; +import { type UserProfilesStore } from "../../../stores/UserProfilesStore"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx index 72ad5f478f..e81606db79 100644 --- a/src/components/views/dialogs/LeaveSpaceDialog.tsx +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { useMemo, useState } from "react"; -import { Room, JoinRule } from "matrix-js-sdk/src/matrix"; +import { type Room, JoinRule } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import DialogButtons from "../elements/DialogButtons"; diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 460871fb36..6e000ef631 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { lazy } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import Modal from "../../../Modal"; import dis from "../../../dispatcher/dispatcher"; diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index 7a2e3b35ef..0f16d3d6f6 100644 --- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -44,22 +44,23 @@ const Entry: React.FC<{ } return ( -