From ea8393304ba839e2c61d4f4fccc9ff29f4cbf6b1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 27 Nov 2024 09:10:29 +0000 Subject: [PATCH 1/5] Improve performance of RoomContext in RoomHeader This allows a component to subscribe to only part of the RoomContext so they do not need to re-render on every single change Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/FilePanel.tsx | 22 +++--- .../structures/NotificationPanel.tsx | 13 ++-- src/components/structures/RoomSearchView.tsx | 4 +- src/components/structures/RoomView.tsx | 33 +++++---- src/components/structures/ThreadPanel.tsx | 19 +++-- src/components/structures/ThreadView.tsx | 17 +++-- .../WaitingForThirdPartyRoomView.tsx | 4 +- src/components/views/avatars/BaseAvatar.tsx | 6 +- .../views/context_menus/WidgetContextMenu.tsx | 4 +- .../views/elements/MiniAvatarUploader.tsx | 4 +- .../views/messages/RoomPredecessorTile.tsx | 6 +- .../views/right_panel/PinnedMessagesCard.tsx | 16 ++--- .../views/right_panel/RoomSummaryCard.tsx | 5 +- .../views/right_panel/TimelineCard.tsx | 15 ++-- src/components/views/rooms/HistoryTile.tsx | 6 +- .../views/rooms/MessageComposerButtons.tsx | 6 +- src/components/views/rooms/NewRoomIntro.tsx | 4 +- src/components/views/rooms/RoomHeader.tsx | 6 +- src/components/views/rooms/ThreadSummary.tsx | 4 +- .../wysiwyg_composer/components/Emoji.tsx | 4 +- .../components/WysiwygAutocomplete.tsx | 4 +- .../components/WysiwygComposer.tsx | 4 +- .../wysiwyg_composer/hooks/useEditing.ts | 4 +- .../hooks/useInitialContent.ts | 10 +-- .../hooks/useInputEventProcessor.ts | 8 +-- .../hooks/usePlainTextListeners.ts | 4 +- .../hooks/useWysiwygEditActionHandler.ts | 5 +- .../hooks/useWysiwygSendActionHandler.ts | 5 +- .../rooms/wysiwyg_composer/hooks/utils.ts | 4 +- .../rooms/wysiwyg_composer/utils/editing.ts | 2 +- .../rooms/wysiwyg_composer/utils/event.ts | 4 +- .../rooms/wysiwyg_composer/utils/message.ts | 4 +- src/contexts/RoomContext.ts | 5 +- src/contexts/ScopedRoomContext.tsx | 71 +++++++++++++++++++ src/hooks/room/useRoomMemberProfile.ts | 7 +- 35 files changed, 203 insertions(+), 136 deletions(-) create mode 100644 src/contexts/ScopedRoomContext.tsx diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 74a91d8cbc5..0f0cbd6a64b 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { roomId: string; @@ -269,12 +270,10 @@ class FilePanel extends React.Component { if (this.state.timelineSet) { return ( - { layout={Layout.Group} /> - + ); } else { return ( - + { > - + ); } } diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index edec675b140..efeecdc0291 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { onClose(): void; @@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent } {content} - + ); } } diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index de2d9d2198e..82146bcc5e0 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import ResizeNotifier from "../../utils/ResizeNotifier"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -import RoomContext from "../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; const DEBUG = false; let debuglog = function (msg: string): void {}; @@ -53,7 +53,7 @@ interface Props { export const RoomSearchView = forwardRef( ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { const client = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("showHiddenEvents"); const [highlights, setHighlights] = useState(null); const [results, setResults] = useState(null); const aborted = useRef(false); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 470b73de7c6..cbf875382c8 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; +import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject } from "react"; import classNames from "classnames"; import { IRecommendedVersion, @@ -54,7 +54,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore"; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; +import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { Action } from "../../dispatcher/actions"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; @@ -126,6 +126,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; +import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -246,6 +247,7 @@ interface LocalRoomViewProps { permalinkCreator: RoomPermalinkCreator; roomView: RefObject; onFileDrop: (dataTransfer: DataTransfer) => Promise; + mainSplitContentType: MainSplitContentType; } /** @@ -255,7 +257,7 @@ interface LocalRoomViewProps { * @returns {ReactElement} */ function LocalRoomView(props: LocalRoomViewProps): ReactElement { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room"); const room = context.room as LocalRoom; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; let encryptionTile: ReactNode; @@ -323,6 +325,7 @@ interface ILocalRoomCreateLoaderProps { localRoom: LocalRoom; names: string; resizeNotifier: ResizeNotifier; + mainSplitContentType: MainSplitContentType; } /** @@ -1959,35 +1962,41 @@ export class RoomView extends React.Component { if (!this.state.room || !this.context?.client) return null; const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); return ( - - - + + + ); } private renderLocalRoomView(localRoom: LocalRoom): ReactNode { return ( - + - + ); } private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { return ( - + - + ); } @@ -2516,7 +2525,7 @@ export class RoomView extends React.Component { } return ( - +
{showChatEffects && this.roomView.current && ( @@ -2543,7 +2552,7 @@ export class RoomView extends React.Component {
-
+ ); } } diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index eb20b05fdc6..0e82baa28b8 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr import { _t } from "../../languageHandler"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; -import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import TimelinePanel from "./TimelinePanel"; import { Layout } from "../../settings/enums/Layout"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; @@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import { clearRoomNotification } from "../../utils/notifications"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { roomId: string; @@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{ setFilterOption: (filterOption: ThreadFilterType) => void; }> = ({ filterOption, setFilterOption }) => { const mxClient = useMatrixClientContext(); - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("room"); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const options: readonly ThreadPanelHeaderOption[] = [ { @@ -184,13 +185,11 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => }, [timelineSet, timelinePanel]); return ( - = ({ roomId, onClose, permalinkCreator }) => )} - + ); }; export default ThreadPanel; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index be538a66693..7e070278592 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C import Heading from "../views/typography/Heading"; import { SdkContextClass } from "../../contexts/SDKContext"; import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -422,14 +423,12 @@ export default class ThreadView extends React.Component { } return ( - { /> )} - + ); } } diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index fd9afc50f23..cabe92a53d3 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -9,7 +9,6 @@ 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 { useRoomContext } from "../../contexts/RoomContext"; import ResizeNotifier from "../../utils/ResizeNotifier"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomHeader from "../views/rooms/RoomHeader"; @@ -19,6 +18,7 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro"; import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface Props { roomView: RefObject; @@ -32,7 +32,7 @@ interface Props { * To avoid UTDs, users are shown a waiting room until the others have joined. */ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resizeNotifier, inviteEvent }) => { - const context = useRoomContext(); + const context = useScopedRoomContext("room"); const brand = SdkConfig.get().brand; return ( diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index db74d7b95ee..766eb561ec8 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -16,10 +16,10 @@ import { Avatar } from "@vector-im/compound-web"; import SettingsStore from "../../../settings/SettingsStore"; import { ButtonEvent } from "../elements/AccessibleButton"; -import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { _t } from "../../../languageHandler"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { name?: React.ComponentProps["name"]; // The name (first initial used as default) @@ -57,8 +57,8 @@ const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = fals const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => { // Since this is a hot code path and the settings store can be slow, we // use the cached lowBandwidth value from the room context if it exists - const roomContext = useContext(RoomContext); - const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); + const roomContext = useScopedRoomContext("lowBandwidth"); + const lowBandwidth = roomContext?.lowBandwidth ?? SettingsStore.getValue("lowBandwidth"); const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth)); const [urlsIndex, setIndex] = useState(0); diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 21239d0fee6..46052de9ff2 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -18,7 +18,6 @@ import { _t } from "../../../languageHandler"; import { isAppWidget } from "../../../stores/WidgetStore"; import WidgetUtils from "../../../utils/WidgetUtils"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; -import RoomContext from "../../../contexts/RoomContext"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; @@ -30,6 +29,7 @@ import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayo import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps extends Omit, "children"> { app: IWidget; @@ -114,7 +114,7 @@ export const WidgetContextMenu: React.FC = ({ ...props }) => { const cli = useContext(MatrixClientContext); - const { room, roomId } = useContext(RoomContext); + const { room, roomId } = useScopedRoomContext("room", "roomId"); const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app)); const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId); diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index cf5a2398148..452b206bef3 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -12,12 +12,12 @@ import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "reac import { Tooltip } from "@vector-im/compound-web"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useTimeout } from "../../../hooks/useTimeout"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; import { getFileChanged } from "../settings/AvatarSetting.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export const AVATAR_SIZE = "52px"; @@ -56,7 +56,7 @@ const MiniAvatarUploader: React.FC = ({ const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId()); if (!canSetAvatar) return {children}; diff --git a/src/components/views/messages/RoomPredecessorTile.tsx b/src/components/views/messages/RoomPredecessorTile.tsx index 2e8633febde..afc81422345 100644 --- a/src/components/views/messages/RoomPredecessorTile.tsx +++ b/src/components/views/messages/RoomPredecessorTile.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext } from "react"; +import React, { useCallback } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix"; @@ -18,10 +18,10 @@ import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EventTileBubble from "./EventTileBubble"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import RoomContext from "../../../contexts/RoomContext"; import { useRoomState } from "../../../hooks/useRoomState"; import SettingsStore from "../../../settings/SettingsStore"; import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { /** The m.room.create MatrixEvent that this tile represents */ @@ -40,7 +40,7 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => // the information inside mxEvent. This allows us the flexibility later to // use a different predecessor (e.g. through MSC3946) and still display it // in the timeline location of the create event. - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("room"); const predecessor = useRoomState( roomContext.room, useCallback( diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index af7106f9c5d..d6161e9434d 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useEffect, JSX } from "react"; +import React, { useCallback, useEffect, JSX, useContext } from "react"; import { Room, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Button, Separator } from "@vector-im/compound-web"; import classNames from "classnames"; @@ -18,7 +18,7 @@ import Spinner from "../elements/Spinner"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { PinnedEventTile } from "../rooms/PinnedEventTile"; import { useRoomState } from "../../../hooks/useRoomState"; -import RoomContext, { TimelineRenderingType, useRoomContext } from "../../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { ReadPinsEventId } from "./types"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { filterBoolean } from "../../../utils/arrays"; @@ -27,6 +27,7 @@ import { UnpinAllDialog } from "../dialogs/UnpinAllDialog"; import EmptyState from "./EmptyState"; import { usePinnedEvents, useReadPinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; import PinningUtils from "../../../utils/PinningUtils.ts"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; /** * List the pinned messages in a room inside a Card. @@ -48,7 +49,7 @@ interface PinnedMessagesCardProps { export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMessagesCardProps): JSX.Element { const cli = useMatrixClientContext(); - const roomContext = useRoomContext(); + const roomContext = useContext(RoomContext); const pinnedEventIds = usePinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); @@ -89,14 +90,9 @@ export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMe className="mx_PinnedMessagesCard" onClose={onClose} > - + {content} - + ); } diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 664977bbe27..396b5aa591e 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -51,7 +51,7 @@ import ShareDialog from "../dialogs/ShareDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomName from "../elements/RoomName"; import ExportDialog from "../dialogs/ExportDialog"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; @@ -76,6 +76,7 @@ import { useTransition } from "../../../hooks/useTransition"; import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; import { usePinnedEvents } from "../../../hooks/usePinnedEvents"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -232,7 +233,7 @@ const RoomSummaryCard: React.FC = ({ }; const isRoomEncrypted = useIsEncrypted(cli, room); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType"); const e2eStatus = roomContext.e2eStatus; const isVideoRoom = calcIsVideoRoom(room); diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index e0988eeaa5d..a9ad2985fb7 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -38,6 +38,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import Measured from "../elements/Measured"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -199,13 +200,11 @@ export default class TimelineCard extends React.Component { const showComposer = myMembership === KnownMembership.Join; return ( - { /> )} - + ); } } diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx index c52ab044a7f..3aa74b8b0c7 100644 --- a/src/components/views/rooms/HistoryTile.tsx +++ b/src/components/views/rooms/HistoryTile.tsx @@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useContext } from "react"; +import React from "react"; import { EventTimeline } from "matrix-js-sdk/src/matrix"; import EventTileBubble from "../messages/EventTileBubble"; -import RoomContext from "../../../contexts/RoomContext"; import { _t } from "../../../languageHandler"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; const HistoryTile: React.FC = () => { - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS); const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 003c2afed96..7bd2e1be45b 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -21,7 +21,6 @@ import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import ContentMessages from "../../../ContentMessages"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useDispatcher } from "../../../hooks/useDispatcher"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; @@ -29,6 +28,7 @@ import { EmojiButton } from "./EmojiButton"; import { filterBoolean } from "../../../utils/arrays"; import { useSettingValue } from "../../../hooks/useSettings"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { addEmoji: (emoji: string) => boolean; @@ -54,7 +54,7 @@ export const OverflowMenuContext = createContext(null const MessageComposerButtons: React.FC = (props: IProps) => { const matrixClient = useContext(MatrixClientContext); - const { room, narrow } = useContext(RoomContext); + const { room, narrow } = useScopedRoomContext("room", "narrow"); const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer"); @@ -168,7 +168,7 @@ interface IUploadButtonProps { // We put the file input outside the UploadButton component so that it doesn't get killed when the context menu closes. const UploadButtonContextProvider: React.FC = ({ roomId, relation, children }) => { const cli = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("timelineRenderingType"); const uploadInput = useRef(null); const onUploadClick = (): void => { diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 05912c482ef..a5577ee372f 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -11,7 +11,6 @@ import { EventType, Room, User, MatrixClient } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t, _td, TranslationKey } from "../../../languageHandler"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; @@ -30,6 +29,7 @@ import { UIComponent } from "../../../settings/UIFeature"; import { privateShouldBeEncrypted } from "../../../utils/rooms"; import { LocalRoom } from "../../../models/LocalRoom"; import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); @@ -51,7 +51,7 @@ const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolea const NewRoomIntro: React.FC = () => { const cli = useContext(MatrixClientContext); - const { room, roomId } = useContext(RoomContext); + const { room, roomId } = useScopedRoomContext("room", "roomId"); if (!room || !roomId) { throw new Error("Unable to create a NewRoomIntro without room and roomId"); diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c2642ea733a..b0040c45fdc 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call"; @@ -48,10 +48,10 @@ import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton"; import { ButtonEvent } from "../elements/AccessibleButton"; import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; -import RoomContext from "../../../contexts/RoomContext"; import { MainSplitContentType } from "../../structures/RoomView"; import defaultDispatcher from "../../../dispatcher/dispatcher.ts"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export default function RoomHeader({ room, @@ -229,7 +229,7 @@ export default function RoomHeader({ voiceCallButton = undefined; } - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("mainSplitContentType"); const isVideoRoom = calcIsVideoRoom(room); const showChatButton = isVideoRoom || diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index 4a3032d6411..ac23331f66d 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -16,7 +16,6 @@ import { CardContext } from "../right_panel/context"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import PosthogTrackers from "../../../PosthogTrackers"; import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import RoomContext from "../../../contexts/RoomContext"; import MemberAvatar from "../avatars/MemberAvatar"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; @@ -24,6 +23,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; import { notificationLevelToIndicator } from "../../../utils/notifications"; import { EventPreviewTile, useEventPreview } from "./EventPreview.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { mxEvent: MatrixEvent; @@ -31,7 +31,7 @@ interface IProps { } const ThreadSummary: React.FC = ({ mxEvent, thread, ...props }) => { - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("narrow"); const cardContext = useContext(CardContext); const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length); const { level } = useUnreadNotifications(thread.room, thread.id); diff --git a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx index b7a6d65e235..9ab3d210abc 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx @@ -13,14 +13,14 @@ import { EmojiButton } from "../../EmojiButton"; import dis from "../../../../../dispatcher/dispatcher"; import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface EmojiProps { menuPosition: MenuProps; } export function Emoji({ menuPosition }: EmojiProps): JSX.Element { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); return ( , ): JSX.Element | null => { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const client = useMatrixClientContext(); function handleConfirm(completion: ICompletion): void { diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 5b6361a58e8..f1e42ce091f 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -19,12 +19,12 @@ import { Editor } from "./Editor"; import { useInputEventProcessor } from "../hooks/useInputEventProcessor"; import { useSetCursorPosition } from "../hooks/useSetCursorPosition"; import { useIsFocused } from "../hooks/useIsFocused"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { parsePermalink } from "../../../../../utils/permalinks/Permalinks"; import { isNotNull } from "../../../../../Typeguards"; import { useSettingValue } from "../../../../../hooks/useSettings"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface WysiwygComposerProps { disabled?: boolean; @@ -56,7 +56,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({ children, eventRelation, }: WysiwygComposerProps) { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const autocompleteRef = useRef(null); const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts index 5d1c3b867e2..20f394e8a3e 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts @@ -10,10 +10,10 @@ import { ISendEventResponse } from "matrix-js-sdk/src/matrix"; import { useCallback, useState } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import { endEditing } from "../utils/editing"; import { editMessage } from "../utils/message"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useEditing( editorStateTransfer: EditorStateTransfer, @@ -24,7 +24,7 @@ export function useEditing( editMessage(): Promise; endEditing(): void; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const mxClient = useMatrixClientContext(); const [isSaveDisabled, setIsSaveDisabled] = useState(true); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts index 2e0ddd3ccd1..3a3799496b8 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts @@ -10,11 +10,11 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { useMemo } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { parseEvent } from "../../../../../editor/deserialize"; import { CommandPartCreator, Part } from "../../../../../editor/parts"; import SettingsStore from "../../../../../settings/SettingsStore"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function getFormattedContent(editorStateTransfer: EditorStateTransfer): string { return ( @@ -60,12 +60,12 @@ export function parseEditorStateTransfer( } export function useInitialContent(editorStateTransfer: EditorStateTransfer): string | undefined { - const roomContext = useRoomContext(); + const { room } = useScopedRoomContext("room"); const mxClient = useMatrixClientContext(); return useMemo(() => { - if (editorStateTransfer && roomContext.room && mxClient) { - return parseEditorStateTransfer(editorStateTransfer, roomContext.room, mxClient); + if (editorStateTransfer && room && mxClient) { + return parseEditorStateTransfer(editorStateTransfer, room, mxClient); } - }, [editorStateTransfer, roomContext, mxClient]); + }, [editorStateTransfer, room, mxClient]); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index 8eac63eb366..cab3bdefb85 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -16,7 +16,6 @@ import { KeyBindingAction } from "../../../../../accessibility/KeyboardShortcuts import { findEditableEvent } from "../../../../../utils/EventUtils"; import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { IRoomState } from "../../../../structures/RoomView"; import { ComposerContextState, useComposerContext } from "../ComposerContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; @@ -26,6 +25,7 @@ import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/ev import { endEditing } from "../utils/editing"; import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useInputEventProcessor( onSend: () => void, @@ -33,7 +33,7 @@ export function useInputEventProcessor( initialContent?: string, eventRelation?: IEventRelation, ): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType"); const composerContext = useComposerContext(); const mxClient = useMatrixClientContext(); const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend"); @@ -94,7 +94,7 @@ function handleKeyboardEvent( initialContent: string | undefined, composer: Wysiwyg, editor: HTMLElement, - roomContext: IRoomState, + roomContext: Pick, composerContext: ComposerContextState, mxClient: MatrixClient | undefined, autocompleteRef: React.RefObject, @@ -175,7 +175,7 @@ function dispatchEditEvent( isForward: boolean, editorStateTransfer: EditorStateTransfer | undefined, composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, ): boolean { const foundEvents = editorStateTransfer diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 742d24fe341..1dc23cc274a 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -16,8 +16,8 @@ import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; import { useSuggestion } from "./useSuggestion"; import { isNotNull, isNotUndefined } from "../../../../../Typeguards"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; @@ -63,7 +63,7 @@ export function usePlainTextListeners( onSelect: (event: SyntheticEvent) => void; suggestion: MappedSuggestion | null; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("room", "timelineRenderingType", "replyToEvent"); const mxClient = useMatrixClientContext(); const ref = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index e1e34623c85..eb76d77af5d 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -11,20 +11,21 @@ import { RefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerFunctions } from "../types"; import { setSelection } from "../utils/selection"; import { useComposerContext } from "../ComposerContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygEditActionHandler( disabled: boolean, composerElement: RefObject, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 16e1e608ec2..d11f3498fd7 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -11,20 +11,21 @@ import { MutableRefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerFunctions } from "../types"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { useComposerContext } from "../ComposerContext"; import { setSelection } from "../utils/selection"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygSendActionHandler( disabled: boolean, composerElement: MutableRefObject, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 39317ea88cd..3345c9f474a 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -22,7 +22,7 @@ import { isNotNull } from "../../../../../Typeguards"; export function focusComposer( composerElement: MutableRefObject, renderingType: TimelineRenderingType, - roomContext: IRoomState, + roomContext: Pick, timeoutId: MutableRefObject, ): void { if (renderingType === roomContext.timelineRenderingType) { @@ -123,7 +123,7 @@ export function handleEventWithAutocomplete( export function handleClipboardEvent( event: ClipboardEvent | InputEvent, data: DataTransfer | null, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, eventRelation?: IEventRelation, ): boolean { diff --git a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts index 58a9e244924..462763b8f46 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts @@ -13,7 +13,7 @@ import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; -export function endEditing(roomContext: IRoomState): void { +export function endEditing(roomContext: Pick): void { // todo local storage // localStorage.removeItem(this.editorRoomKey); // localStorage.removeItem(this.editorStateKey); diff --git a/src/components/views/rooms/wysiwyg_composer/utils/event.ts b/src/components/views/rooms/wysiwyg_composer/utils/event.ts index 5fd37b3665b..45c6b1cac34 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/event.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/event.ts @@ -15,7 +15,7 @@ import { ComposerContextState } from "../ComposerContext"; // From EditMessageComposer private get events(): MatrixEvent[] export function getEventsFromEditorStateTransfer( editorStateTransfer: EditorStateTransfer, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, ): MatrixEvent[] | undefined { const liveTimelineEvents = roomContext.liveTimeline?.getEvents(); @@ -41,7 +41,7 @@ export function getEventsFromEditorStateTransfer( // From SendMessageComposer private onKeyDown = (event: KeyboardEvent): void export function getEventsFromRoom( composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick, ): MatrixEvent[] | undefined { const isReplyingToThread = composerContext.eventRelation?.key === THREAD_RELATION_TYPE.name; return roomContext.liveTimeline diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts index 44e10e3cc53..b7fca8ecb4a 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts @@ -39,7 +39,7 @@ export interface SendMessageParams { mxClient: MatrixClient; relation?: IEventRelation; replyToEvent?: MatrixEvent; - roomContext: IRoomState; + roomContext: Pick; } export async function sendMessage( @@ -177,7 +177,7 @@ export async function sendMessage( interface EditMessageParams { mxClient: MatrixClient; - roomContext: IRoomState; + roomContext: Pick; editorStateTransfer: EditorStateTransfer; } diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index acd4bfa6f41..2d5f5946357 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { createContext, useContext } from "react"; +import { createContext } from "react"; import { IRoomState } from "../components/structures/RoomView"; import { Layout } from "../settings/enums/Layout"; @@ -78,6 +78,3 @@ const RoomContext = createContext< }); RoomContext.displayName = "RoomContext"; export default RoomContext; -export function useRoomContext(): IRoomState { - return useContext(RoomContext); -} diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx new file mode 100644 index 00000000000..8074d7945e5 --- /dev/null +++ b/src/contexts/ScopedRoomContext.tsx @@ -0,0 +1,71 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; +import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useRef, useState } from "react"; + +import { objectKeyChanges } from "../utils/objects.ts"; +import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts"; +import RoomContext from "./RoomContext.ts"; + +type ContextValue = ContextType; + +export enum NotificationStateEvents { + Update = "update", +} + +type EventHandlerMap> = { + [NotificationStateEvents.Update]: (keys: Array) => void; +}; + +class EfficientContext> extends TypedEventEmitter< + NotificationStateEvents, + EventHandlerMap +> { + public state?: C; + + public setState(state: C): void { + const changedKeys = objectKeyChanges(this.state ?? ({} as C), state); + this.state = state; + this.emit(NotificationStateEvents.Update, changedKeys); + } +} + +const ScopedRoomContext = createContext | undefined>(undefined); + +// Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare) +export const ScopedRoomContextProvider = memo( + ({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => { + const contextRef = useRef(new EfficientContext()); + useEffect(() => { + contextRef.current.setState(state); + }, [state]); + + // Includes the legacy RoomContext provider for backwards compatibility with class components + return ( + + {children} + + ); + }, +); + +export function useScopedRoomContext>( + ...keys: K +): { [key in K[number]]: ContextValue[key] } { + const context = useContext(ScopedRoomContext); + const [state, setState] = useState<{ [key in K[number]]: ContextValue[key] }>({} as any); + + useTypedEventEmitter(context, NotificationStateEvents.Update, (updatedKeys: K[]): void => { + if (context?.state && updatedKeys.some((updatedKey) => keys.includes(updatedKey as any))) { + setState(context.state); + } + }); + + return state; +} diff --git a/src/hooks/room/useRoomMemberProfile.ts b/src/hooks/room/useRoomMemberProfile.ts index 57f72a722eb..b8bb44c50de 100644 --- a/src/hooks/room/useRoomMemberProfile.ts +++ b/src/hooks/room/useRoomMemberProfile.ts @@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { useContext, useMemo } from "react"; +import { useMemo } from "react"; -import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../contexts/RoomContext"; import { useSettingValue } from "../useSettings"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; export function useRoomMemberProfile({ userId = "", @@ -21,7 +22,7 @@ export function useRoomMemberProfile({ member?: RoomMember | null; forceHistorical?: boolean; }): RoomMember | undefined | null { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room", "timelineRenderingType"); const useOnlyCurrentProfiles = useSettingValue("useOnlyCurrentProfiles"); const member = useMemo(() => { From 3665958df6a651a548b50067e1b65caf592d195a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 27 Nov 2024 12:58:47 +0000 Subject: [PATCH 2/5] Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../components/structures/MessagePanel-test.tsx | 5 +++-- .../components/structures/ThreadPanel-test.tsx | 12 ++++++------ .../components/structures/ThreadView-test.tsx | 8 ++++---- .../audio_messages/RecordingPlayback-test.tsx | 7 ++++--- .../components/views/avatars/MemberAvatar-test.tsx | 6 +++--- .../context_menus/MessageContextMenu-test.tsx | 7 ++++--- .../components/views/messages/MFileBody-test.tsx | 7 ++++--- .../views/messages/MessageActionBar-test.tsx | 5 +++-- .../views/messages/RoomPredecessorTile-test.tsx | 6 +++--- .../views/right_panel/RoomSummaryCard-test.tsx | 11 ++++++----- .../views/rooms/EditMessageComposer-test.tsx | 4 ++-- .../components/views/rooms/EventTile-test.tsx | 7 ++++--- .../views/rooms/MessageComposer-test.tsx | 6 +++--- .../views/rooms/MessageComposerButtons-test.tsx | 4 ++-- .../components/views/rooms/NewRoomIntro-test.tsx | 6 +++--- .../views/rooms/SendMessageComposer-test.tsx | 7 ++++--- .../wysiwyg_composer/EditWysiwygComposer-test.tsx | 14 +++++++------- .../wysiwyg_composer/SendWysiwygComposer-test.tsx | 6 +++--- .../components/PlainTextComposer-test.tsx | 6 +++--- .../components/WysiwygAutocomplete-test.tsx | 6 +++--- .../components/WysiwygComposer-test.tsx | 10 +++++----- 21 files changed, 79 insertions(+), 71 deletions(-) diff --git a/test/unit-tests/components/structures/MessagePanel-test.tsx b/test/unit-tests/components/structures/MessagePanel-test.tsx index cf44716ba9e..dbb83da3124 100644 --- a/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -30,6 +30,7 @@ import { import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { IRoomState } from "../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -91,9 +92,9 @@ describe("MessagePanel", function () { const getComponent = (props = {}, roomContext: Partial = {}) => ( - + - + ); diff --git a/test/unit-tests/components/structures/ThreadPanel-test.tsx b/test/unit-tests/components/structures/ThreadPanel-test.tsx index c19127de259..5b84f1123a5 100644 --- a/test/unit-tests/components/structures/ThreadPanel-test.tsx +++ b/test/unit-tests/components/structures/ThreadPanel-test.tsx @@ -20,7 +20,6 @@ import { import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../../src/components/structures/ThreadPanel"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { _t } from "../../../../src/languageHandler"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; @@ -28,6 +27,7 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; import { IRoomState } from "../../../../src/components/structures/RoomView"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/Feedback"); @@ -81,11 +81,11 @@ describe("ThreadPanel", () => { room: mockRoom, } as unknown as IRoomState; const { container } = render( - + undefined} /> - , + , ); fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); await waitFor(() => @@ -114,8 +114,8 @@ describe("ThreadPanel", () => { const TestThreadPanel = () => ( - @@ -125,7 +125,7 @@ describe("ThreadPanel", () => { resizeNotifier={new ResizeNotifier()} permalinkCreator={new RoomPermalinkCreator(room)} /> - + ); diff --git a/test/unit-tests/components/structures/ThreadView-test.tsx b/test/unit-tests/components/structures/ThreadView-test.tsx index 697fd251810..ee4afff525b 100644 --- a/test/unit-tests/components/structures/ThreadView-test.tsx +++ b/test/unit-tests/components/structures/ThreadView-test.tsx @@ -23,7 +23,6 @@ import React, { useState } from "react"; import ThreadView from "../../../../src/components/structures/ThreadView"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import { Action } from "../../../../src/dispatcher/actions"; import dispatcher from "../../../../src/dispatcher/dispatcher"; @@ -34,6 +33,7 @@ import { mockPlatformPeg } from "../../../test-utils/platform"; import { getRoomContext } from "../../../test-utils/room"; import { mkMessage, stubClient } from "../../../test-utils/test-utils"; import { mkThread } from "../../../test-utils/threads"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; describe("ThreadView", () => { const ROOM_ID = "!roomId:example.org"; @@ -51,8 +51,8 @@ describe("ThreadView", () => { return ( - @@ -63,7 +63,7 @@ describe("ThreadView", () => { initialEvent={initialEvent} resizeNotifier={new ResizeNotifier()} /> - + , ); diff --git a/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx b/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx index 1f193d68832..419357e5c84 100644 --- a/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx +++ b/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx @@ -15,10 +15,11 @@ import RecordingPlayback, { PlaybackLayout, } from "../../../../../src/components/views/audio_messages/RecordingPlayback"; import { Playback } from "../../../../../src/audio/Playback"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { createAudioContext } from "../../../../../src/audio/compat"; import { flushPromises } from "../../../../test-utils"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/WorkerManager", () => ({ WorkerManager: jest.fn(() => ({ @@ -56,9 +57,9 @@ describe("", () => { const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState; const getComponent = (props: React.ComponentProps, room = defaultRoom) => render( - + - , + , ); beforeEach(() => { diff --git a/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx b/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx index eaa3a308d54..1c5bf3a44fd 100644 --- a/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx +++ b/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx @@ -12,11 +12,11 @@ import { MatrixClient, PendingEventOrdering, Room, RoomMember } from "matrix-js- import React, { ComponentProps } from "react"; import MemberAvatar from "../../../../../src/components/views/avatars/MemberAvatar"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { getRoomContext } from "../../../../test-utils/room"; import { stubClient } from "../../../../test-utils/test-utils"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("MemberAvatar", () => { const ROOM_ID = "roomId"; @@ -27,9 +27,9 @@ describe("MemberAvatar", () => { function getComponent(props: Partial>) { return ( - + - + ); } diff --git a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx index 54e8fa434bf..892ca6dbeda 100644 --- a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx @@ -27,7 +27,7 @@ import { mocked } from "jest-mock"; import userEvent from "@testing-library/user-event"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { canEditContent } from "../../../../../src/utils/EventUtils"; import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings"; @@ -40,6 +40,7 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; import { createMessageEventContent } from "../../../../test-utils/events"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/strings", () => ({ copyPlaintext: jest.fn(), @@ -546,8 +547,8 @@ function createMenu( client.getRoom = jest.fn().mockReturnValue(room); return render( - + - , + , ); } diff --git a/test/unit-tests/components/views/messages/MFileBody-test.tsx b/test/unit-tests/components/views/messages/MFileBody-test.tsx index 60795babdeb..e124e3a2ffa 100644 --- a/test/unit-tests/components/views/messages/MFileBody-test.tsx +++ b/test/unit-tests/components/views/messages/MFileBody-test.tsx @@ -20,7 +20,8 @@ import { import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import MFileBody from "../../../../../src/components/views/messages/MFileBody.tsx"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("matrix-encrypt-attachment", () => ({ decryptAttachment: jest.fn(), @@ -72,14 +73,14 @@ describe("", () => { it("should show a download button in file rendering type", async () => { const { container, getByRole } = render( - + - , + , ); expect(getByRole("link", { name: "Download" })).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/messages/MessageActionBar-test.tsx b/test/unit-tests/components/views/messages/MessageActionBar-test.tsx index 8b639eb94e7..dda5b348a9a 100644 --- a/test/unit-tests/components/views/messages/MessageActionBar-test.tsx +++ b/test/unit-tests/components/views/messages/MessageActionBar-test.tsx @@ -35,6 +35,7 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { Action } from "../../../../../src/dispatcher/actions"; import PinningUtils from "../../../../../src/utils/PinningUtils"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/dispatcher/dispatcher"); @@ -117,9 +118,9 @@ describe("", () => { } as unknown as IRoomState; const getComponent = (props = {}, roomContext: Partial = {}) => render( - + - , + , ); beforeEach(() => { diff --git a/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx b/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx index f666b13b33b..3ed7d7eb2b2 100644 --- a/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx +++ b/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx @@ -20,9 +20,9 @@ import { } from "../../../../../src/components/views/messages/RoomPredecessorTile"; import { stubClient, upsertRoomStateEvents } from "../../../../test-utils/test-utils"; import { Action } from "../../../../../src/dispatcher/actions"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { filterConsole, getRoomContext } from "../../../../test-utils"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/dispatcher/dispatcher"); @@ -99,9 +99,9 @@ describe("", () => { expect(createEvent).toBeTruthy(); return render( - + - , + , ); } diff --git a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx index 4026149f984..a2178466de1 100644 --- a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx @@ -30,7 +30,8 @@ import { _t } from "../../../../../src/languageHandler"; import { tagRoom } from "../../../../../src/utils/room/tagRoom"; import { DefaultTagID } from "../../../../../src/stores/room-list/models"; import { Action } from "../../../../../src/dispatcher/actions"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/room/tagRoom"); @@ -172,14 +173,14 @@ describe("", () => { const onSearchChange = jest.fn(); const { rerender } = render( - + - + , ); @@ -188,13 +189,13 @@ describe("", () => { rerender( - + - + , ); expect(screen.getByPlaceholderText("Search messages…")).toHaveValue(""); diff --git a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx index a34e2e895b1..0ef3dd76e54 100644 --- a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx @@ -27,12 +27,12 @@ import { import DocumentOffset from "../../../../../src/editor/offset"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import Autocompleter, { IProviderCompletions } from "../../../../../src/autocomplete/Autocompleter"; import NotifProvider from "../../../../../src/autocomplete/NotifProvider"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("", () => { const userId = "@alice:server.org"; @@ -79,7 +79,7 @@ describe("", () => { render(, { wrapper: ({ children }) => ( - {children} + {children} ), }); diff --git a/test/unit-tests/components/views/rooms/EventTile-test.tsx b/test/unit-tests/components/views/rooms/EventTile-test.tsx index e57514904a2..d41bc9b6f05 100644 --- a/test/unit-tests/components/views/rooms/EventTile-test.tsx +++ b/test/unit-tests/components/views/rooms/EventTile-test.tsx @@ -30,7 +30,7 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing"; import EventTile, { EventTileProps } from "../../../../../src/components/views/rooms/EventTile"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../../test-utils"; import { mkThread } from "../../../../test-utils/threads"; @@ -40,6 +40,7 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import PinningUtils from "../../../../../src/utils/PinningUtils"; import { Layout } from "../../../../../src/settings/enums/Layout"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("EventTile", () => { const ROOM_ID = "!roomId:example.org"; @@ -56,13 +57,13 @@ describe("EventTile", () => { }) { return ( - + - + ); } diff --git a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx index 3bd9a6cf624..4ef533091a6 100644 --- a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx @@ -24,7 +24,6 @@ import { import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import ResizeNotifier from "../../../../../src/utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; @@ -40,6 +39,7 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../../src/voice-broadcast"; import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; const openStickerPicker = async (): Promise => { await userEvent.click(screen.getByLabelText("More options")); @@ -512,9 +512,9 @@ function wrapAndRender( const getRawComponent = (props = {}, context = roomContext, client = mockClient) => ( - + - + ); return { diff --git a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx index a66dc0d9258..08204350ca0 100644 --- a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx @@ -10,11 +10,11 @@ import React from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-utils"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("MessageComposerButtons", () => { // @ts-ignore - we're deliberately not implementing the whole interface here, but @@ -54,7 +54,7 @@ describe("MessageComposerButtons", () => { return render( - {component} + {component} , ); } diff --git a/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx b/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx index 291d28c967c..a1cd4526107 100644 --- a/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx +++ b/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx @@ -13,19 +13,19 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { LocalRoom } from "../../../../../src/models/LocalRoom"; import { filterConsole, mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../../../test-utils"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { DirectoryMember } from "../../../../../src/utils/direct-messages"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => { render( - + - + , ); }; diff --git a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx index 9853ddb1ba0..c372819ce21 100644 --- a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx @@ -18,7 +18,7 @@ import SendMessageComposer, { isQuickReaction, } from "../../../../../src/components/views/rooms/SendMessageComposer"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import EditorModel from "../../../../../src/editor/model"; import { createPartCreator } from "../../../editor/mock"; import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils"; @@ -30,6 +30,7 @@ import { IRoomState, MainSplitContentType } from "../../../../../src/components/ import { mockPlatformPeg } from "../../../../test-utils/platform"; import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room"; import { addTextToComposer } from "../../../../test-utils/composer"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/local-room", () => ({ doMaybeLocalRoomAction: jest.fn(), @@ -365,9 +366,9 @@ describe("", () => { }; const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => ( - + - + ); const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => { diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 5d3c4552884..820358da7fe 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -11,7 +11,6 @@ import React from "react"; import { fireEvent, render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { flushPromises, mkEvent } from "../../../../../test-utils"; @@ -23,6 +22,7 @@ import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispa import { ActionPayload } from "../../../../../../src/dispatcher/payloads"; import * as EmojiButton from "../../../../../../src/components/views/rooms/EmojiButton"; import { createMocks } from "./utils"; +import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("EditWysiwygComposer", () => { afterEach(() => { @@ -39,9 +39,9 @@ describe("EditWysiwygComposer", () => { ) => { return render( - + - + , ); }; @@ -64,9 +64,9 @@ describe("EditWysiwygComposer", () => { rerender( - + - + , ); @@ -275,10 +275,10 @@ describe("EditWysiwygComposer", () => { ); render( - + - + , ); // Same behavior as in RoomView.tsx diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 27110149f5c..89415448b7b 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -11,7 +11,6 @@ import React from "react"; import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { flushPromises } from "../../../../../test-utils"; @@ -20,6 +19,7 @@ import { aboveLeftOf } from "../../../../../../src/components/structures/Context import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispatcher/payloads/ComposerInsertPayload"; import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; import { createMocks } from "./utils"; +import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({ EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => { @@ -66,7 +66,7 @@ describe("SendWysiwygComposer", () => { ) => { return render( - + { menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })} placeholder={placeholder} /> - + , ); }; diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx index 1ca6e9736cc..ae37afe8608 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx @@ -14,7 +14,7 @@ import { PlainTextComposer } from "../../../../../../../src/components/views/roo import * as mockUseSettingsHook from "../../../../../../../src/hooks/useSettings"; import * as mockKeyboard from "../../../../../../../src/Keyboard"; import { createMocks } from "../utils"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("PlainTextComposer", () => { const customRender = ( @@ -275,9 +275,9 @@ describe("PlainTextComposer", () => { const { defaultRoomContext } = createMocks(); render( - + - , + , ); expect(screen.getByTestId("autocomplete-wrapper")).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 68ce88ce0c9..b0f4bbb5bd1 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -11,12 +11,12 @@ import React, { createRef } from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; import { WysiwygAutocomplete } from "../../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete"; import { getRoomContext, mkStubRoom, stubClient } from "../../../../../../test-utils"; import Autocomplete from "../../../../../../../src/components/views/rooms/Autocomplete"; import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplete/Autocompleter"; import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; const mockCompletion: ICompletion[] = [ { @@ -71,7 +71,7 @@ describe("WysiwygAutocomplete", () => { return render( - + { handleAtRoomMention={mockHandleAtRoomMention} {...props} /> - + , ); }; diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index adfd1412a7d..c3c480eb828 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -18,7 +18,6 @@ import defaultDispatcher from "../../../../../../../src/dispatcher/dispatcher"; import * as EventUtils from "../../../../../../../src/utils/EventUtils"; import { Action } from "../../../../../../../src/dispatcher/actions"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; import { ComposerContext, getDefaultContextValue, @@ -32,20 +31,21 @@ import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplet import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider"; import * as Permalinks from "../../../../../../../src/utils/permalinks/Permalinks"; import { PermalinkParts } from "../../../../../../../src/utils/permalinks/PermalinkConstructor"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("WysiwygComposer", () => { const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { const { mockClient, defaultRoomContext } = createMocks(); return render( - + - + , ); }; @@ -523,7 +523,7 @@ describe("WysiwygComposer", () => { ) => { return render( - + @@ -537,7 +537,7 @@ describe("WysiwygComposer", () => { } /> - + , ); }; From b17eefc74344f7cdbcc61c8e2ca70d86e54c7c3d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 27 Nov 2024 13:21:44 +0000 Subject: [PATCH 3/5] Prettier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 872bcd0225a..6f286b667a5 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -9,15 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { - ChangeEvent, - ComponentProps, - createRef, - ReactElement, - ReactNode, - RefObject, - JSX, -} from "react"; +import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react"; import classNames from "classnames"; import { IRecommendedVersion, From f3826bf9d8085f0ca3802df0d995b98a46ce0040 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 27 Nov 2024 13:28:14 +0000 Subject: [PATCH 4/5] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/contexts/ScopedRoomContext.tsx | 18 ++++++++++-------- .../EditWysiwygComposer-test.tsx | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx index 8074d7945e5..1257a7fc4df 100644 --- a/src/contexts/ScopedRoomContext.tsx +++ b/src/contexts/ScopedRoomContext.tsx @@ -27,7 +27,9 @@ class EfficientContext> extends TypedEventEmitter< NotificationStateEvents, EventHandlerMap > { - public state?: C; + public constructor(public state: C) { + super(); + } public setState(state: C): void { const changedKeys = objectKeyChanges(this.state ?? ({} as C), state); @@ -41,7 +43,7 @@ const ScopedRoomContext = createContext | undefin // Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare) export const ScopedRoomContextProvider = memo( ({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => { - const contextRef = useRef(new EfficientContext()); + const contextRef = useRef(new EfficientContext(state)); useEffect(() => { contextRef.current.setState(state); }, [state]); @@ -55,14 +57,14 @@ export const ScopedRoomContextProvider = memo( }, ); -export function useScopedRoomContext>( - ...keys: K -): { [key in K[number]]: ContextValue[key] } { +type ScopedRoomContext> = { [key in K[number]]: ContextValue[key] }; + +export function useScopedRoomContext>(...keys: K): ScopedRoomContext { const context = useContext(ScopedRoomContext); - const [state, setState] = useState<{ [key in K[number]]: ContextValue[key] }>({} as any); + const [state, setState] = useState>(context?.state ?? ({} as ScopedRoomContext)); - useTypedEventEmitter(context, NotificationStateEvents.Update, (updatedKeys: K[]): void => { - if (context?.state && updatedKeys.some((updatedKey) => keys.includes(updatedKey as any))) { + useTypedEventEmitter(context, NotificationStateEvents.Update, (updatedKeys: K): void => { + if (context?.state && updatedKeys.some((updatedKey) => keys.includes(updatedKey))) { setState(context.state); } }); diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 820358da7fe..167618e4529 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -64,7 +64,7 @@ describe("EditWysiwygComposer", () => { rerender( - + , From 279be47101b205ac223df52d3d8b79214bc1b93b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 09:10:20 +0000 Subject: [PATCH 5/5] Add comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/contexts/ScopedRoomContext.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx index 1257a7fc4df..1222443d290 100644 --- a/src/contexts/ScopedRoomContext.tsx +++ b/src/contexts/ScopedRoomContext.tsx @@ -13,6 +13,11 @@ import { objectKeyChanges } from "../utils/objects.ts"; import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts"; import RoomContext from "./RoomContext.ts"; +// React Contexts with frequently changing values (like State where the object reference is changed on every update) +// cause performance issues by triggering a re-render on every component subscribed to that context. +// With ScopedRoomContext we're effectively setting up virtual contexts which are a subset of the overall context object +// and subscribers specify which fields they care about, and they will only be awoken on updates to those specific fields. + type ContextValue = ContextType; export enum NotificationStateEvents {