{ room={this.state.room} resizing={this.state.resizing} waitForCall={isVideoRoom(this.state.room)} - skipLobby={this.context.roomViewStore.skipCallLobby() ?? false} + skipLobby={this.props.context.roomViewStore.skipCallLobby() ?? false} role="main" /> {previewBar} @@ -2593,15 +2600,15 @@ export class RoomView extends React.Component { onAppsClick = null; onForgetClick = null; onSearchClick = null; - if (this.state.room.canInvite(this.context.client.getSafeUserId())) { + if (this.state.room.canInvite(this.props.context.client.getSafeUserId())) { onInviteClick = this.onInviteClick; } viewingCall = true; } - const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId()); + const myMember = this.state.room!.getMember(this.props.context.client!.getSafeUserId()); const showForgetButton = - !this.context.client.isGuest() && + !this.props.context.client.isGuest() && (([KnownMembership.Leave, KnownMembership.Ban] as Array).includes(myMembership) || myMember?.isKicked()); @@ -2665,4 +2672,6 @@ export class RoomView extends React.Component { } } -export default RoomView; +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index f24fa57d7d8..1a65e70b4c6 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, ReactNode } from "react"; +import React, { createRef, forwardRef, ReactNode } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -56,6 +56,7 @@ import { shouldShowFeedback } from "../../utils/Feedback"; interface IProps { isPanelCollapsed: boolean; children?: ReactNode; + context: React.ContextType; } type PartialDOMRect = Pick; @@ -84,25 +85,21 @@ const below = (rect: PartialDOMRect): MenuProps => { }; }; -export default class UserMenu extends React.Component { - public static contextType = SDKContext; - public context!: React.ContextType; - +class UserMenu extends React.Component { private dispatcherRef?: string; private themeWatcherRef?: string; 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.context = context; this.state = { contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), isHighContrast: this.isUserOnHighContrastTheme(), selectedSpace: SpaceStore.instance.activeSpaceRoom, - showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(), + showLiveAvatarAddon: this.props.context.voiceBroadcastRecordingsStore.hasCurrent(), }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); @@ -110,7 +107,7 @@ export default class UserMenu extends React.Component { } private get hasHomePage(): boolean { - return !!getHomePageUrl(SdkConfig.get(), this.context.client!); + return !!getHomePageUrl(SdkConfig.get(), this.props.context.client!); } private onCurrentVoiceBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => { @@ -120,7 +117,7 @@ export default class UserMenu extends React.Component { }; public componentDidMount(): void { - this.context.voiceBroadcastRecordingsStore.on( + this.props.context.voiceBroadcastRecordingsStore.on( VoiceBroadcastRecordingsStoreEvent.CurrentChanged, this.onCurrentVoiceBroadcastRecordingChanged, ); @@ -134,7 +131,7 @@ export default class UserMenu extends React.Component { if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); - this.context.voiceBroadcastRecordingsStore.off( + this.props.context.voiceBroadcastRecordingsStore.off( VoiceBroadcastRecordingsStoreEvent.CurrentChanged, this.onCurrentVoiceBroadcastRecordingChanged, ); @@ -496,3 +493,7 @@ export default class UserMenu extends React.Component { ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index f623ae7dcbb..5fd1bb422b2 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent, SyntheticEvent } from "react"; +import React, { ChangeEvent, forwardRef, 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"; @@ -60,6 +60,7 @@ interface IProps { // Called when the SSO login completes onTokenLoginCompleted: () => void; + context: React.ContextType; } interface IState { @@ -70,14 +71,9 @@ interface IState { flows: LoginFlow[]; } -export default class SoftLogout extends React.Component { - public static contextType = SDKContext; - public context!: React.ContextType; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); - - this.context = context; +class SoftLogout extends React.Component { + public constructor(props: IProps) { + super(props); this.state = { loginView: LoginView.Loading, @@ -104,7 +100,7 @@ export default class SoftLogout extends React.Component { if (!wipeData) return; logger.log("Clearing data from soft-logged-out session"); - Lifecycle.logout(this.context.oidcClientStore); + Lifecycle.logout(this.props.context.oidcClientStore); }, }); }; @@ -338,3 +334,7 @@ export default class SoftLogout extends React.Component { ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 828f9691dae..a78c3da01d8 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { MatrixEvent, Room, @@ -66,6 +66,7 @@ interface IProps { searchQuery: string; onClose(): void; onSearchQueryChanged: (query: string) => void; + context: React.ContextType; } interface IState { @@ -77,18 +78,16 @@ interface IState { truncateAtInvited: number; } -export default class MemberList extends React.Component { +class MemberList extends React.Component { private readonly showPresence: boolean; private mounted = false; - public static contextType = SDKContext; - public context!: React.ContextType; private tiles: Map = new Map(); - public constructor(props: IProps, context: React.ContextType) { + public constructor(props: IProps) { super(props); this.state = this.getMembersState([], []); - this.showPresence = context?.memberListStore.isPresenceEnabled() ?? true; + this.showPresence = props.context?.memberListStore.isPresenceEnabled() ?? true; this.mounted = true; this.listenForMembersChanges(); } @@ -218,7 +217,7 @@ export default class MemberList extends React.Component { if (showLoadingSpinner) { this.setState({ loading: true }); } - const { joined, invited } = await this.context.memberListStore.loadMemberList( + const { joined, invited } = await this.props.context.memberListStore.loadMemberList( this.props.roomId, this.props.searchQuery, ); @@ -449,3 +448,7 @@ export default class MemberList extends React.Component { inviteToRoom(room); }; } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 3009c81a17f..5cd96aad05c 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; +import React, { forwardRef, ReactNode } from "react"; import { SERVICE_TYPES, HTTPError, IThreepid, ThreepidMedium } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -61,6 +61,7 @@ import { SDKContext } from "../../../../../contexts/SDKContext"; interface IProps { closeSettingsFn: () => void; + context: React.ContextType; } interface IState { @@ -92,17 +93,13 @@ interface IState { canMake3pidChanges: boolean; } -export default class GeneralUserSettingsTab extends React.Component { - public static contextType = SDKContext; - public context!: React.ContextType; - +class GeneralUserSettingsTab extends React.Component { private readonly dispatcherRef: string; - public constructor(props: IProps, context: React.ContextType) { + public constructor(props: IProps) { super(props); - this.context = context; - const cli = this.context.client!; + const cli = this.props.context.client!; this.state = { language: languageHandler.getCurrentLanguage(), @@ -151,7 +148,7 @@ export default class GeneralUserSettingsTab extends React.Component { if (payload.action === "id_server_changed") { - this.setState({ haveIdServer: Boolean(this.context.client!.getIdentityServerUrl()) }); + this.setState({ haveIdServer: Boolean(this.props.context.client!.getIdentityServerUrl()) }); this.getThreepidState(); } }; @@ -165,7 +162,7 @@ export default class GeneralUserSettingsTab extends React.Component { - const cli = this.context.client!; + const cli = this.props.context.client!; const capabilities = await cli.getCapabilities(); // this is cached const changePasswordCap = capabilities["m.change_password"]; @@ -175,8 +172,8 @@ export default class GeneralUserSettingsTab extends React.Component { - const cli = this.context.client!; + const cli = this.props.context.client!; // Check to see if terms need accepting this.checkTerms(); @@ -213,7 +210,7 @@ export default class GeneralUserSettingsTab extends React.Component { // By starting the terms flow we get the logic for checking which terms the user has signed // for free. So we might as well use that for our own purposes. - const idServerUrl = this.context.client!.getIdentityServerUrl(); + const idServerUrl = this.props.context.client!.getIdentityServerUrl(); if (!this.state.haveIdServer || !idServerUrl) { this.setState({ idServerHasUnsignedTerms: false }); return; @@ -223,7 +220,7 @@ export default class GeneralUserSettingsTab extends React.Component { return new Promise((resolve, reject) => { @@ -573,3 +570,9 @@ export default class GeneralUserSettingsTab extends React.Component>((props, ref) => ( + + {(context) => } + +)); diff --git a/test/components/structures/RoomView-test.tsx b/test/components/structures/RoomView-test.tsx index 31f5c896aec..166c11f5b5d 100644 --- a/test/components/structures/RoomView-test.tsx +++ b/test/components/structures/RoomView-test.tsx @@ -53,7 +53,7 @@ import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { Action } from "../../../src/dispatcher/actions"; import dis, { defaultDispatcher } from "../../../src/dispatcher/dispatcher"; import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload"; -import { RoomView as _RoomView } from "../../../src/components/structures/RoomView"; +import _RoomView from "../../../src/components/structures/RoomView"; import ResizeNotifier from "../../../src/utils/ResizeNotifier"; import SettingsStore from "../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../src/settings/SettingLevel"; @@ -112,7 +112,7 @@ describe("RoomView", () => { jest.clearAllMocks(); }); - const mountRoomView = async (ref?: RefObject<_RoomView>): Promise => { + const mountRoomView = async (ref?: RefObject>): Promise => { if (stores.roomViewStore.getRoomId() !== room.roomId) { const switchedRoom = new Promise((resolve) => { const subFn = () => { @@ -185,8 +185,8 @@ describe("RoomView", () => { await flushPromises(); return roomView; }; - const getRoomViewInstance = async (): Promise<_RoomView> => { - const ref = createRef<_RoomView>(); + const getRoomViewInstance = async (): Promise> => { + const ref = createRef>(); await mountRoomView(ref); return ref.current!; }; @@ -197,7 +197,7 @@ describe("RoomView", () => { }); describe("when there is an old room", () => { - let instance: _RoomView; + let instance: React.ComponentRef; let oldRoom: Room; beforeEach(async () => { @@ -596,7 +596,7 @@ describe("RoomView", () => { const eventMapper = (obj: Partial) => new MatrixEvent(obj); - const roomViewRef = createRef<_RoomView>(); + const roomViewRef = createRef>(); const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef); // @ts-ignore - triggering a search organically is a lot of work roomViewRef.current!.setState({ @@ -657,7 +657,7 @@ describe("RoomView", () => { const eventMapper = (obj: Partial) => new MatrixEvent(obj); - const roomViewRef = createRef<_RoomView>(); + const roomViewRef = createRef>(); const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef); // @ts-ignore - triggering a search organically is a lot of work roomViewRef.current!.setState({ diff --git a/test/components/views/rooms/MemberList-test.tsx b/test/components/views/rooms/MemberList-test.tsx index 3b7c5a13bd4..dbf5b270df4 100644 --- a/test/components/views/rooms/MemberList-test.tsx +++ b/test/components/views/rooms/MemberList-test.tsx @@ -59,7 +59,7 @@ describe("MemberList", () => { let client: MatrixClient; let root: RenderResult; let memberListRoom: Room; - let memberList: MemberList; + let memberList: React.ComponentRef; let adminUsers: RoomMember[] = []; let moderatorUsers: RoomMember[] = []; @@ -214,7 +214,7 @@ describe("MemberList", () => { memberListRoom.currentState.members[member.userId] = member; } - const gatherWrappedRef = (r: MemberList) => { + const gatherWrappedRef = (r: React.ComponentRef) => { memberList = r; }; const context = new TestSdkContext(); From dc4c33a07be138370bbba2ecacb9caa4486f49dc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 12:40:07 +0100 Subject: [PATCH 2/8] Remove legacy consumers of the OverflowMenuContext in favour of HOCs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/MessageComposerButtons.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 7f23efbce2b..88e8f2def37 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -289,20 +289,22 @@ function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | nu } function pollButton(room: Room, relation?: IEventRelation): ReactElement { - return ; + return ( + + {(context) => } + + ); } interface IPollButtonProps { room: Room; relation?: IEventRelation; + context: React.ContextType; } class PollButton extends React.PureComponent { - public static contextType = OverflowMenuContext; - public context!: React.ContextType; - private onCreateClick = (): void => { - this.context?.(); // close overflow menu + this.props.context?.(); // close overflow menu const canSend = this.props.room.currentState.maySendEvent( M_POLL_START.name, MatrixClientPeg.safeGet().getSafeUserId(), From f4296b48933bce4ffc07b17eac8f679de91a513d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 12:42:33 +0100 Subject: [PATCH 3/8] Remove legacy consumers of the RovingTabIndexContext in favour of HOCs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/emojipicker/Search.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index 33549b7489a..9d4b10bed94 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { _t } from "../../../languageHandler"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; @@ -27,12 +27,10 @@ interface IProps { onChange(value: string): void; onEnter(): void; onKeyDown(event: React.KeyboardEvent): void; + context: React.ContextType; } class Search extends React.PureComponent { - public static contextType = RovingTabIndexContext; - public context!: React.ContextType; - private inputRef = React.createRef(); public componentDidMount(): void { @@ -78,7 +76,7 @@ class Search extends React.PureComponent { onChange={(ev) => this.props.onChange(ev.target.value)} onKeyDown={this.onKeyDown} ref={this.inputRef} - aria-activedescendant={this.context.state.activeRef?.current?.id} + aria-activedescendant={this.props.context.state.activeRef?.current?.id} aria-controls="mx_EmojiPicker_body" aria-haspopup="grid" aria-autocomplete="list" @@ -89,4 +87,8 @@ class Search extends React.PureComponent { } } -export default Search; +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); From 9fe036ae1d37595e76a0b46b81e9f53f17b99c78 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 14:31:48 +0100 Subject: [PATCH 4/8] Remove legacy consumers of the RoomContext in favour of HOCs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/FilePanel.tsx | 13 +-- src/components/structures/MessagePanel.tsx | 63 ++++++++------ .../structures/NotificationPanel.tsx | 15 ++-- src/components/structures/RoomView.tsx | 8 +- src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/ThreadView.tsx | 18 ++-- src/components/structures/TimelinePanel.tsx | 31 ++++--- .../WaitingForThirdPartyRoomView.tsx | 6 +- .../structures/grouper/BaseGrouper.ts | 4 +- .../structures/grouper/CreationGrouper.tsx | 5 +- .../structures/grouper/MainGrouper.tsx | 7 +- .../context_menus/MessageContextMenu.tsx | 20 +++-- .../views/elements/EventListSummary.tsx | 18 ++-- src/components/views/elements/ReplyChain.tsx | 8 +- .../views/emojipicker/ReactionPicker.tsx | 24 +++--- src/components/views/messages/CallEvent.tsx | 2 +- src/components/views/messages/IBodyProps.ts | 3 - src/components/views/messages/MAudioBody.tsx | 23 +++-- src/components/views/messages/MFileBody.tsx | 24 +++--- src/components/views/messages/MImageBody.tsx | 31 ++++--- .../views/messages/MImageReplyBody.tsx | 18 +++- .../views/messages/MStickerBody.tsx | 16 +++- src/components/views/messages/MVideoBody.tsx | 23 +++-- .../views/messages/MVoiceMessageBody.tsx | 18 +++- .../views/messages/MessageActionBar.tsx | 25 +++--- .../views/messages/MessageEvent.tsx | 22 ++--- .../views/messages/ReactionsRow.tsx | 27 +++--- src/components/views/messages/TextualBody.tsx | 19 +++-- .../views/messages/TextualEvent.tsx | 13 +-- .../views/right_panel/TimelineCard.tsx | 17 ++-- src/components/views/rooms/Autocomplete.tsx | 13 +-- .../views/rooms/BasicMessageComposer.tsx | 2 +- .../views/rooms/EditMessageComposer.tsx | 39 +++++---- src/components/views/rooms/EventTile.tsx | 83 ++++++++++--------- .../views/rooms/LegacyRoomHeader.tsx | 23 +++-- .../views/rooms/MessageComposer.tsx | 40 ++++----- src/components/views/rooms/ReplyPreview.tsx | 13 +-- src/components/views/rooms/ReplyTile.tsx | 6 +- .../views/rooms/SearchResultTile.tsx | 24 +++--- .../views/rooms/SendMessageComposer.tsx | 49 ++++++----- .../views/rooms/VoiceRecordComposerTile.tsx | 15 ++-- .../components/WysiwygAutocomplete.tsx | 2 +- .../components/WysiwygComposer.tsx | 2 +- .../hooks/useInputEventProcessor.ts | 4 +- .../hooks/usePlainTextListeners.ts | 4 +- .../rooms/wysiwyg_composer/hooks/utils.ts | 2 +- src/editor/autocomplete.ts | 2 +- src/events/EventTileFactory.tsx | 7 +- .../structures/TimelinePanel-test.tsx | 10 +-- .../views/rooms/LegacyRoomHeader-test.tsx | 9 +- .../rooms/VoiceRecordComposerTile-test.tsx | 2 +- .../components/WysiwygAutocomplete-test.tsx | 2 +- 52 files changed, 509 insertions(+), 367 deletions(-) diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 38368634315..a1d4808b022 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from "react"; +import React, { createRef, forwardRef } from "react"; import { Filter, EventTimelineSet, @@ -45,6 +45,7 @@ interface IProps { roomId: string; onClose: () => void; resizeNotifier: ResizeNotifier; + context: React.ContextType; } interface IState { @@ -56,8 +57,6 @@ interface IState { * Component which shows the filtered file using a TimelinePanel */ class FilePanel extends React.Component { - public static contextType = RoomContext; - // This is used to track if a decrypted event was a live event and should be // added to the timeline. private decryptingEvents = new Set(); @@ -267,7 +266,7 @@ class FilePanel extends React.Component { return ( { return ( @@ -312,4 +311,6 @@ class FilePanel extends React.Component { } } -export default FilePanel; +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 3a97f27b381..121577855c4 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, ReactNode, TransitionEvent } from "react"; +import React, { createRef, forwardRef, ReactNode, TransitionEvent } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import { Room, MatrixClient, RoomStateEvent, EventStatus, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; @@ -31,7 +31,6 @@ import EventTile, { GetRelationsForEvent, IReadReceiptProps, isEligibleForSpecialReceipt, - UnwrappedEventTile, } from "../views/rooms/EventTile"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -188,6 +187,7 @@ interface IProps { disableGrouping?: boolean; callEventGroupers: Map; + context: React.ContextType; } interface IState { @@ -203,10 +203,7 @@ interface IReadReceiptForUser { /* (almost) stateless UI component which builds the event tiles in the room timeline. */ -export default class MessagePanel extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - +class MessagePanel extends React.Component { public static defaultProps = { disableGrouping: false, }; @@ -256,13 +253,13 @@ export default class MessagePanel extends React.Component { private scrollPanel = createRef(); private readonly showTypingNotificationsWatcherRef: string; - private eventTiles: Record = {}; + private eventTiles: Record> = {}; // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. public grouperKeyMap = new WeakMap(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { // previous positions the read marker has been in, so we can @@ -320,7 +317,7 @@ export default class MessagePanel extends React.Component { defaultDispatcher.dispatch({ action: Action.EditEvent, event: !event?.isRedacted() ? event : null, - timelineRenderingType: this.context.timelineRenderingType, + timelineRenderingType: this.props.context.timelineRenderingType, }); } } @@ -354,7 +351,7 @@ export default class MessagePanel extends React.Component { return this.eventTiles[eventId]?.ref?.current ?? undefined; } - public getTileForEventId(eventId?: string): UnwrappedEventTile | undefined { + public getTileForEventId(eventId?: string): React.ComponentRef | undefined { if (!this.eventTiles || !eventId) { return undefined; } @@ -454,7 +451,7 @@ export default class MessagePanel extends React.Component { }; public get showHiddenEvents(): boolean { - return this.context?.showHiddenEvents ?? this._showHiddenEvents; + return this.props.context?.showHiddenEvents ?? this._showHiddenEvents; } // TODO: Implement granular (per-room) hide options @@ -481,7 +478,7 @@ export default class MessagePanel extends React.Component { // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; - return !shouldHideEvent(mxEv, this.context); + return !shouldHideEvent(mxEv, this.props.context); } public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode { @@ -592,7 +589,9 @@ export default class MessagePanel extends React.Component { } try { - return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType)); + return localStorage.getItem( + editorRoomKey(this.props.room.roomId, this.props.context.timelineRenderingType), + ); } catch (err) { logger.error(err); return null; @@ -775,13 +774,25 @@ export default class MessagePanel extends React.Component { willWantSeparator === SeparatorKind.Date || mxEv.getSender() !== nextEv.getSender() || getEventDisplayInfo(cli, nextEv, this.showHiddenEvents).isInfoMessage || - !shouldFormContinuation(mxEv, nextEv, cli, this.showHiddenEvents, this.context.timelineRenderingType); + !shouldFormContinuation( + mxEv, + nextEv, + cli, + this.showHiddenEvents, + this.props.context.timelineRenderingType, + ); } // is this a continuation of the previous message? const continuation = wantsSeparator === SeparatorKind.None && - shouldFormContinuation(prevEvent, mxEv, cli, this.showHiddenEvents, this.context.timelineRenderingType); + shouldFormContinuation( + prevEvent, + mxEv, + cli, + this.showHiddenEvents, + this.props.context.timelineRenderingType, + ); const eventId = mxEv.getId()!; const highlight = eventId === this.props.highlightedEventId; @@ -826,7 +837,7 @@ export default class MessagePanel extends React.Component { } public wantsSeparator(prevEvent: MatrixEvent | null, mxEvent: MatrixEvent): SeparatorKind { - if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { + if (this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { return SeparatorKind.None; } @@ -863,13 +874,13 @@ export default class MessagePanel extends React.Component { return null; } - const receiptDestination = this.context.threadId ? room.getThread(this.context.threadId) : room; + const receiptDestination = this.props.context.threadId ? room.getThread(this.props.context.threadId) : room; const receipts: IReadReceiptProps[] = []; if (!receiptDestination) { logger.debug( - "Discarding request, could not find the receiptDestination for event: " + this.context.threadId, + "Discarding request, could not find the receiptDestination for event: " + this.props.context.threadId, ); return receipts; } @@ -950,7 +961,7 @@ export default class MessagePanel extends React.Component { return receiptsByEvent; } - private collectEventTile = (eventId: string, node: UnwrappedEventTile): void => { + private collectEventTile = (eventId: string, node: React.ComponentRef): void => { this.eventTiles[eventId] = node; }; @@ -1033,7 +1044,7 @@ export default class MessagePanel extends React.Component { if ( this.props.room && this.state.showTypingNotifications && - this.context.timelineRenderingType === TimelineRenderingType.Room + this.props.context.timelineRenderingType === TimelineRenderingType.Room ) { whoIsTyping = ( { } const classes = classNames(this.props.className, { - mx_MessagePanel_narrow: this.context.narrow, + mx_MessagePanel_narrow: this.props.context.narrow, }); return ( @@ -1079,10 +1090,14 @@ export default class MessagePanel extends React.Component { } } +export default forwardRef>((props, ref) => ( + {(context) => } +)); + /** - * Holds on to an event, caching the information about it in the context of the current messages list. + * Holds on to an event, caching the information about it in the props.context. of the current messages list. * Avoids calling shouldShowEvent more times than we need to. - * Simplifies threading of event context like whether it's the last successful event we sent which cannot be determined + * Simplifies threading of event props.context. like whether it's the last successful event we sent which cannot be determined * by a consumer from the event alone, so has to be done by the event list processing code earlier. */ export interface WrappedEvent { diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 0da27a19b10..082e01d9f98 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../languageHandler"; @@ -29,6 +29,7 @@ import Heading from "../views/typography/Heading"; interface IProps { onClose(): void; + context: React.ContextType; } interface IState { @@ -38,9 +39,7 @@ interface IState { /* * Component which shows the global notification list using a TimelinePanel */ -export default class NotificationPanel extends React.PureComponent { - public static contextType = RoomContext; - +class NotificationPanel extends React.PureComponent { private card = React.createRef(); public constructor(props: IProps) { @@ -86,7 +85,7 @@ export default class NotificationPanel extends React.PureComponent>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d23eafc625c..1e171162ed1 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -323,7 +323,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { ) : ( { private roomView = createRef(); private searchResultsPanel = createRef(); - private messagePanel: TimelinePanel | null = null; + private messagePanel: React.ComponentRef | null = null; private roomViewBody = createRef(); public constructor(props: IRoomProps) { @@ -1958,7 +1958,7 @@ class RoomView extends React.Component { * We pass it down to the scroll panel. */ public handleScrollKey = (ev: React.KeyboardEvent | KeyboardEvent): void => { - let panel: ScrollPanel | TimelinePanel | undefined; + let panel: ScrollPanel | React.ComponentRef | undefined; if (this.searchResultsPanel.current) { panel = this.searchResultsPanel.current; } else if (this.messagePanel) { @@ -1980,7 +1980,7 @@ class RoomView extends React.Component { // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. - private gatherTimelinePanelRef = (r: TimelinePanel | null): void => { + private gatherTimelinePanelRef = (r: React.ComponentRef | null): void => { this.messagePanel = r; }; diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index d1e83601747..7dab4d68967 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -230,7 +230,7 @@ const EmptyThread: React.FC = ({ hasThreads, filterOption, sh const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => { const mxClient = useContext(MatrixClientContext); const roomContext = useContext(RoomContext); - const timelinePanel = useRef(null); + const timelinePanel = useRef | null>(null); const card = useRef(null); const closeButonRef = useRef(null); diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index a345faf9f74..1a29fd61c80 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent } from "react"; +import React, { createRef, forwardRef, KeyboardEvent } from "react"; import { Thread, THREAD_RELATION_TYPE, @@ -70,6 +70,7 @@ interface IProps { initialEvent?: MatrixEvent; isInitialEventHighlighted?: boolean; initialEventScrollIntoView?: boolean; + context: React.ContextType; } interface IState { @@ -81,13 +82,10 @@ interface IState { narrow: boolean; } -export default class ThreadView extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - +class ThreadView extends React.Component { private dispatcherRef: string | null = null; private readonly layoutWatcherRef: string; - private timelinePanel = createRef(); + private timelinePanel = createRef>(); private card = createRef(); // Set by setEventId in ctor. @@ -398,12 +396,12 @@ export default class ThreadView extends React.Component { { ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index ba3c4d203b0..8010304ac6f 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, ReactNode } from "react"; +import React, { createRef, forwardRef, ReactNode } from "react"; import ReactDOM from "react-dom"; import { Room, @@ -167,6 +167,7 @@ interface IProps { hideThreadedMessages?: boolean; disableGrouping?: boolean; + context: React.ContextType; } interface IState { @@ -244,10 +245,7 @@ interface IEventIndexOpts { * * Also responsible for handling and sending read receipts. */ -class TimelinePanel extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - +export class TimelinePanel extends React.Component { // a map from room id to read marker event timestamp public static roomReadMarkerTsMap: Record = {}; @@ -264,7 +262,7 @@ class TimelinePanel extends React.Component { private lastRRSentEventId: string | null | undefined = undefined; private lastRMSentEventId: string | null | undefined = undefined; - private readonly messagePanel = createRef(); + private readonly messagePanel = createRef>(); private readonly dispatcherRef: string; private timelineWindow?: TimelineWindow; private overlayTimelineWindow?: TimelineWindow; @@ -276,9 +274,8 @@ class TimelinePanel extends React.Component { private callEventGroupers = new Map(); private initialReadMarkerId: string | null = null; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); - this.context = context; + public constructor(props: IProps) { + super(props); debuglog("mounting"); @@ -500,7 +497,7 @@ class TimelinePanel extends React.Component { } logger.debug( - `TimelinePanel(${this.context.timelineRenderingType}): Debugging info for ${room?.roomId}\n` + + `TimelinePanel(${this.props.context.timelineRenderingType}): Debugging info for ${room?.roomId}\n` + `\tevents(${eventIdList.length})=${JSON.stringify(eventIdList)}\n` + `\trenderedEventIds(${renderedEventIds?.length ?? 0})=` + `${JSON.stringify(renderedEventIds)}\n` + @@ -736,7 +733,7 @@ class TimelinePanel extends React.Component { return; } - if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) { + if (!Thread.hasServerSideSupport && this.props.context.timelineRenderingType === TimelineRenderingType.Thread) { if (toStartOfTimeline && !this.state.canBackPaginate) { this.setState({ canBackPaginate: true, @@ -1780,8 +1777,8 @@ class TimelinePanel extends React.Component { pendingEvents, ); - if (this.context.timelineRenderingType === TimelineRenderingType.Thread) { - return threadId === this.context.threadId; + if (this.props.context.timelineRenderingType === TimelineRenderingType.Thread) { + return threadId === this.props.context.threadId; } { return shouldLiveInRoom; @@ -1812,7 +1809,7 @@ class TimelinePanel extends React.Component { const room = this.props.timelineSet.room; const isThreadTimeline = [TimelineRenderingType.Thread, TimelineRenderingType.ThreadsList].includes( - this.context.timelineRenderingType, + this.props.context.timelineRenderingType, ); if (events.length === 0 || !room || !cli.isRoomEncrypted(room.roomId) || isThreadTimeline) { logger.debug("checkForPreJoinUISI: showing all messages, skipping check"); @@ -1881,7 +1878,7 @@ class TimelinePanel extends React.Component { /* Threads do not have server side support for read receipts and the concept is very tied to the main room timeline, we are forcing the timeline to send read receipts for threaded events */ - if (this.context.timelineRenderingType === TimelineRenderingType.Thread) { + if (this.props.context.timelineRenderingType === TimelineRenderingType.Thread) { return 0; } const index = this.state.events.findIndex((ev) => ev.getId() === evId); @@ -2193,4 +2190,6 @@ function serializeEventIdsFromTimelineSets(timelineSets: EventTimelineSet[]): { return serializedEventIdsInTimelineSet; } -export default TimelinePanel; +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 1b61abb3bb8..2b16642718f 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -26,7 +26,7 @@ import RoomHeader from "../views/rooms/RoomHeader"; import ScrollPanel from "./ScrollPanel"; import EventTileBubble from "../views/messages/EventTileBubble"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; -import { UnwrappedEventTile } from "../views/rooms/EventTile"; +import EventTile from "../views/rooms/EventTile"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; import SettingsStore from "../../settings/SettingsStore"; @@ -53,7 +53,7 @@ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resize ) : ( = ({ roomView, resize subtitle={_t("room|waiting_for_join_subtitle", { brand })} /> - + diff --git a/src/components/structures/grouper/BaseGrouper.ts b/src/components/structures/grouper/BaseGrouper.ts index 5821797ad54..57b07e4f2b3 100644 --- a/src/components/structures/grouper/BaseGrouper.ts +++ b/src/components/structures/grouper/BaseGrouper.ts @@ -31,7 +31,7 @@ import MessagePanel, { WrappedEvent } from "../MessagePanel"; * when determining things such as whether a date separator is necessary */ export abstract class BaseGrouper { - public static canStartGroup = (_panel: MessagePanel, _ev: WrappedEvent): boolean => true; + public static canStartGroup = (_panel: React.ComponentRef, _ev: WrappedEvent): boolean => true; public events: WrappedEvent[] = []; // events that we include in the group but then eject out and place above the group. @@ -39,7 +39,7 @@ export abstract class BaseGrouper { public readMarker: ReactNode; public constructor( - public readonly panel: MessagePanel, + public readonly panel: React.ComponentRef, public readonly firstEventAndShouldShow: WrappedEvent, public readonly prevEvent: MatrixEvent | null, public readonly lastShownEvent: MatrixEvent | undefined, diff --git a/src/components/structures/grouper/CreationGrouper.tsx b/src/components/structures/grouper/CreationGrouper.tsx index fa91a1bd90c..bcb5552968c 100644 --- a/src/components/structures/grouper/CreationGrouper.tsx +++ b/src/components/structures/grouper/CreationGrouper.tsx @@ -33,7 +33,10 @@ import { SeparatorKind } from "../../views/messages/TimelineSeparator"; // the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event export class CreationGrouper extends BaseGrouper { - public static canStartGroup = function (_panel: MessagePanel, { event }: WrappedEvent): boolean { + public static canStartGroup = function ( + _panel: React.ComponentRef, + { event }: WrappedEvent, + ): boolean { return event.getType() === EventType.RoomCreate; }; diff --git a/src/components/structures/grouper/MainGrouper.tsx b/src/components/structures/grouper/MainGrouper.tsx index 28a62d7ac9d..721bc93c9c4 100644 --- a/src/components/structures/grouper/MainGrouper.tsx +++ b/src/components/structures/grouper/MainGrouper.tsx @@ -36,7 +36,10 @@ const groupedStateEvents = [ // Wrap consecutive grouped events in a ListSummary export class MainGrouper extends BaseGrouper { - public static canStartGroup = function (panel: MessagePanel, { event: ev, shouldShow }: WrappedEvent): boolean { + public static canStartGroup = function ( + panel: React.ComponentRef, + { event: ev, shouldShow }: WrappedEvent, + ): boolean { if (!shouldShow) return false; if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { @@ -55,7 +58,7 @@ export class MainGrouper extends BaseGrouper { }; public constructor( - public readonly panel: MessagePanel, + public readonly panel: React.ComponentRef, public readonly firstEventAndShouldShow: WrappedEvent, public readonly prevEvent: MatrixEvent | null, public readonly lastShownEvent: MatrixEvent | undefined, diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index e0fca0a4c0b..9c87d738949 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, useContext } from "react"; +import React, { createRef, forwardRef, useContext } from "react"; import { EventStatus, MatrixEvent, @@ -123,6 +123,7 @@ interface IProps extends MenuProps { link?: string; getRelationsForEvent?: GetRelationsForEvent; + context: React.ContextType; } interface IState { @@ -131,11 +132,8 @@ interface IState { reactionPickerDisplayed: boolean; } -export default class MessageContextMenu extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - - private reactButtonRef = createRef(); // XXX Ref to a functional component +class MessageContextMenu extends React.Component { + private reactButtonRef = createRef(); public constructor(props: IProps) { super(props); @@ -315,7 +313,7 @@ export default class MessageContextMenu extends React.Component editEvent( MatrixClientPeg.safeGet(), this.props.mxEvent, - this.context.timelineRenderingType, + this.props.context.timelineRenderingType, this.props.getRelationsForEvent, ); this.closeMenu(); @@ -325,7 +323,7 @@ export default class MessageContextMenu extends React.Component dis.dispatch({ action: "reply_to_event", event: this.props.mxEvent, - context: this.context.timelineRenderingType, + context: this.props.context.timelineRenderingType, }); this.closeMenu(); }; @@ -733,3 +731,9 @@ export default class MessageContextMenu extends React.Component ); } } + +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index a1270427ccd..932f9d992a1 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, ReactNode } from "react"; +import React, { ComponentProps, forwardRef, ReactNode } from "react"; import { MatrixEvent, RoomMember, EventType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; @@ -43,7 +43,8 @@ interface IProps extends Omit, "s // The maximum number of avatars to display in the summary avatarsMaxLength?: number; // The currently selected layout - layout: Layout; + layout?: Layout; + context: React.ContextType; } interface IUserEvents { @@ -77,12 +78,9 @@ enum TransitionType { const SEP = ","; -export default class EventListSummary extends React.Component< +class EventListSummary extends React.Component< IProps & Required> > { - public static contextType = RoomContext; - public context!: React.ContextType; - public static defaultProps = { summaryLength: 1, threshold: 3, @@ -527,7 +525,7 @@ export default class EventListSummary extends React.Component< let displayName = userKey; if (e.isRedacted()) { - const sender = this.context?.room?.getMember(userKey); + const sender = this.props.context?.room?.getMember(userKey); if (sender) { displayName = sender.name; latestUserAvatarMember.set(userKey, sender); @@ -569,3 +567,9 @@ export default class EventListSummary extends React.Component< ); } } + +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index b7e833a629b..ae3d31a0350 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -31,7 +31,6 @@ import ReplyTile from "../rooms/ReplyTile"; import { Pill, PillType } from "./Pill"; import AccessibleButton from "./AccessibleButton"; import { getParentEventId, shouldDisplayReply } from "../../../utils/Reply"; -import RoomContext from "../../../contexts/RoomContext"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { GetRelationsForEvent } from "../rooms/EventTile"; @@ -72,15 +71,12 @@ interface IState { // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would // be low as each event being loaded (after the first) is triggered by an explicit user action. export default class ReplyChain extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - private unmounted = false; private room: Room; private blockquoteRef = React.createRef(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { events: [], diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index 075a6e6cee7..4a671a8b126 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { MatrixEvent, EventType, RelationType, Relations, RelationsEvent } from "matrix-js-sdk/src/matrix"; import EmojiPicker from "./EmojiPicker"; @@ -29,6 +29,7 @@ interface IProps { mxEvent: MatrixEvent; reactions?: Relations | null | undefined; onFinished(): void; + context: React.ContextType; } interface IState { @@ -36,11 +37,8 @@ interface IState { } class ReactionPicker extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { selectedEmojis: new Set(Object.keys(this.getReactions())), @@ -95,12 +93,12 @@ class ReactionPicker extends React.Component { this.props.onFinished(); const myReactions = this.getReactions(); if (myReactions.hasOwnProperty(reaction)) { - if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return false; + if (this.props.mxEvent.isRedacted() || !this.props.context.canSelfRedact) return false; MatrixClientPeg.safeGet().redactEvent(this.props.mxEvent.getRoomId()!, myReactions[reaction]); dis.dispatch({ action: Action.FocusAComposer, - context: this.context.timelineRenderingType, + context: this.props.context.timelineRenderingType, }); // Tell the emoji picker not to bump this in the more frequently used list. return false; @@ -115,7 +113,7 @@ class ReactionPicker extends React.Component { dis.dispatch({ action: "message_sent" }); dis.dispatch({ action: Action.FocusAComposer, - context: this.context.timelineRenderingType, + context: this.props.context.timelineRenderingType, }); return true; } @@ -123,7 +121,7 @@ class ReactionPicker extends React.Component { private isEmojiDisabled = (unicode: string): boolean => { if (!this.getReactions()[unicode]) return false; - if (this.context.canSelfRedact) return false; + if (this.props.context.canSelfRedact) return false; return true; }; @@ -140,4 +138,8 @@ class ReactionPicker extends React.Component { } } -export default ReactionPicker; +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index e37217c4224..ff3190f308f 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -163,7 +163,7 @@ interface CallEventProps { /** * An event tile representing an active or historical Element call. */ -export const CallEvent = forwardRef(({ mxEvent }, ref) => { +export const CallEvent = forwardRef(({ mxEvent }, ref) => { const client = useContext(MatrixClientContext); const call = useCall(mxEvent.getRoomId()!); const latestEvent = client diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts index fcc204dae3b..54631f538bc 100644 --- a/src/components/views/messages/IBodyProps.ts +++ b/src/components/views/messages/IBodyProps.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { LegacyRef } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; @@ -54,8 +53,6 @@ export interface IBodyProps { // helper function to access relations for this event getRelationsForEvent?: GetRelationsForEvent; - ref?: React.RefObject | LegacyRef; - // Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order. // This may be useful when displaying a preview of the event. inhibitInteraction?: boolean; diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index de30b65f724..9bd613585f7 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { IContent } from "matrix-js-sdk/src/matrix"; import { MediaEventContent } from "matrix-js-sdk/src/types"; @@ -31,16 +31,17 @@ import { PlaybackQueue } from "../../../audio/PlaybackQueue"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import MediaProcessingError from "./shared/MediaProcessingError"; +interface Props extends IBodyProps { + context: React.ContextType; +} + interface IState { error?: boolean; playback?: Playback; } -export default class MAudioBody extends React.PureComponent { - public static contextType = RoomContext; - public context!: React.ContextType; - - public constructor(props: IBodyProps) { +export class MAudioBody extends React.PureComponent { + public constructor(props: Props) { super(props); this.state = {}; @@ -88,9 +89,9 @@ export default class MAudioBody extends React.PureComponent protected get showFileBody(): boolean { return ( - this.context.timelineRenderingType !== TimelineRenderingType.Room && - this.context.timelineRenderingType !== TimelineRenderingType.Pinned && - this.context.timelineRenderingType !== TimelineRenderingType.Search + this.props.context.timelineRenderingType !== TimelineRenderingType.Room && + this.props.context.timelineRenderingType !== TimelineRenderingType.Pinned && + this.props.context.timelineRenderingType !== TimelineRenderingType.Search ); } @@ -131,3 +132,7 @@ export default class MAudioBody extends React.PureComponent ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 12d4c804168..be113926587 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { AllHTMLAttributes, createRef } from "react"; +import React, { AllHTMLAttributes, createRef, forwardRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { MediaEventContent } from "matrix-js-sdk/src/types"; @@ -97,17 +97,15 @@ export function computedStyle(element: HTMLElement | null): string { interface IProps extends IBodyProps { /* whether or not to show the default placeholder for the file. Defaults to true. */ - showGenericPlaceholder: boolean; + showGenericPlaceholder?: boolean; + context: React.ContextType; } interface IState { decryptedBlob?: Blob; } -export default class MFileBody extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - +class MFileBody extends React.Component { public static defaultProps = { showGenericPlaceholder: true, }; @@ -226,11 +224,11 @@ export default class MFileBody extends React.Component { let showDownloadLink = !this.props.showGenericPlaceholder || - (this.context.timelineRenderingType !== TimelineRenderingType.Room && - this.context.timelineRenderingType !== TimelineRenderingType.Search && - this.context.timelineRenderingType !== TimelineRenderingType.Pinned); + (this.props.context.timelineRenderingType !== TimelineRenderingType.Room && + this.props.context.timelineRenderingType !== TimelineRenderingType.Search && + this.props.context.timelineRenderingType !== TimelineRenderingType.Pinned); - if (this.context.timelineRenderingType === TimelineRenderingType.Thread) { + if (this.props.context.timelineRenderingType === TimelineRenderingType.Thread) { showDownloadLink = false; } @@ -348,7 +346,7 @@ export default class MFileBody extends React.Component { {_t("timeline|m.file|download_label", { text: this.linkText })} - {this.context.timelineRenderingType === TimelineRenderingType.File && ( + {this.props.context.timelineRenderingType === TimelineRenderingType.File && (
{this.content.info?.size ? fileSize(this.content.info.size) : ""}
@@ -368,3 +366,7 @@ export default class MFileBody extends React.Component { } } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 36f3a851687..2191a2cea4d 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, createRef, ReactNode } from "react"; +import React, { ComponentProps, createRef, forwardRef, ReactNode } from "react"; import { Blurhash } from "react-blurhash"; import classNames from "classnames"; import { CSSTransition, SwitchTransition } from "react-transition-group"; @@ -47,6 +47,10 @@ enum Placeholder { Blurhash, } +interface Props extends IBodyProps { + context: React.ContextType; +} + interface IState { contentUrl: string | null; thumbUrl: string | null; @@ -63,17 +67,14 @@ interface IState { placeholder: Placeholder; } -export default class MImageBody extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - +export class MImageBody extends React.Component { private unmounted = true; private image = createRef(); private timeout?: number; private sizeWatcher?: string; private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; - public constructor(props: IBodyProps) { + public constructor(props: Props) { super(props); this.reconnectedListener = createReconnectedListener(this.clearError); @@ -391,7 +392,9 @@ export default class MImageBody extends React.Component { protected getBanner(content: ImageContent): ReactNode { // Hide it for the threads list & the file panel where we show it as text anyway. if ( - [TimelineRenderingType.ThreadsList, TimelineRenderingType.File].includes(this.context.timelineRenderingType) + [TimelineRenderingType.ThreadsList, TimelineRenderingType.File].includes( + this.props.context.timelineRenderingType, + ) ) { return null; } @@ -601,11 +604,11 @@ export default class MImageBody extends React.Component { * link as the message action bar will fulfill that */ const hasMessageActionBar = - this.context.timelineRenderingType === TimelineRenderingType.Room || - this.context.timelineRenderingType === TimelineRenderingType.Pinned || - this.context.timelineRenderingType === TimelineRenderingType.Search || - this.context.timelineRenderingType === TimelineRenderingType.Thread || - this.context.timelineRenderingType === TimelineRenderingType.ThreadsList; + this.props.context.timelineRenderingType === TimelineRenderingType.Room || + this.props.context.timelineRenderingType === TimelineRenderingType.Pinned || + this.props.context.timelineRenderingType === TimelineRenderingType.Search || + this.props.context.timelineRenderingType === TimelineRenderingType.Thread || + this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList; if (!hasMessageActionBar) { return ; } @@ -648,6 +651,10 @@ export default class MImageBody extends React.Component { } } +export default forwardRef>((props, ref) => ( + {(context) => } +)); + interface PlaceholderIProps { hover?: boolean; maxWidth?: number; diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index ded0d374a80..bccb7178192 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -14,14 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { ImageContent } from "matrix-js-sdk/src/types"; -import MImageBody from "./MImageBody"; +import { MImageBody } from "./MImageBody"; +import { IBodyProps } from "./IBodyProps"; +import RoomContext from "../../../contexts/RoomContext"; const FORCED_IMAGE_HEIGHT = 44; -export default class MImageReplyBody extends MImageBody { +interface Props extends IBodyProps { + context: React.ContextType; +} + +class MImageReplyBody extends MImageBody { public onClick = (ev: React.MouseEvent): void => { ev.preventDefault(); }; @@ -43,3 +49,9 @@ export default class MImageReplyBody extends MImageBody { return
{thumbnail}
; } } + +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx index 073da22b2a1..f5d46f8037c 100644 --- a/src/components/views/messages/MStickerBody.tsx +++ b/src/components/views/messages/MStickerBody.tsx @@ -14,14 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, ReactNode } from "react"; +import React, { ComponentProps, forwardRef, ReactNode } from "react"; import { Tooltip } from "@vector-im/compound-web"; import { MediaEventContent } from "matrix-js-sdk/src/types"; -import MImageBody from "./MImageBody"; +import { MImageBody } from "./MImageBody"; import { BLURHASH_FIELD } from "../../../utils/image-media"; +import RoomContext from "../../../contexts/RoomContext"; +import { IBodyProps } from "./IBodyProps"; -export default class MStickerBody extends MImageBody { +interface Props extends IBodyProps { + context: React.ContextType; +} + +class MStickerBody extends MImageBody { // Mostly empty to prevent default behaviour of MImageBody protected onClick = (ev: React.MouseEvent): void => { ev.preventDefault(); @@ -83,3 +89,7 @@ export default class MStickerBody extends MImageBody { return null; // we don't need a banner, we have a tooltip } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index be6ae4442ce..a68e3b1907b 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; +import React, { forwardRef, ReactNode } from "react"; import { decode } from "blurhash"; import { MediaEventContent } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; @@ -30,6 +30,10 @@ import { ImageSize, suggestedSize as suggestedVideoSize } from "../../../setting import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import MediaProcessingError from "./shared/MediaProcessingError"; +interface Props extends IBodyProps { + context: React.ContextType; +} + interface IState { decryptedUrl: string | null; decryptedThumbnailUrl: string | null; @@ -40,14 +44,11 @@ interface IState { blurhashUrl: string | null; } -export default class MVideoBody extends React.PureComponent { - public static contextType = RoomContext; - public context!: React.ContextType; - +class MVideoBody extends React.PureComponent { private videoRef = React.createRef(); private sizeWatcher?: string; - public constructor(props: IBodyProps) { + public constructor(props: Props) { super(props); this.state = { @@ -221,9 +222,9 @@ export default class MVideoBody extends React.PureComponent protected get showFileBody(): boolean { return ( - this.context.timelineRenderingType !== TimelineRenderingType.Room && - this.context.timelineRenderingType !== TimelineRenderingType.Pinned && - this.context.timelineRenderingType !== TimelineRenderingType.Search + this.props.context.timelineRenderingType !== TimelineRenderingType.Room && + this.props.context.timelineRenderingType !== TimelineRenderingType.Pinned && + this.props.context.timelineRenderingType !== TimelineRenderingType.Search ); } @@ -306,3 +307,7 @@ export default class MVideoBody extends React.PureComponent ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index 096824e4b23..10fb5c810cd 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -14,16 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import InlineSpinner from "../elements/InlineSpinner"; import { _t } from "../../../languageHandler"; import RecordingPlayback from "../audio_messages/RecordingPlayback"; -import MAudioBody from "./MAudioBody"; +import { MAudioBody } from "./MAudioBody"; import MFileBody from "./MFileBody"; import MediaProcessingError from "./shared/MediaProcessingError"; +import { IBodyProps } from "./IBodyProps"; +import RoomContext from "../../../contexts/RoomContext"; -export default class MVoiceMessageBody extends MAudioBody { +interface Props extends IBodyProps { + context: React.ContextType; +} + +class MVoiceMessageBody extends MAudioBody { // A voice message is an audio file but rendered in a special way. public render(): React.ReactNode { if (this.state.error) { @@ -51,3 +57,9 @@ export default class MVoiceMessageBody extends MAudioBody { ); } } + +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 9cf35922089..fa3df0ca03d 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactElement, useCallback, useContext, useEffect } from "react"; +import React, { forwardRef, ReactElement, useCallback, useContext, useEffect } from "react"; import { EventStatus, MatrixEvent, @@ -261,11 +261,10 @@ interface IMessageActionBarProps { toggleThreadExpanded: () => void; isQuoteExpanded?: boolean; getRelationsForEvent?: GetRelationsForEvent; + context: React.ContextType; } -export default class MessageActionBar extends React.PureComponent { - public static contextType = RoomContext; - +class MessageActionBar extends React.PureComponent { public componentDidMount(): void { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { this.props.mxEvent.on(MatrixEventEvent.Status, this.onSent); @@ -314,7 +313,7 @@ export default class MessageActionBar extends React.PureComponent>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index db0016de7b0..853fd7f9a9d 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -51,8 +51,8 @@ import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoStat // onMessageAllowed is handled internally interface IProps extends Omit { /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */ - overrideBodyTypes?: Record; - overrideEventTypes?: Record; + overrideBodyTypes?: Record; + overrideEventTypes?: Record; // helper function to access relations for this event getRelationsForEvent?: GetRelationsForEvent; @@ -64,7 +64,9 @@ export interface IOperableEventTile { getEventTileOps(): IEventTileOps | null; } -const baseBodyTypes = new Map([ +type ComponentType = React.ComponentType>; + +const baseBodyTypes = new Map([ [MsgType.Text, TextualBody], [MsgType.Notice, TextualBody], [MsgType.Emote, TextualBody], @@ -73,7 +75,7 @@ const baseBodyTypes = new Map([ [MsgType.Audio, MVoiceOrAudioBody], [MsgType.Video, MVideoBody], ]); -const baseEvTypes = new Map>([ +const baseEvTypes = new Map([ [EventType.Sticker, MStickerBody], [M_POLL_START.name, MPollBody], [M_POLL_START.altName, MPollBody], @@ -84,10 +86,10 @@ const baseEvTypes = new Map>([ ]); export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { - private body: React.RefObject = createRef(); + private body: React.RefObject = createRef(); private mediaHelper?: MediaEventHelper; - private bodyTypes = new Map(baseBodyTypes.entries()); - private evTypes = new Map>(baseEvTypes.entries()); + private bodyTypes = new Map(baseBodyTypes.entries()); + private evTypes = new Map(baseEvTypes.entries()); public static contextType = MatrixClientContext; public context!: React.ContextType; @@ -121,12 +123,12 @@ export default class MessageEvent extends React.Component implements IMe } private updateComponentMaps(): void { - this.bodyTypes = new Map(baseBodyTypes.entries()); + this.bodyTypes = new Map(baseBodyTypes.entries()); for (const [bodyType, bodyComponent] of Object.entries(this.props.overrideBodyTypes ?? {})) { this.bodyTypes.set(bodyType, bodyComponent); } - this.evTypes = new Map>(baseEvTypes.entries()); + this.evTypes = new Map(baseEvTypes.entries()); for (const [evType, evComponent] of Object.entries(this.props.overrideEventTypes ?? {})) { this.evTypes.set(evType, evComponent); } @@ -156,7 +158,7 @@ export default class MessageEvent extends React.Component implements IMe const content = this.props.mxEvent.getContent(); const type = this.props.mxEvent.getType(); const msgtype = content.msgtype; - let BodyType: React.ComponentType = RedactedBody; + let BodyType: ComponentType = RedactedBody; if (!this.props.mxEvent.isRedacted()) { // only resolve BodyType if event is not redacted if (this.props.mxEvent.isDecryptionFailure()) { diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index e57326edd73..fcf748bd6a9 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { SyntheticEvent } from "react"; +import React, { forwardRef, SyntheticEvent } from "react"; import classNames from "classnames"; import { MatrixEvent, MatrixEventEvent, Relations, RelationsEvent } from "matrix-js-sdk/src/matrix"; import { uniqBy } from "lodash"; @@ -35,7 +35,7 @@ const MAX_ITEMS_WHEN_LIMITED = 8; export const REACTION_SHORTCODE_KEY = new UnstableValue("shortcode", "com.beeper.reaction.shortcode"); -const ReactButton: React.FC = ({ mxEvent, reactions }) => { +const ReactButton: React.FC> = ({ mxEvent, reactions }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); let contextMenu: JSX.Element | undefined; @@ -74,6 +74,7 @@ interface IProps { mxEvent: MatrixEvent; // The Relations model from the JS SDK for reactions to `mxEvent` reactions?: Relations | null | undefined; + context: React.ContextType; } interface IState { @@ -81,13 +82,9 @@ interface IState { showAll: boolean; } -export default class ReactionsRow extends React.PureComponent { - public static contextType = RoomContext; - public context!: React.ContextType; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); - this.context = context; +class ReactionsRow extends React.PureComponent { + public constructor(props: IProps) { + super(props); this.state = { myReactions: this.getMyReactions(), @@ -151,7 +148,7 @@ export default class ReactionsRow extends React.PureComponent { if (!reactions) { return null; } - const userId = this.context.room?.client.getUserId(); + const userId = this.props.context.room?.client.getUserId(); if (!userId) return null; const myReactions = reactions.getAnnotationsBySender()?.[userId]; if (!myReactions) { @@ -202,8 +199,8 @@ export default class ReactionsRow extends React.PureComponent { myReactionEvent={myReactionEvent} customReactionImagesEnabled={customReactionImagesEnabled} disabled={ - !this.context.canReact || - (myReactionEvent && !myReactionEvent.isRedacted() && !this.context.canSelfRedact) + !this.props.context.canReact || + (myReactionEvent && !myReactionEvent.isRedacted() && !this.props.context.canSelfRedact) } /> ); @@ -226,7 +223,7 @@ export default class ReactionsRow extends React.PureComponent { } let addReactionButton: JSX.Element | undefined; - if (this.context.canReact) { + if (this.props.context.canReact) { addReactionButton = ; } @@ -239,3 +236,7 @@ export default class ReactionsRow extends React.PureComponent { ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index f0b5c70f17d..69ee45c91b2 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, SyntheticEvent, MouseEvent } from "react"; +import React, { createRef, SyntheticEvent, MouseEvent, forwardRef } from "react"; import ReactDOM from "react-dom"; import { MsgType } from "matrix-js-sdk/src/matrix"; @@ -51,6 +51,10 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; const MAX_HIGHLIGHT_LENGTH = 4096; +interface Props extends IBodyProps { + context: React.ContextType; +} + interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. links: string[]; @@ -59,17 +63,14 @@ interface IState { widgetHidden: boolean; } -export default class TextualBody extends React.Component { +class TextualBody extends React.Component { private readonly contentRef = createRef(); private unmounted = false; private pills: Element[] = []; private tooltips: Element[] = []; - public static contextType = RoomContext; - public context!: React.ContextType; - - public constructor(props: IBodyProps) { + public constructor(props: Props) { super(props); this.state = { @@ -429,7 +430,7 @@ export default class TextualBody extends React.Component { dis.dispatch({ action: Action.ComposerInsert, userId: mxEvent.getSender(), - timelineRenderingType: this.context.timelineRenderingType, + timelineRenderingType: this.props.context.timelineRenderingType, }); }; @@ -659,3 +660,7 @@ export default class TextualBody extends React.Component { ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index ae94fd31f9e..b49f64a475f 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import RoomContext from "../../../contexts/RoomContext"; @@ -23,19 +23,22 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; interface IProps { mxEvent: MatrixEvent; + context: React.ContextType; } -export default class TextualEvent extends React.Component { - public static contextType = RoomContext; - +class TextualEvent extends React.Component { public render(): React.ReactNode { const text = TextForEvent.textForEvent( this.props.mxEvent, MatrixClientPeg.safeGet(), true, - this.context?.showHiddenEvents, + this.props.context?.showHiddenEvents, ); if (!text) return null; return
{text}
; } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index 7dfcd633070..6e21b5895c2 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { IEventRelation, MatrixEvent, @@ -59,6 +59,7 @@ interface IProps { timelineRenderingType?: TimelineRenderingType; showComposer?: boolean; composerRelation?: IEventRelation; + context: React.ContextType; } interface IState { @@ -75,12 +76,10 @@ interface IState { showReadReceipts?: boolean; } -export default class TimelineCard extends React.Component { - public static contextType = RoomContext; - +class TimelineCard extends React.Component { private dispatcherRef?: string; private layoutWatcherRef?: string; - private timelinePanel = React.createRef(); + private timelinePanel = React.createRef>(); private card = React.createRef(); private readReceiptsSettingWatcher: string | undefined; @@ -224,7 +223,7 @@ export default class TimelineCard extends React.Component { { manageReadMarkers={false} // No RM support in the TimelineCard sendReadReceiptOnLoad={true} timelineSet={this.props.timelineSet} - showUrlPreview={this.context.showUrlPreview} + showUrlPreview={this.props.context.showUrlPreview} // The right panel timeline (and therefore threads) don't support IRC layout at this time layout={this.state.layout === Layout.Bubble ? Layout.Bubble : Layout.Group} hideThreadedMessages={false} @@ -281,3 +280,7 @@ export default class TimelineCard extends React.Component { ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 8eedf0867ea..1dde894624c 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent } from "react"; +import React, { createRef, forwardRef, KeyboardEvent } from "react"; import classNames from "classnames"; import { flatMap } from "lodash"; import { Room } from "matrix-js-sdk/src/matrix"; @@ -38,6 +38,7 @@ interface IProps { selection: ISelectionRange; // The room in which we're autocompleting room: Room; + context: React.ContextType; } interface IState { @@ -49,14 +50,12 @@ interface IState { forceComplete: boolean; } -export default class Autocomplete extends React.PureComponent { +export class Autocomplete extends React.PureComponent { public autocompleter?: Autocompleter; public queryRequested?: string; public debounceCompletionsRequest?: number; private containerRef = createRef(); - public static contextType = RoomContext; - public constructor(props: IProps) { super(props); @@ -80,7 +79,7 @@ export default class Autocomplete extends React.PureComponent { } public componentDidMount(): void { - this.autocompleter = new Autocompleter(this.props.room, this.context.timelineRenderingType); + this.autocompleter = new Autocompleter(this.props.room, this.props.context.timelineRenderingType); this.applyNewProps(); } @@ -320,3 +319,7 @@ export default class Autocomplete extends React.PureComponent { ) : null; } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 061dfe27039..50933ad82d9 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -123,7 +123,7 @@ interface IState { export default class BasicMessageEditor extends React.Component { public readonly editorRef = createRef(); - private autocompleteRef = createRef(); + private autocompleteRef = createRef>(); private formatBarRef = createRef(); private modifiedFlag = false; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index fabca13a1c7..1f13aa48e1d 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent } from "react"; +import React, { createRef, forwardRef, KeyboardEvent } from "react"; import classNames from "classnames"; import { EventStatus, MatrixEvent, Room, MsgType } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -122,28 +122,25 @@ export function createEditContent( interface IEditMessageComposerProps extends MatrixClientProps { editState: EditorStateTransfer; className?: string; + context: React.ContextType; } interface IState { saveDisabled: boolean; } class EditMessageComposer extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - private readonly editorRef = createRef(); private readonly dispatcherRef: string; private readonly replyToEvent?: MatrixEvent; private model!: EditorModel; - public constructor(props: IEditMessageComposerProps, context: React.ContextType) { + public constructor(props: IEditMessageComposerProps) { super(props); - this.context = context; // otherwise React will only set it prior to render due to type def above const isRestored = this.createEditorModel(); const ev = this.props.editState.getEvent(); - this.replyToEvent = ev.replyEventId ? this.context.room?.findEventById(ev.replyEventId) : undefined; + this.replyToEvent = ev.replyEventId ? this.props.context.room?.findEventById(ev.replyEventId) : undefined; const editContent = createEditContent(this.model, ev, this.replyToEvent); this.state = { @@ -155,10 +152,10 @@ class EditMessageComposer extends React.Component { @@ -191,7 +188,7 @@ class EditMessageComposer extends React.Component>( + (props, ref) => ( + + {(context) => } + + ), +); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 108e0d9d938..32ff6c2b4c0 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -266,6 +266,10 @@ interface IState { threadNotification?: NotificationCountType; } +interface Props extends EventTileProps { + context: React.ContextType; +} + /** * When true, the tile qualifies for some sort of special read receipt. * This could be a 'sending' or 'sent' receipt, for example. @@ -282,7 +286,7 @@ export function isEligibleForSpecialReceipt(event: MatrixEvent): boolean { } // MUST be rendered within a RoomContext with a set timelineRenderingType -export class UnwrappedEventTile extends React.Component { +class UnwrappedEventTile extends React.Component { private suppressReadReceiptAnimation: boolean; private isListeningForReceipts: boolean; private tile = createRef(); @@ -297,13 +301,10 @@ export class UnwrappedEventTile extends React.Component layout: Layout.Group, }; - public static contextType = RoomContext; - public context!: React.ContextType; - private unmounted = false; - public constructor(props: EventTileProps, context: React.ContextType) { - super(props, context); + public constructor(props: Props) { + super(props); const thread = this.thread; @@ -508,7 +509,10 @@ export class UnwrappedEventTile extends React.Component ); } - if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) { + if ( + this.props.context.timelineRenderingType === TimelineRenderingType.Search && + this.props.mxEvent.threadRootId + ) { if (this.props.highlightLink) { return ( @@ -676,8 +680,8 @@ export class UnwrappedEventTile extends React.Component */ private shouldHighlight(): boolean { if (this.props.forExport) return false; - if (this.context.timelineRenderingType === TimelineRenderingType.Notification) return false; - if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) return false; + if (this.props.context.timelineRenderingType === TimelineRenderingType.Notification) return false; + if (this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList) return false; const cli = MatrixClientPeg.safeGet(); const actions = cli.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); @@ -701,7 +705,7 @@ export class UnwrappedEventTile extends React.Component dis.dispatch({ action: Action.ComposerInsert, userId: this.props.mxEvent.getSender()!, - timelineRenderingType: this.context.timelineRenderingType, + timelineRenderingType: this.props.context.timelineRenderingType, }); }; @@ -715,7 +719,7 @@ export class UnwrappedEventTile extends React.Component highlighted: true, room_id: this.props.mxEvent.getRoomId(), metricsTrigger: - this.context.timelineRenderingType === TimelineRenderingType.Search ? "MessageSearch" : undefined, + this.props.context.timelineRenderingType === TimelineRenderingType.Search ? "MessageSearch" : undefined, }); }; @@ -923,7 +927,7 @@ export class UnwrappedEventTile extends React.Component } = getEventDisplayInfo( MatrixClientPeg.safeGet(), this.props.mxEvent, - this.context.showHiddenEvents, + this.props.context.showHiddenEvents, this.shouldHideEvent(), ); const { isQuoteExpanded } = this.state; @@ -958,15 +962,15 @@ export class UnwrappedEventTile extends React.Component let isContinuation = this.props.continuation; if ( - this.context.timelineRenderingType !== TimelineRenderingType.Room && - this.context.timelineRenderingType !== TimelineRenderingType.Search && - this.context.timelineRenderingType !== TimelineRenderingType.Thread && + this.props.context.timelineRenderingType !== TimelineRenderingType.Room && + this.props.context.timelineRenderingType !== TimelineRenderingType.Search && + this.props.context.timelineRenderingType !== TimelineRenderingType.Thread && this.props.layout !== Layout.Bubble ) { isContinuation = false; } - const isRenderingNotification = this.context.timelineRenderingType === TimelineRenderingType.Notification; + const isRenderingNotification = this.props.context.timelineRenderingType === TimelineRenderingType.Notification; const isEditing = !!this.props.editState; const classes = classNames({ @@ -990,7 +994,8 @@ export class UnwrappedEventTile extends React.Component mx_EventTile_emote: msgtype === MsgType.Emote, mx_EventTile_noSender: this.props.hideSender, mx_EventTile_clamp: - this.context.timelineRenderingType === TimelineRenderingType.ThreadsList || isRenderingNotification, + this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList || + isRenderingNotification, mx_EventTile_noBubble: noBubbleEvent, }); @@ -1020,8 +1025,8 @@ export class UnwrappedEventTile extends React.Component avatarSize = "14px"; needsSenderProfile = false; } else if ( - this.context.timelineRenderingType === TimelineRenderingType.ThreadsList || - (this.context.timelineRenderingType === TimelineRenderingType.Thread && !this.props.continuation) + this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList || + (this.props.context.timelineRenderingType === TimelineRenderingType.Thread && !this.props.continuation) ) { avatarSize = "32px"; needsSenderProfile = true; @@ -1032,7 +1037,7 @@ export class UnwrappedEventTile extends React.Component avatarSize = "14px"; needsSenderProfile = true; } else if ( - (this.props.continuation && this.context.timelineRenderingType !== TimelineRenderingType.File) || + (this.props.continuation && this.props.context.timelineRenderingType !== TimelineRenderingType.File) || eventType === EventType.CallInvite || ElementCall.CALL_EVENT_TYPE.matches(eventType) ) { @@ -1058,7 +1063,7 @@ export class UnwrappedEventTile extends React.Component const viewUserOnClick = !this.props.inhibitInteraction && ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes( - this.context.timelineRenderingType, + this.props.context.timelineRenderingType, ); avatar = (
@@ -1074,13 +1079,13 @@ export class UnwrappedEventTile extends React.Component if (needsSenderProfile && this.props.hideSender !== true) { if ( - this.context.timelineRenderingType === TimelineRenderingType.Room || - this.context.timelineRenderingType === TimelineRenderingType.Search || - this.context.timelineRenderingType === TimelineRenderingType.Pinned || - this.context.timelineRenderingType === TimelineRenderingType.Thread + this.props.context.timelineRenderingType === TimelineRenderingType.Room || + this.props.context.timelineRenderingType === TimelineRenderingType.Search || + this.props.context.timelineRenderingType === TimelineRenderingType.Pinned || + this.props.context.timelineRenderingType === TimelineRenderingType.Thread ) { sender = ; - } else if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { + } else if (this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { sender = ; } else { sender = ; @@ -1113,7 +1118,7 @@ export class UnwrappedEventTile extends React.Component // Thread panel shows the timestamp of the last reply in that thread let ts = - this.context.timelineRenderingType !== TimelineRenderingType.ThreadsList + this.props.context.timelineRenderingType !== TimelineRenderingType.ThreadsList ? this.props.mxEvent.getTs() : this.state.thread?.replyToEvent?.getTs(); if (typeof ts !== "number") { @@ -1123,7 +1128,7 @@ export class UnwrappedEventTile extends React.Component const messageTimestamp = ( let replyChain: JSX.Element | undefined; if ( - haveRendererForEvent(this.props.mxEvent, MatrixClientPeg.safeGet(), this.context.showHiddenEvents) && + haveRendererForEvent(this.props.mxEvent, MatrixClientPeg.safeGet(), this.props.context.showHiddenEvents) && shouldDisplayReply(this.props.mxEvent) ) { replyChain = ( @@ -1202,7 +1207,7 @@ export class UnwrappedEventTile extends React.Component // Use `getSender()` because searched events might not have a proper `sender`. const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.safeGet().getUserId(); - switch (this.context.timelineRenderingType) { + switch (this.props.context.timelineRenderingType) { case TimelineRenderingType.Thread: { return React.createElement( this.props.as || "li", @@ -1242,7 +1247,7 @@ export class UnwrappedEventTile extends React.Component onHeightChanged: () => this.props.onHeightChanged, permalinkCreator: this.props.permalinkCreator!, }, - this.context.showHiddenEvents, + this.props.context.showHiddenEvents, )} {actionBar} @@ -1268,7 +1273,7 @@ export class UnwrappedEventTile extends React.Component "aria-atomic": "true", "data-scroll-tokens": scrollToken, "data-layout": this.props.layout, - "data-shape": this.context.timelineRenderingType, + "data-shape": this.props.context.timelineRenderingType, "data-self": isOwnEvent, "data-has-reply": !!replyChain, "onMouseEnter": () => this.setState({ hover: true }), @@ -1277,7 +1282,7 @@ export class UnwrappedEventTile extends React.Component const target = ev.currentTarget as HTMLElement; let index = -1; if (target.parentElement) index = Array.from(target.parentElement.children).indexOf(target); - switch (this.context.timelineRenderingType) { + switch (this.props.context.timelineRenderingType) { case TimelineRenderingType.Notification: this.viewInRoom(ev); break; @@ -1333,7 +1338,7 @@ export class UnwrappedEventTile extends React.Component
{this.renderThreadPanelSummary()} - {this.context.timelineRenderingType === TimelineRenderingType.ThreadsList && ( + {this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList && ( onHeightChanged: this.props.onHeightChanged, permalinkCreator: this.props.permalinkCreator, }, - this.context.showHiddenEvents, + this.props.context.showHiddenEvents, )} , {groupPadlock} {replyChain} {renderTile( - this.context.timelineRenderingType, + this.props.context.timelineRenderingType, { ...this.props, @@ -1434,7 +1439,7 @@ export class UnwrappedEventTile extends React.Component onHeightChanged: this.props.onHeightChanged, permalinkCreator: this.props.permalinkCreator, }, - this.context.showHiddenEvents, + this.props.context.showHiddenEvents, )} {actionBar} {this.props.layout === Layout.IRC && ( @@ -1463,7 +1468,9 @@ const SafeEventTile = forwardRef((props, ref return ( <> - + + {(context) => } + ); diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx index 5fab692046d..fd9458fe774 100644 --- a/src/components/views/rooms/LegacyRoomHeader.tsx +++ b/src/components/views/rooms/LegacyRoomHeader.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { FC, useState, useMemo, useCallback } from "react"; +import React, { FC, useState, useMemo, useCallback, forwardRef } from "react"; import classNames from "classnames"; import { throttle } from "lodash"; import { RoomStateEvent, ISearchResults } from "matrix-js-sdk/src/matrix"; @@ -470,7 +470,7 @@ export interface ISearchInfo { count?: number; } -export interface IProps { +interface IProps { room: Room; oobData?: IOOBData; inRoom: boolean; @@ -478,7 +478,7 @@ export interface IProps { onInviteClick: (() => void) | null; onForgetClick: (() => void) | null; onAppsClick: (() => void) | null; - e2eStatus: E2EStatus; + e2eStatus?: E2EStatus; appsShown: boolean; searchInfo?: ISearchInfo; excludedRightPanelPhaseButtons?: Array; @@ -487,6 +487,7 @@ export interface IProps { viewingCall: boolean; activeCall: Call | null; additionalButtons?: ViewRoomOpts["buttons"]; + context: React.ContextType; } interface IState { @@ -498,7 +499,7 @@ interface IState { /** * @deprecated use `src/components/views/rooms/RoomHeader.tsx` instead */ -export default class RoomHeader extends React.Component { +class LegacyRoomHeader extends React.Component { public static defaultProps: Partial = { inRoom: false, excludedRightPanelPhaseButtons: [], @@ -506,13 +507,11 @@ export default class RoomHeader extends React.Component { enableRoomOptionsMenu: true, }; - public static contextType = RoomContext; - public context!: React.ContextType; private readonly client = this.props.room.client; private readonly featureAskToJoinWatcher: string; - public constructor(props: IProps, context: IState) { - super(props, context); + public constructor(props: IProps) { + super(props); const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room); notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate); this.state = { @@ -590,7 +589,7 @@ export default class RoomHeader extends React.Component { private renderButtons(isVideoRoom: boolean): React.ReactNode { const startButtons: JSX.Element[] = []; - if (!this.props.viewingCall && this.props.inRoom && !this.context.tombstone) { + if (!this.props.viewingCall && this.props.inRoom && !this.props.context.tombstone) { startButtons.push(); } @@ -866,3 +865,9 @@ export default class RoomHeader extends React.Component { ); } } + +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index f9aae433feb..4911a216a86 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, ReactNode } from "react"; +import React, { createRef, forwardRef, ReactNode } from "react"; import classNames from "classnames"; import { IEventRelation, @@ -44,7 +44,7 @@ import { RecordingState } from "../../../audio/VoiceRecording"; import Tooltip, { Alignment } from "../elements/Tooltip"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import { E2EStatus } from "../../../utils/ShieldUtils"; -import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer"; +import SendMessageComposer from "./SendMessageComposer"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; import EditorModel from "../../../editor/model"; @@ -92,6 +92,7 @@ interface IProps extends MatrixClientProps { relation?: IEventRelation; e2eStatus?: E2EStatus; compact?: boolean; + context: React.ContextType; } interface IState { @@ -113,16 +114,13 @@ interface IState { export class MessageComposer extends React.Component { private tooltipId = `mx_MessageComposer_${Math.random()}`; private dispatcherRef?: string; - private messageComposerInput = createRef(); - private voiceRecordingButton = createRef(); + private messageComposerInput = createRef>(); + private voiceRecordingButton = createRef>(); private ref: React.RefObject = createRef(); private instanceId: number; private _voiceRecording: Optional; - public static contextType = RoomContext; - public context!: React.ContextType; - public static defaultProps = { compact: false, showVoiceBroadcastButton: false, @@ -189,7 +187,7 @@ export class MessageComposer extends React.Component { private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry): void => { if (type === UI_EVENTS.Resize) { - const { narrow } = this.context; + const { narrow } = this.props.context; this.setState({ isMenuOpen: !narrow ? false : this.state.isMenuOpen, isStickerPickerOpen: false, @@ -200,7 +198,7 @@ export class MessageComposer extends React.Component { private onAction = (payload: ActionPayload): void => { switch (payload.action) { case "reply_to_event": - if (payload.context === this.context.timelineRenderingType) { + if (payload.context === this.props.context.timelineRenderingType) { // add a timeout for the reply preview to be rendered, so // that the ScrollPanel listening to the resizeNotifier can // correctly measure it's new height and scroll down to keep @@ -274,7 +272,7 @@ export class MessageComposer extends React.Component { private onTombstoneClick = (ev: ButtonEvent): void => { ev.preventDefault(); - const replacementRoomId = this.context.tombstone?.getContent()["replacement_room"]; + const replacementRoomId = this.props.context.tombstone?.getContent()["replacement_room"]; const replacementRoom = MatrixClientPeg.safeGet().getRoom(replacementRoomId); let createEventId: string | undefined; if (replacementRoom) { @@ -282,7 +280,7 @@ export class MessageComposer extends React.Component { if (createEvent?.getId()) createEventId = createEvent.getId(); } - const sender = this.context.tombstone?.getSender(); + const sender = this.props.context.tombstone?.getSender(); const viaServers = sender ? [sender.split(":").slice(1).join(":")] : undefined; dis.dispatch({ @@ -324,7 +322,7 @@ export class MessageComposer extends React.Component { dis.dispatch({ action: Action.ComposerInsert, text: emoji, - timelineRenderingType: this.context.timelineRenderingType, + timelineRenderingType: this.props.context.timelineRenderingType, }); return true; }; @@ -345,11 +343,11 @@ export class MessageComposer extends React.Component { this.setState({ composerContent: "", initialComposerContent: "" }); dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer, - timelineRenderingType: this.context.timelineRenderingType, + timelineRenderingType: this.props.context.timelineRenderingType, }); await sendMessage(composerContent, this.state.isRichTextEnabled, { mxClient: this.props.mxClient, - roomContext: this.context, + roomContext: this.props.context, permalinkCreator, relation, replyToEvent, @@ -467,7 +465,7 @@ export class MessageComposer extends React.Component { this.voiceRecordingButton.current?.onRecordStartEndClick(); } - if (this.context.narrow) { + if (this.props.context.narrow) { this.toggleButtonMenu(); } }; @@ -483,7 +481,7 @@ export class MessageComposer extends React.Component { const controls: ReactNode[] = []; const menuPosition = this.getMenuPosition(); - const canSendMessages = this.context.canSendMessages && !this.context.tombstone; + const canSendMessages = this.props.context.canSendMessages && !this.props.context.tombstone; let composer: ReactNode; if (canSendMessages) { if (this.state.isWysiwygLabEnabled && menuPosition) { @@ -528,8 +526,8 @@ export class MessageComposer extends React.Component { replyToEvent={this.props.replyToEvent} />, ); - } else if (this.context.tombstone) { - const replacementRoomId = this.context.tombstone.getContent()["replacement_room"]; + } else if (this.props.context.tombstone) { + const replacementRoomId = this.props.context.tombstone.getContent()["replacement_room"]; const continuesLink = replacementRoomId ? ( { } const MessageComposerWithMatrixClient = withMatrixClientHOC(MessageComposer); -export default MessageComposerWithMatrixClient; +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx index 50c625c4286..20b085755dc 100644 --- a/src/components/views/rooms/ReplyPreview.tsx +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import dis from "../../../dispatcher/dispatcher"; @@ -35,11 +35,10 @@ function cancelQuoting(context: TimelineRenderingType): void { interface IProps { permalinkCreator?: RoomPermalinkCreator; replyToEvent?: MatrixEvent; + context: React.ContextType; } -export default class ReplyPreview extends React.Component { - public static contextType = RoomContext; - +class ReplyPreview extends React.Component { public render(): JSX.Element | null { if (!this.props.replyToEvent) return null; @@ -50,7 +49,7 @@ export default class ReplyPreview extends React.Component { {_t("composer|replying_title")} cancelQuoting(this.context.timelineRenderingType)} + onClick={() => cancelQuoting(this.props.context.timelineRenderingType)} /> @@ -59,3 +58,7 @@ export default class ReplyPreview extends React.Component { ); } } + +export default forwardRef>((props, ref) => ( + {(context) => } +)); diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 56ca6ade609..986bb8d09cd 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -31,7 +31,7 @@ import MFileBody from "../messages/MFileBody"; import MemberAvatar from "../avatars/MemberAvatar"; import MVoiceMessageBody from "../messages/MVoiceMessageBody"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { renderReplyTile } from "../../../events/EventTileFactory"; +import { EventTileTypeProps, renderReplyTile } from "../../../events/EventTileFactory"; import { GetRelationsForEvent } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -147,13 +147,13 @@ export default class ReplyTile extends React.PureComponent { ); } - const msgtypeOverrides: Record = { + const msgtypeOverrides: Record> = { [MsgType.Image]: MImageReplyBody, // Override audio and video body with file body. We also hide the download/decrypt button using CSS [MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody, [MsgType.Video]: MFileBody, }; - const evOverrides: Record = { + const evOverrides: Record> = { // Use MImageReplyBody so that the sticker isn't taking up a lot of space [EventType.Sticker]: MImageReplyBody, }; diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 99b5f0805c3..2efca1731b8 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; @@ -40,17 +40,15 @@ interface IProps { ourEventsIndexes: number[]; onHeightChanged?: () => void; permalinkCreator?: RoomPermalinkCreator; + context: React.ContextType; } -export default class SearchResultTile extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - +class SearchResultTile extends React.Component { // A map of private callEventGroupers = new Map(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.buildLegacyCallEventGroupers(this.props.timeline); } @@ -79,7 +77,7 @@ export default class SearchResultTile extends React.Component { highlights = this.props.searchHighlights; } - if (haveRendererForEvent(mxEv, cli, this.context?.showHiddenEvents)) { + if (haveRendererForEvent(mxEv, cli, this.props.context?.showHiddenEvents)) { // do we need a date separator since the last event? const prevEv = timeline[j - 1]; // is this a continuation of the previous message? @@ -90,7 +88,7 @@ export default class SearchResultTile extends React.Component { prevEv, mxEv, cli, - this.context?.showHiddenEvents, + this.props.context?.showHiddenEvents, TimelineRenderingType.Search, ); @@ -108,7 +106,7 @@ export default class SearchResultTile extends React.Component { mxEv, nextEv, cli, - this.context?.showHiddenEvents, + this.props.context?.showHiddenEvents, TimelineRenderingType.Search, ); } @@ -140,3 +138,9 @@ export default class SearchResultTile extends React.Component { ); } } + +export default forwardRef>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0ea0bdf94c1..113b5ef2b79 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent, SyntheticEvent } from "react"; +import React, { createRef, forwardRef, KeyboardEvent, SyntheticEvent } from "react"; import EMOJI_REGEX from "emojibase-regex"; import { IContent, @@ -250,12 +250,10 @@ interface ISendMessageComposerProps extends MatrixClientProps { onChange?(model: EditorModel): void; includeReplyLegacyFallback?: boolean; toggleStickerPickerOpen: () => void; + context: React.ContextType; } -export class SendMessageComposer extends React.Component { - public static contextType = RoomContext; - public context!: React.ContextType; - +class SendMessageComposer extends React.Component { private readonly prepareToEncrypt?: DebouncedFunc<() => void>; private readonly editorRef = createRef(); private model: EditorModel; @@ -267,9 +265,8 @@ export class SendMessageComposer extends React.Component) { - super(props, context); - this.context = context; // otherwise React will only set it prior to render due to type def above + public constructor(props: ISendMessageComposerProps) { + super(props); if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) { this.prepareToEncrypt = throttle( @@ -336,7 +333,7 @@ export class SendMessageComposer extends React.Component { @@ -789,4 +786,10 @@ export class SendMessageComposer extends React.Component>( + (props, ref) => ( + + {(context) => } + + ), +); diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 70cabb474c9..4c8e8385131 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; +import React, { forwardRef, ReactNode } from "react"; import { Room, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; @@ -50,6 +50,7 @@ interface IProps { permalinkCreator?: RoomPermalinkCreator; relation?: IEventRelation; replyToEvent?: MatrixEvent; + context: React.ContextType; } interface IState { @@ -61,9 +62,7 @@ interface IState { /** * Container tile for rendering the voice message recorder in the composer. */ -export default class VoiceRecordComposerTile extends React.PureComponent { - public static contextType = RoomContext; - public context!: React.ContextType; +class VoiceRecordComposerTile extends React.PureComponent { private voiceRecordingId: string; public constructor(props: IProps) { @@ -141,7 +140,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent>((props, ref) => ( + + {(context) => } + +)); diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 1e1cb0eb60d..7d4bcec7129 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -59,7 +59,7 @@ interface WysiwygAutocompleteProps { const WysiwygAutocomplete = forwardRef( ( { suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps, - ref: ForwardedRef, + ref: ForwardedRef>, ): JSX.Element | null => { const { room } = useRoomContext(); const client = useMatrixClientContext(); diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index ba0dad529b8..d22e8e270d0 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -58,7 +58,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({ eventRelation, }: WysiwygComposerProps) { const { room } = useRoomContext(); - const autocompleteRef = useRef(null); + const autocompleteRef = useRef | null>(null); const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation); const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({ diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index a9cfa2966e1..aec01df7bd0 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -37,7 +37,7 @@ import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsCli export function useInputEventProcessor( onSend: () => void, - autocompleteRef: React.RefObject, + autocompleteRef: React.RefObject>, initialContent?: string, eventRelation?: IEventRelation, ): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null { @@ -105,7 +105,7 @@ function handleKeyboardEvent( roomContext: IRoomState, composerContext: ComposerContextState, mxClient: MatrixClient | undefined, - autocompleteRef: React.RefObject, + autocompleteRef: React.RefObject>, ): KeyboardEvent | null { const { editorStateTransfer } = composerContext; const isEditing = Boolean(editorStateTransfer); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 6121f0c877f..22eb0a358fe 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -55,7 +55,7 @@ export function usePlainTextListeners( eventRelation?: IEventRelation, ): { ref: RefObject; - autocompleteRef: React.RefObject; + autocompleteRef: React.RefObject>; content?: string; onBeforeInput(event: SyntheticEvent): void; onInput(event: SyntheticEvent): void; @@ -72,7 +72,7 @@ export function usePlainTextListeners( const mxClient = useMatrixClientContext(); const ref = useRef(null); - const autocompleteRef = useRef(null); + const autocompleteRef = useRef | null>(null); const [content, setContent] = useState(initialContent); const send = useCallback(() => { diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index f95405c3bfd..8d4500314fe 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -70,7 +70,7 @@ export function setCursorPositionAtTheEnd(element: HTMLElement): void { * @returns boolean - whether or not the autocomplete has handled the event */ export function handleEventWithAutocomplete( - autocompleteRef: RefObject, + autocompleteRef: RefObject>, // we get a React Keyboard event from plain text composer, a Keyboard Event from the rich text composer event: KeyboardEvent | React.KeyboardEvent, ): boolean { diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 3aed32d91fd..6e92196faf3 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -28,7 +28,7 @@ export interface ICallback { } export type UpdateCallback = (data: ICallback) => void; -export type GetAutocompleterComponent = () => Autocomplete | null; +export type GetAutocompleterComponent = () => React.ComponentRef | null; export type UpdateQuery = (test: string) => Promise; export default class AutocompleteWrapperModel { diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 88ebb0a5600..d5c427a0e98 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -56,6 +56,7 @@ import { shouldDisplayAsVoiceBroadcastStoppedText, VoiceBroadcastChunkEventType, } from "../voice-broadcast"; +import { IBodyProps } from "../components/views/messages/IBodyProps"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -78,11 +79,11 @@ export interface EventTileTypeProps ref?: React.RefObject; // `any` because it's effectively impossible to convince TS of a reasonable type timestamp?: JSX.Element; maxImageHeight?: number; // pixels - overrideBodyTypes?: Record; - overrideEventTypes?: Record; + overrideBodyTypes?: Record>; + overrideEventTypes?: Record>; } -type FactoryProps = Omit; +type FactoryProps = EventTileTypeProps; type Factory = (ref: Optional>, props: X) => JSX.Element; export const MessageEventFactory: Factory = (ref, props) => ; diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index 0396ea68a44..9992c772e87 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -40,7 +40,7 @@ import React, { createRef } from "react"; import { Mocked, mocked } from "jest-mock"; import { forEachRight } from "lodash"; -import TimelinePanel from "../../../src/components/structures/TimelinePanel"; +import TimelinePanel, { TimelinePanel as TimelinePanelClass } from "../../../src/components/structures/TimelinePanel"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { isCallEvent } from "../../../src/components/structures/LegacyCallEventGrouper"; @@ -77,7 +77,7 @@ const mkTimeline = (room: Room, events: MatrixEvent[]): [EventTimeline, EventTim return [timeline, timelineSet]; }; -const getProps = (room: Room, events: MatrixEvent[]): TimelinePanel["props"] => { +const getProps = (room: Room, events: MatrixEvent[]): React.ComponentProps => { const [, timelineSet] = mkTimeline(room, events); return { @@ -184,7 +184,7 @@ describe("TimelinePanel", () => { const roomId = "#room:example.com"; let room: Room; let timelineSet: EventTimelineSet; - let timelinePanel: TimelinePanel; + let timelinePanel: React.ComponentRef; const ev1 = new MatrixEvent({ event_id: "ev1", @@ -203,7 +203,7 @@ describe("TimelinePanel", () => { }); const renderTimelinePanel = async (): Promise => { - const ref = createRef(); + const ref = createRef>(); render( { }); afterEach(() => { - TimelinePanel.roomReadMarkerTsMap = {}; + TimelinePanelClass.roomReadMarkerTsMap = {}; }); it("when there is no event, it should not send any receipt", async () => { diff --git a/test/components/views/rooms/LegacyRoomHeader-test.tsx b/test/components/views/rooms/LegacyRoomHeader-test.tsx index c01d749eeab..dd648c9f5f0 100644 --- a/test/components/views/rooms/LegacyRoomHeader-test.tsx +++ b/test/components/views/rooms/LegacyRoomHeader-test.tsx @@ -50,7 +50,7 @@ import { } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; -import RoomHeader, { IProps as RoomHeaderProps } from "../../../../src/components/views/rooms/LegacyRoomHeader"; +import RoomHeader from "../../../../src/components/views/rooms/LegacyRoomHeader"; import { SearchScope } from "../../../../src/components/views/rooms/SearchBar"; import { E2EStatus } from "../../../../src/utils/ShieldUtils"; import { IRoomState } from "../../../../src/components/structures/RoomView"; @@ -201,7 +201,10 @@ describe("LegacyRoomHeader", () => { WidgetMessagingStore.instance.stopMessaging(widget, call.roomId); }; - const renderHeader = (props: Partial = {}, roomContext: Partial = {}) => { + const renderHeader = ( + props: Partial> = {}, + roomContext: Partial = {}, + ) => { render( ): RenderResult { - const props: RoomHeaderProps = { + const props: React.ComponentProps = { room, inRoom: true, onSearchClick: () => {}, diff --git a/test/components/views/rooms/VoiceRecordComposerTile-test.tsx b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx index 72456f3c190..e5ac821b6c4 100644 --- a/test/components/views/rooms/VoiceRecordComposerTile-test.tsx +++ b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx @@ -43,7 +43,7 @@ jest.mock("../../../../src/stores/VoiceRecordingStore", () => ({ })); describe("", () => { - let voiceRecordComposerTile: RefObject; + let voiceRecordComposerTile: RefObject>; let mockRecorder: VoiceMessageRecording; let mockUpload: IUpload; let mockClient: MatrixClient; diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 0b20cc61b21..310b0d547c5 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -60,7 +60,7 @@ describe("WysiwygAutocomplete", () => { jest.restoreAllMocks(); }); - const autocompleteRef = createRef(); + const autocompleteRef = createRef>(); const getCompletionsSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockResolvedValue([ { completions: mockCompletion, From ffd8b7f975a0f4eb0dee2bbedba5460ab7b8d4ec Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 15:48:04 +0100 Subject: [PATCH 5/8] Remove legacy consumers of the MatrixClientContext in favour of HOCs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/EmbeddedPage.tsx | 15 +++--- src/components/structures/RightPanel.tsx | 21 ++++---- src/components/structures/RoomStatusBar.tsx | 48 +++++++++++-------- src/components/structures/SpaceRoomView.tsx | 21 ++++---- src/components/structures/UserView.tsx | 13 +++-- .../views/dialogs/CreateRoomDialog.tsx | 6 +-- .../views/dialogs/CreateSubspaceDialog.tsx | 2 +- src/components/views/elements/AppTile.tsx | 34 +++++++------ .../views/elements/PersistentApp.tsx | 20 ++++---- .../views/elements/RoomAliasField.tsx | 19 ++++---- .../views/location/LocationPicker.tsx | 14 +++--- .../views/location/LocationShareMenu.tsx | 2 +- .../views/messages/EditHistoryMessage.tsx | 20 ++++---- .../views/messages/MLocationBody.tsx | 23 ++++----- src/components/views/messages/MPollBody.tsx | 31 +++++++----- .../views/messages/MessageEvent.tsx | 8 +--- .../views/messages/ReactionsRowButton.tsx | 17 ++++--- .../messages/ReactionsRowButtonTooltip.tsx | 14 +++--- .../views/room_settings/AliasSettings.tsx | 35 +++++++------- .../views/rooms/PinnedEventTile.tsx | 4 -- src/components/views/rooms/RoomList.tsx | 4 -- .../views/rooms/RoomUpgradeWarningBar.tsx | 19 ++++---- .../settings/tabs/room/BridgeSettingsTab.tsx | 13 +++-- .../tabs/room/GeneralRoomSettingsTab.tsx | 19 ++++---- .../tabs/room/NotificationSettingsTab.tsx | 17 ++++--- .../tabs/room/RolesRoomSettingsTab.tsx | 31 ++++++------ .../tabs/room/SecurityRoomSettingsTab.tsx | 37 +++++++------- .../tabs/user/AppearanceUserSettingsTab.tsx | 15 +++--- .../tabs/user/HelpUserSettingsTab.tsx | 29 +++++------ .../tabs/user/VoiceUserSettingsTab.tsx | 11 ++--- .../views/spaces/SpaceCreateMenu.tsx | 6 +-- .../views/spaces/SpaceTreeLevel.tsx | 9 ++-- .../previews/PollStartEventPreview.ts | 4 -- src/stores/widgets/StopGapWidget.ts | 8 ++-- .../messages/ReactionsRowButton-test.tsx | 4 +- .../room/NotificationSettingsTab-test.tsx | 10 ++-- 36 files changed, 290 insertions(+), 313 deletions(-) diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index c0d7835747f..4d942baedd4 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -24,11 +24,11 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t, TranslationKey } from "../../languageHandler"; import dis from "../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { ActionPayload } from "../../dispatcher/payloads"; -interface IProps { +interface IProps extends MatrixClientProps { // URL to request embedded page content from url?: string; // Class name prefix to apply for a given instance @@ -43,13 +43,12 @@ interface IState { page: string; } -export default class EmbeddedPage extends React.PureComponent { - public static contextType = MatrixClientContext; +class EmbeddedPage extends React.PureComponent { private unmounted = false; private dispatcherRef: string | null = null; - public constructor(props: IProps, context: typeof MatrixClientContext) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { page: "", @@ -120,7 +119,7 @@ export default class EmbeddedPage extends React.PureComponent { public render(): React.ReactNode { // HACK: Workaround for the context's MatrixClient not updating. - const client = this.context || MatrixClientPeg.get(); + const client = this.props.mxClient || MatrixClientPeg.get(); const isGuest = client ? client.isGuest() : true; const className = this.props.className; const classes = classnames(className, { @@ -137,3 +136,5 @@ export default class EmbeddedPage extends React.PureComponent { } } } + +export default withMatrixClientHOC(EmbeddedPage); diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 68d600ec1de..1d80b388c3c 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -22,7 +22,7 @@ import { throttle } from "lodash"; import dis from "../../dispatcher/dispatcher"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import RightPanelStore from "../../stores/right-panel/RightPanelStore"; -import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; import SettingsStore from "../../settings/SettingsStore"; @@ -43,7 +43,7 @@ import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/ import { Action } from "../../dispatcher/actions"; import { XOR } from "../../@types/common"; -interface BaseProps { +interface BaseProps extends MatrixClientProps { overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView) resizeNotifier: ResizeNotifier; e2eStatus?: E2EStatus; @@ -68,12 +68,9 @@ interface IState { cardState?: IRightPanelCardState; } -export default class RightPanel extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - - public constructor(props: Props, context: React.ContextType) { - super(props, context); +class RightPanel extends React.Component { + public constructor(props: Props) { + super(props); this.state = { searchQuery: "", @@ -89,12 +86,12 @@ export default class RightPanel extends React.Component { ); public componentDidMount(): void { - this.context.on(RoomStateEvent.Members, this.onRoomStateMember); + this.props.mxClient.on(RoomStateEvent.Members, this.onRoomStateMember); RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); } public componentWillUnmount(): void { - this.context?.removeListener(RoomStateEvent.Members, this.onRoomStateMember); + this.props.mxClient?.removeListener(RoomStateEvent.Members, this.onRoomStateMember); RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); } @@ -199,7 +196,7 @@ export default class RightPanel extends React.Component { card = ( { ); } } + +export default withMatrixClientHOC(RightPanel); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 311f6b89b55..7e7586b0ac6 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -15,7 +15,16 @@ limitations under the License. */ import React, { ReactNode } from "react"; -import { EventStatus, MatrixEvent, Room, MatrixError, SyncState, SyncStateData } from "matrix-js-sdk/src/matrix"; +import { + EventStatus, + MatrixEvent, + Room, + MatrixError, + SyncState, + SyncStateData, + ClientEvent, + RoomEvent, +} from "matrix-js-sdk/src/matrix"; import { Icon as WarningIcon } from "../../../res/img/feather-customised/warning-triangle.svg"; import { _t, _td } from "../../languageHandler"; @@ -26,7 +35,7 @@ import { Action } from "../../dispatcher/actions"; import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; import AccessibleButton from "../views/elements/AccessibleButton"; import InlineSpinner from "../views/elements/InlineSpinner"; -import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext"; import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages"; import ExternalLink from "../views/elements/ExternalLink"; @@ -45,7 +54,7 @@ export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] }); } -interface IProps { +interface IProps extends MatrixClientProps { // the room this statusbar is representing. room: Room; @@ -79,31 +88,30 @@ interface IProps { } interface IState { - syncState: SyncState; - syncStateData: SyncStateData; + syncState: SyncState | null; + syncStateData: SyncStateData | null; unsentMessages: MatrixEvent[]; isResending: boolean; } -export default class RoomStatusBar extends React.PureComponent { +class RoomStatusBar extends React.PureComponent { private unmounted = false; - public static contextType = MatrixClientContext; - public constructor(props: IProps, context: typeof MatrixClientContext) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { - syncState: this.context.getSyncState(), - syncStateData: this.context.getSyncStateData(), + syncState: this.props.mxClient.getSyncState(), + syncStateData: this.props.mxClient.getSyncStateData(), unsentMessages: getUnsentMessages(this.props.room), isResending: false, }; } public componentDidMount(): void { - const client = this.context; - client.on("sync", this.onSyncStateChange); - client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated); + const client = this.props.mxClient; + client.on(ClientEvent.Sync, this.onSyncStateChange); + client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); this.checkSize(); } @@ -115,21 +123,21 @@ export default class RoomStatusBar extends React.PureComponent { public componentWillUnmount(): void { this.unmounted = true; // we may have entirely lost our client as we're logging out before clicking login on the guest bar... - const client = this.context; + const client = this.props.mxClient; if (client) { - client.removeListener("sync", this.onSyncStateChange); - client.removeListener("Room.localEchoUpdated", this.onRoomLocalEchoUpdated); + client.removeListener(ClientEvent.Sync, this.onSyncStateChange); + client.removeListener(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); } } - private onSyncStateChange = (state: SyncState, prevState: SyncState, data: SyncStateData): void => { + private onSyncStateChange = (state: SyncState, prevState: SyncState | null, data?: SyncStateData): void => { if (state === "SYNCING" && prevState === "SYNCING") { return; } if (this.unmounted) return; this.setState({ syncState: state, - syncStateData: data, + syncStateData: data ?? null, }); }; @@ -289,3 +297,5 @@ export default class RoomStatusBar extends React.PureComponent { return null; } } + +export default withMatrixClientHOC(RoomStatusBar); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index edc857edaf0..5fe4d7d8baf 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -19,7 +19,7 @@ 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 MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext"; import createRoom, { IOpts } from "../../createRoom"; import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; import { Action } from "../../dispatcher/actions"; @@ -76,7 +76,7 @@ import RightPanel from "./RightPanel"; import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -interface IProps { +interface IProps extends MatrixClientProps { space: Room; justCreatedOpts?: IOpts; resizeNotifier: ResizeNotifier; @@ -603,19 +603,16 @@ const SpaceSetupPrivateInvite: React.FC<{ ); }; -export default class SpaceRoomView extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class SpaceRoomView extends React.PureComponent { private readonly dispatcherRef: string; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); let phase = Phase.Landing; const creator = this.props.space.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender(); - const showSetup = this.props.justCreatedOpts && context.getSafeUserId() === creator; + const showSetup = this.props.justCreatedOpts && props.mxClient.getSafeUserId() === creator; if (showSetup) { phase = @@ -635,13 +632,13 @@ export default class SpaceRoomView extends React.PureComponent { } public componentDidMount(): void { - this.context.on(RoomEvent.MyMembership, this.onMyMembership); + this.props.mxClient.on(RoomEvent.MyMembership, this.onMyMembership); } public componentWillUnmount(): void { defaultDispatcher.unregister(this.dispatcherRef); RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); - this.context.off(RoomEvent.MyMembership, this.onMyMembership); + this.props.mxClient.off(RoomEvent.MyMembership, this.onMyMembership); } private onMyMembership = (room: Room, myMembership: string): void => { @@ -780,3 +777,5 @@ export default class SpaceRoomView extends React.PureComponent { ); } } + +export default withMatrixClientHOC(SpaceRoomView); diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 4d5dd258e01..846dcc2b11c 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -27,9 +27,9 @@ import Spinner from "../views/elements/Spinner"; import ResizeNotifier from "../../utils/ResizeNotifier"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage"; -import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext"; -interface IProps { +interface IProps extends MatrixClientProps { userId: string; resizeNotifier: ResizeNotifier; } @@ -39,10 +39,7 @@ interface IState { member?: RoomMember; } -export default class UserView extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class UserView extends React.Component { public constructor(props: IProps) { super(props); this.state = { @@ -69,7 +66,7 @@ export default class UserView extends React.Component { this.setState({ loading: true }); let profileInfo: Awaited>; try { - profileInfo = await this.context.getProfileInfo(this.props.userId); + profileInfo = await this.props.mxClient.getProfileInfo(this.props.userId); } catch (err) { Modal.createDialog(ErrorDialog, { title: _t("error_dialog|error_loading_user_profile"), @@ -105,3 +102,5 @@ export default class UserView extends React.Component { } } } + +export default withMatrixClientHOC(UserView); diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index a1fdc13f299..208a319d748 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -24,7 +24,7 @@ import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { checkUserIsAllowedToChangeEncryption, IOpts } from "../../../createRoom"; import Field from "../elements/Field"; -import RoomAliasField from "../elements/RoomAliasField"; +import RoomAliasField, { RoomAliasField as RoomAliasFieldClass } from "../elements/RoomAliasField"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "../dialogs/BaseDialog"; @@ -93,7 +93,7 @@ export default class CreateRoomDialog extends React.Component { private readonly askToJoinEnabled: boolean; private readonly supportsRestricted: boolean; private nameField = createRef(); - private aliasField = createRef(); + private aliasField = createRef(); public constructor(props: IProps) { super(props); @@ -197,7 +197,7 @@ export default class CreateRoomDialog extends React.Component { if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) { this.props.onFinished(true, this.roomCreateOptions()); } else { - let field: RoomAliasField | Field | null = null; + let field: RoomAliasFieldClass | Field | null = null; if (!this.state.nameIsValid) { field = this.nameField.current; } else if (this.aliasField.current && !this.aliasField.current.isValid) { diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 3804e71f7d7..9c7dbb13401 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -24,7 +24,7 @@ import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { BetaPill } from "../beta/BetaCard"; import Field from "../elements/Field"; -import RoomAliasField from "../elements/RoomAliasField"; +import { RoomAliasField } from "../elements/RoomAliasField"; import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; import { SubspaceSelector } from "./AddExistingToSpaceDialog"; import JoinRuleDropdown from "../elements/JoinRuleDropdown"; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 7a1b641791a..84f9b49f489 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ContextType, createRef, CSSProperties, MutableRefObject, ReactNode } from "react"; +import React, { createRef, CSSProperties, MutableRefObject, ReactNode } from "react"; import classNames from "classnames"; import { IWidget, MatrixCapabilities } from "matrix-widget-api"; import { Room, RoomEvent } from "matrix-js-sdk/src/matrix"; @@ -50,7 +50,7 @@ import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayo import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import WidgetUtils from "../../../utils/WidgetUtils"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; import { ActionPayload } from "../../../dispatcher/payloads"; import { Action } from "../../../dispatcher/actions"; import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities"; @@ -59,7 +59,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext"; import { ModuleRunner } from "../../../modules/ModuleRunner"; import { parseUrl } from "../../../utils/UrlUtils"; -interface IProps { +interface IProps extends MatrixClientProps { app: IWidget | IApp; // If room is not specified then it is an account level widget // which bypasses permission prompts as it was added explicitly by that user @@ -71,10 +71,10 @@ interface IProps { // Optional. If set, renders a smaller view of the widget miniMode?: boolean; // UserId of the current user - userId: string; + userId?: string; // UserId of the entity that added / modified the widget - creatorUserId: string; - waitForIframeLoad: boolean; + creatorUserId?: string; + waitForIframeLoad?: boolean; showMenubar?: boolean; // Optional onEditClickHandler (overrides default behaviour) onEditClick?: () => void; @@ -87,7 +87,7 @@ interface IProps { // Optionally hide the popout widget icon showPopout?: boolean; // Is this an instance of a user widget - userWidget: boolean; + userWidget?: boolean; // sets the pointer-events property on the iframe pointerEvents?: CSSProperties["pointerEvents"]; widgetPageTitle?: string; @@ -118,10 +118,7 @@ interface IState { hasContextMenuOptions: boolean; } -export default class AppTile extends React.Component { - public static contextType = MatrixClientContext; - public context!: ContextType; - +class AppTile extends React.Component { public static defaultProps: Partial = { waitForIframeLoad: true, showMenubar: true, @@ -142,9 +139,8 @@ export default class AppTile extends React.Component { private dispatcherRef?: string; private unmounted = false; - public constructor(props: IProps, context: ContextType) { + public constructor(props: IProps) { super(props); - this.context = context; // XXX: workaround for lack of `declare` support on `public context!:` definition // Tiles in miniMode are floating, and therefore not docked if (!this.props.miniMode) { @@ -264,10 +260,10 @@ export default class AppTile extends React.Component { menuDisplayed: false, requiresClient: this.determineInitialRequiresClientState(), hasContextMenuOptions: showContextMenu( - this.context, + this.props.mxClient, this.props.room, newProps.app, - newProps.userWidget, + !!newProps.userWidget, !newProps.userWidget, newProps.onDeleteClick, ), @@ -315,7 +311,7 @@ export default class AppTile extends React.Component { this.watchUserReady(); if (this.props.room) { - this.context.on(RoomEvent.MyMembership, this.onMyMembership); + this.props.mxClient.on(RoomEvent.MyMembership, this.onMyMembership); } this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); // Widget action listeners @@ -348,7 +344,7 @@ export default class AppTile extends React.Component { if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.props.room) { - this.context.off(RoomEvent.MyMembership, this.onMyMembership); + this.props.mxClient.off(RoomEvent.MyMembership, this.onMyMembership); } if (this.allowedWidgetsWatchRef) SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef); @@ -646,7 +642,7 @@ export default class AppTile extends React.Component { ); } else if (!this.state.hasPermissionToLoad && this.props.room) { // only possible for room widgets, can assert this.props.room here - const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId); + const isEncrypted = this.props.mxClient.isRoomEncrypted(this.props.room.roomId); appTileBody = (
{ ); } } + +export default withMatrixClientHOC(AppTile); diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 812d3432498..96dd73003d3 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -15,29 +15,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ContextType, CSSProperties, MutableRefObject } from "react"; +import React, { CSSProperties, MutableRefObject } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; import WidgetUtils from "../../../utils/WidgetUtils"; import AppTile from "./AppTile"; import WidgetStore from "../../../stores/WidgetStore"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -interface IProps { +interface IProps extends MatrixClientProps { persistentWidgetId: string; persistentRoomId: string; pointerEvents?: CSSProperties["pointerEvents"]; movePersistedElement: MutableRefObject<(() => void) | undefined>; } -export default class PersistentApp extends React.Component { - public static contextType = MatrixClientContext; - public context!: ContextType; +class PersistentApp extends React.Component> { private room: Room; - public constructor(props: IProps, context: ContextType) { - super(props, context); - this.room = context.getRoom(this.props.persistentRoomId)!; + public constructor(props: React.PropsWithChildren) { + super(props); + this.room = props.mxClient.getRoom(this.props.persistentRoomId)!; } public render(): JSX.Element | null { @@ -50,7 +48,7 @@ export default class PersistentApp extends React.Component { app={app} fullWidth={true} room={this.room} - userId={this.context.getSafeUserId()} + userId={this.props.mxClient.getSafeUserId()} creatorUserId={app.creatorUserId} widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} waitForIframeLoad={app.waitForIframeLoad} @@ -63,3 +61,5 @@ export default class PersistentApp extends React.Component { ); } } + +export default withMatrixClientHOC(PersistentApp); diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index d5353dcabc3..49687a9e6ff 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -20,9 +20,9 @@ import { MatrixError } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import withValidation, { IFieldState, IValidationResult } from "./Validation"; import Field, { IValidateOpts } from "./Field"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -interface IProps { +interface IProps extends MatrixClientProps { domain?: string; value: string; label?: string; @@ -39,14 +39,11 @@ interface IState { } // Controlled form component wrapping Field for inputting a room alias scoped to a given domain -export default class RoomAliasField extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +export class RoomAliasField extends React.PureComponent { private fieldRef = createRef(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { isValid: true, @@ -182,7 +179,7 @@ export default class RoomAliasField extends React.PureComponent if (!value) { return true; } - const client = this.context; + const client = this.props.mxClient; try { const result = await client.getRoomIdForAlias(this.asFullAlias(value)); return result.room_id === this.props.roomId; @@ -200,7 +197,7 @@ export default class RoomAliasField extends React.PureComponent if (!value) { return true; } - const client = this.context; + const client = this.props.mxClient; try { await client.getRoomIdForAlias(this.asFullAlias(value)); // we got a room id, so the alias is taken @@ -235,3 +232,5 @@ export default class RoomAliasField extends React.PureComponent this.fieldRef.current?.focus(); } } + +export default withMatrixClientHOC(RoomAliasField); diff --git a/src/components/views/location/LocationPicker.tsx b/src/components/views/location/LocationPicker.tsx index 2ddd19dfe3d..49d671d2de0 100644 --- a/src/components/views/location/LocationPicker.tsx +++ b/src/components/views/location/LocationPicker.tsx @@ -20,7 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { RoomMember, ClientEvent, IClientWellKnown } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; import Modal from "../../../Modal"; import { tileServerFromWellKnown } from "../../../utils/WellKnownUtils"; import { GenericPosition, genericPositionFromGeolocation, getGeoUri } from "../../../utils/beacon"; @@ -32,7 +32,7 @@ import LiveDurationDropdown, { DEFAULT_DURATION_MS } from "./LiveDurationDropdow import { LocationShareType, ShareLocationFn } from "./shareLocation"; import Marker from "./Marker"; -export interface ILocationPickerProps { +export interface ILocationPickerProps extends MatrixClientProps { sender: RoomMember; shareType: LocationShareType; onChoose: ShareLocationFn; @@ -49,8 +49,6 @@ const isSharingOwnLocation = (shareType: LocationShareType): boolean => shareType === LocationShareType.Own || shareType === LocationShareType.Live; class LocationPicker extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; private map?: maplibregl.Map; private geolocate?: maplibregl.GeolocateControl; private marker?: maplibregl.Marker; @@ -70,12 +68,12 @@ class LocationPicker extends React.Component { }; public componentDidMount(): void { - this.context.on(ClientEvent.ClientWellKnown, this.updateStyleUrl); + this.props.mxClient.on(ClientEvent.ClientWellKnown, this.updateStyleUrl); try { this.map = new maplibregl.Map({ container: "mx_LocationPicker_map", - style: findMapStyleUrl(this.context), + style: findMapStyleUrl(this.props.mxClient), center: [0, 0], zoom: 1, }); @@ -133,7 +131,7 @@ class LocationPicker extends React.Component { this.geolocate?.off("error", this.onGeolocateError); this.geolocate?.off("geolocate", this.onGeolocate); this.map?.off("click", this.onClick); - this.context.off(ClientEvent.ClientWellKnown, this.updateStyleUrl); + this.props.mxClient.off(ClientEvent.ClientWellKnown, this.updateStyleUrl); } private addMarkerToMap = (): void => { @@ -270,4 +268,4 @@ class LocationPicker extends React.Component { } } -export default LocationPicker; +export default withMatrixClientHOC(LocationPicker); diff --git a/src/components/views/location/LocationShareMenu.tsx b/src/components/views/location/LocationShareMenu.tsx index 036e2e1fe08..3b81ff3fdd6 100644 --- a/src/components/views/location/LocationShareMenu.tsx +++ b/src/components/views/location/LocationShareMenu.tsx @@ -29,7 +29,7 @@ import { EnableLiveShare } from "./EnableLiveShare"; import { useFeatureEnabled } from "../../../hooks/useSettings"; import { SettingLevel } from "../../../settings/SettingLevel"; -type Props = Omit & { +type Props = Omit & { onFinished: (ev?: SyntheticEvent) => void; menuPosition: MenuProps; openMenu: () => void; diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index 49e0f1f7de4..39c4f77cb5b 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -30,14 +30,14 @@ import AccessibleButton from "../elements/AccessibleButton"; import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog"; import ViewSource from "../../structures/ViewSource"; import SettingsStore from "../../../settings/SettingsStore"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; function getReplacedContent(event: MatrixEvent): IContent { const originalContent = event.getOriginalContent(); return originalContent["m.new_content"] || originalContent; } -interface IProps { +interface IProps extends MatrixClientProps { // the message event being edited mxEvent: MatrixEvent; previousEdit?: MatrixEvent; @@ -50,19 +50,15 @@ interface IState { sendStatus: EventStatus | null; } -export default class EditHistoryMessage extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class EditHistoryMessage extends React.PureComponent { private content = createRef(); private pills: Element[] = []; private tooltips: Element[] = []; - public constructor(props: IProps, context: React.ContextType) { + public constructor(props: IProps) { super(props); - this.context = context; - const cli = this.context; + const cli = this.props.mxClient; const userId = cli.getSafeUserId(); const event = this.props.mxEvent; const room = cli.getRoom(event.getRoomId()); @@ -77,7 +73,7 @@ export default class EditHistoryMessage extends React.PureComponent => { const event = this.props.mxEvent; - const cli = this.context; + const cli = this.props.mxClient; Modal.createDialog( ConfirmAndWaitRedactDialog, @@ -105,7 +101,7 @@ export default class EditHistoryMessage extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class MLocationBody extends React.Component { private unmounted = false; private mapId: string; private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; - public constructor(props: IBodyProps) { + public constructor(props: Props) { super(props); // multiple instances of same map might be in document @@ -62,7 +61,7 @@ export default class MLocationBody extends React.Component { Modal.createDialog( LocationViewDialog, { - matrixClient: this.context, + matrixClient: this.props.mxClient, mxEvent: this.props.mxEvent, }, "mx_LocationViewDialog_wrapper", @@ -72,7 +71,7 @@ export default class MLocationBody extends React.Component { }; private clearError = (): void => { - this.context.off(ClientEvent.Sync, this.reconnectedListener); + this.props.mxClient.off(ClientEvent.Sync, this.reconnectedListener); this.setState({ error: undefined }); }; @@ -80,13 +79,13 @@ export default class MLocationBody extends React.Component { if (this.unmounted) return; this.setState({ error }); // Unregister first in case we already had it registered - this.context.off(ClientEvent.Sync, this.reconnectedListener); - this.context.on(ClientEvent.Sync, this.reconnectedListener); + this.props.mxClient.off(ClientEvent.Sync, this.reconnectedListener); + this.props.mxClient.on(ClientEvent.Sync, this.reconnectedListener); }; public componentWillUnmount(): void { this.unmounted = true; - this.context.off(ClientEvent.Sync, this.reconnectedListener); + this.props.mxClient.off(ClientEvent.Sync, this.reconnectedListener); } public render(): React.ReactElement { @@ -104,6 +103,8 @@ export default class MLocationBody extends React.Component { } } +export default withMatrixClientHOC(MLocationBody); + export const LocationBodyFallbackContent: React.FC<{ event: MatrixEvent; error: Error }> = ({ error, event }) => { const errorType = error?.message as LocationShareError; const message = `${_t("location_sharing|failed_load_map")}: ${getLocationShareErrorMessage(errorType)}`; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index d777ed9d779..fb0536df277 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -35,7 +35,7 @@ import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import { IBodyProps } from "./IBodyProps"; import { formatList } from "../../../utils/FormattingUtils"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; import ErrorDialog from "../dialogs/ErrorDialog"; import { GetRelationsForEvent } from "../rooms/EventTile"; import PollCreateDialog from "../elements/PollCreateDialog"; @@ -43,6 +43,8 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Spinner from "../elements/Spinner"; import { PollOption } from "../polls/PollOption"; +interface Props extends IBodyProps, MatrixClientProps {} + interface IState { poll?: Poll; // poll instance has fetched at least one page of responses @@ -145,12 +147,10 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge } } -export default class MPollBody extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; +class MPollBody extends React.Component { private seenEventIds: string[] = []; // Events we have already seen - public constructor(props: IBodyProps) { + public constructor(props: Props) { super(props); this.state = { @@ -160,7 +160,7 @@ export default class MPollBody extends React.Component { } public componentDidMount(): void { - const room = this.context?.getRoom(this.props.mxEvent.getRoomId()); + const room = this.props.mxClient?.getRoom(this.props.mxEvent.getRoomId()); const poll = room?.polls.get(this.props.mxEvent.getId()!); if (poll) { this.setPollInstance(poll); @@ -218,7 +218,7 @@ export default class MPollBody extends React.Component { return; } const userVotes = this.collectUserVotes(); - const userId = this.context.getSafeUserId(); + const userId = this.props.mxClient.getSafeUserId(); const myVote = userVotes.get(userId)?.answers[0]; if (answerId === myVote) { return; @@ -226,7 +226,7 @@ export default class MPollBody extends React.Component { const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()!).serialize(); - this.context + this.props.mxClient .sendEvent( this.props.mxEvent.getRoomId()!, response.type as keyof TimelineEvents, @@ -248,10 +248,14 @@ export default class MPollBody extends React.Component { * @returns userId -> UserVote */ private collectUserVotes(): Map { - if (!this.state.voteRelations || !this.context) { + if (!this.state.voteRelations || !this.props.mxClient) { return new Map(); } - return collectUserVotes(allVotes(this.state.voteRelations), this.context.getUserId(), this.state.selected); + return collectUserVotes( + allVotes(this.state.voteRelations), + this.props.mxClient.getUserId(), + this.state.selected, + ); } /** @@ -271,7 +275,7 @@ export default class MPollBody extends React.Component { if (newEvents.length > 0) { for (const mxEvent of newEvents) { - if (mxEvent.getSender() === this.context.getUserId()) { + if (mxEvent.getSender() === this.props.mxClient.getUserId()) { newSelected = null; } } @@ -303,7 +307,7 @@ export default class MPollBody extends React.Component { const votes = countVotes(userVotes, pollEvent); const totalVotes = this.totalVotes(votes); const winCount = Math.max(...votes.values()); - const userId = this.context.getSafeUserId(); + const userId = this.props.mxClient.getSafeUserId(); const myVote = userVotes?.get(userId)?.answers[0]; const disclosed = M_POLL_KIND_DISCLOSED.matches(pollEvent.kind.name); @@ -372,6 +376,9 @@ export default class MPollBody extends React.Component { ); } } + +export default withMatrixClientHOC(MPollBody); + export class UserVote { public constructor( public readonly ts: number, diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 853fd7f9a9d..bd37ae7cafe 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -32,7 +32,6 @@ import UnknownBody from "./UnknownBody"; import { IMediaBody } from "./IMediaBody"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { IBodyProps } from "./IBodyProps"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import TextualBody from "./TextualBody"; import MImageBody from "./MImageBody"; import MFileBody from "./MFileBody"; @@ -91,11 +90,8 @@ export default class MessageEvent extends React.Component implements IMe private bodyTypes = new Map(baseBodyTypes.entries()); private evTypes = new Map(baseEvTypes.entries()); - public static contextType = MatrixClientContext; - public context!: React.ContextType; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); if (MediaEventHelper.isEligible(this.props.mxEvent)) { this.mediaHelper = new MediaEventHelper(this.props.mxEvent); diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 2737212d33b..9f60bde5ea9 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -24,10 +24,10 @@ import { formatList } from "../../../utils/FormattingUtils"; import dis from "../../../dispatcher/dispatcher"; import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip"; import AccessibleButton from "../elements/AccessibleButton"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; import { REACTION_SHORTCODE_KEY } from "./ReactionsRow"; -export interface IProps { +interface IProps extends MatrixClientProps { // The event we're displaying reactions for mxEvent: MatrixEvent; // The reaction content / key / emoji @@ -49,10 +49,7 @@ interface IState { tooltipVisible: boolean; } -export default class ReactionsRowButton extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class ReactionsRowButton extends React.PureComponent { public state = { tooltipRendered: false, tooltipVisible: false, @@ -61,9 +58,9 @@ export default class ReactionsRowButton extends React.PureComponent { const { mxEvent, myReactionEvent, content } = this.props; if (myReactionEvent) { - this.context.redactEvent(mxEvent.getRoomId()!, myReactionEvent.getId()!); + this.props.mxClient.redactEvent(mxEvent.getRoomId()!, myReactionEvent.getId()!); } else { - this.context.sendEvent(mxEvent.getRoomId()!, EventType.Reaction, { + this.props.mxClient.sendEvent(mxEvent.getRoomId()!, EventType.Reaction, { "m.relates_to": { rel_type: RelationType.Annotation, event_id: mxEvent.getId()!, @@ -110,7 +107,7 @@ export default class ReactionsRowButton extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class ReactionsRowButtonTooltip extends React.PureComponent { public render(): React.ReactNode { const { content, reactionEvents, mxEvent, visible } = this.props; - const room = this.context.getRoom(mxEvent.getRoomId()); + const room = this.props.mxClient.getRoom(mxEvent.getRoomId()); let tooltipLabel: JSX.Element | undefined; if (room) { const senders: string[] = []; @@ -88,3 +86,5 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { - private aliasField = createRef(); + private aliasField = createRef(); private onAliasAdded = async (ev: SyntheticEvent): Promise => { ev.preventDefault(); @@ -78,7 +78,7 @@ class EditableAliasesList extends EditableItemList { } } -interface IProps { +interface IProps extends MatrixClientProps { roomId: string; canSetCanonicalAlias: boolean; canSetAliases: boolean; @@ -100,17 +100,14 @@ interface IState { newAltAlias?: string; } -export default class AliasSettings extends React.Component { - public static contextType = MatrixClientContext; - public context!: ContextType; - +class AliasSettings extends React.Component { public static defaultProps = { canSetAliases: false, canSetCanonicalAlias: false, }; - public constructor(props: IProps, context: ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); const state: IState = { altAliases: [], @@ -144,7 +141,7 @@ export default class AliasSettings extends React.Component { private async loadLocalAliases(): Promise { this.setState({ localAliasesLoading: true }); try { - const mxClient = this.context; + const mxClient = this.props.mxClient; let localAliases: string[] = []; const response = await mxClient.getLocalAliases(this.props.roomId); @@ -176,7 +173,7 @@ export default class AliasSettings extends React.Component { if (alias) eventContent["alias"] = alias; - this.context + this.props.mxClient .sendStateEvent(this.props.roomId, EventType.RoomCanonicalAlias, eventContent, "") .catch((err) => { logger.error(err); @@ -207,7 +204,7 @@ export default class AliasSettings extends React.Component { eventContent["alt_aliases"] = altAliases; } - this.context + this.props.mxClient .sendStateEvent(this.props.roomId, EventType.RoomCanonicalAlias, eventContent, "") .then(() => { this.setState({ @@ -234,10 +231,10 @@ export default class AliasSettings extends React.Component { private onLocalAliasAdded = (alias?: string): void => { if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases - const localDomain = this.context.getDomain(); + const localDomain = this.props.mxClient.getDomain(); if (!alias.includes(":")) alias += ":" + localDomain; - this.context + this.props.mxClient .createAlias(alias, this.props.roomId) .then(() => { this.setState({ @@ -261,7 +258,7 @@ export default class AliasSettings extends React.Component { const alias = this.state.localAliases[index]; // TODO: In future, we should probably be making sure that the alias actually belongs // to this room. See https://github.com/vector-im/element-web/issues/7353 - this.context + this.props.mxClient .deleteAlias(alias) .then(() => { const localAliases = this.state.localAliases.filter((a) => a !== alias); @@ -330,7 +327,7 @@ export default class AliasSettings extends React.Component { } public render(): React.ReactNode { - const mxClient = this.context; + const mxClient = this.props.mxClient; const localDomain = mxClient.getDomain()!; const isSpaceRoom = mxClient.getRoom(this.props.roomId)?.isSpaceRoom(); @@ -455,3 +452,5 @@ export default class AliasSettings extends React.Component { ); } } + +export default withMatrixClientHOC(AliasSettings); diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx index 1395dcc2c5a..42b0ef9b7c7 100644 --- a/src/components/views/rooms/PinnedEventTile.tsx +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -25,7 +25,6 @@ import MessageEvent from "../messages/MessageEvent"; import MemberAvatar from "../avatars/MemberAvatar"; import { _t } from "../../../languageHandler"; import { formatDate } from "../../../DateUtils"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; @@ -40,9 +39,6 @@ interface IProps { const AVATAR_SIZE = "24px"; export default class PinnedEventTile extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - private onTileClicked = (): void => { dis.dispatch({ action: Action.ViewRoom, diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 073950e30f4..e8c6d1fc8d0 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -18,7 +18,6 @@ import { EventType, RoomType, Room } from "matrix-js-sdk/src/matrix"; import React, { ComponentType, createRef, ReactComponentElement, SyntheticEvent } from "react"; import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { Action } from "../../../dispatcher/actions"; import defaultDispatcher from "../../../dispatcher/dispatcher"; @@ -428,9 +427,6 @@ export default class RoomList extends React.PureComponent { private dispatcherRef?: string; private treeRef = createRef(); - public static contextType = MatrixClientContext; - public context!: React.ContextType; - public constructor(props: IProps) { super(props); diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 99ae4f7ba4b..8e695aca7b5 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -21,9 +21,9 @@ import Modal from "../../../Modal"; import { _t } from "../../../languageHandler"; import RoomUpgradeDialog from "../dialogs/RoomUpgradeDialog"; import AccessibleButton from "../elements/AccessibleButton"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -interface IProps { +interface IProps extends MatrixClientProps { room: Room; } @@ -31,12 +31,9 @@ interface IState { upgraded?: boolean; } -export default class RoomUpgradeWarningBar extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); +class RoomUpgradeWarningBar extends React.PureComponent { + public constructor(props: IProps) { + super(props); const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", ""); this.state = { @@ -45,11 +42,11 @@ export default class RoomUpgradeWarningBar extends React.PureComponent { @@ -119,3 +116,5 @@ export default class RoomUpgradeWarningBar extends React.PureComponent { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class BridgeSettingsTab extends React.Component { private renderBridgeCard(event: MatrixEvent, room: Room | null): ReactNode { const content = event.getContent(); if (!room || !content?.channel || !content.protocol) return null; @@ -54,7 +51,7 @@ export default class BridgeSettingsTab extends React.Component { public render(): React.ReactNode { // This settings tab will only be invoked if the following function returns more // than 0 events, so no validation is needed at this stage. - const bridgeEvents = BridgeSettingsTab.getBridgeStateEvents(this.context, this.props.room.roomId); + const bridgeEvents = BridgeSettingsTab.getBridgeStateEvents(this.props.mxClient, this.props.room.roomId); const room = this.props.room; let content: JSX.Element; @@ -108,3 +105,5 @@ export default class BridgeSettingsTab extends React.Component { ); } } + +export default withMatrixClientHOC(BridgeSettingsTab); diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 7b260e3a7e6..b0cf7181ebd 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ContextType } from "react"; +import React from "react"; import { Room } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; @@ -22,7 +22,7 @@ import { _t } from "../../../../../languageHandler"; import RoomProfileSettings from "../../../room_settings/RoomProfileSettings"; import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher/dispatcher"; -import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../../../contexts/MatrixClientContext"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings"; @@ -32,7 +32,7 @@ import SettingsSubsection from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -interface IProps { +interface IProps extends MatrixClientProps { room: Room; } @@ -40,12 +40,9 @@ interface IState { isRoomPublished: boolean; } -export default class GeneralRoomSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: ContextType; - - public constructor(props: IProps, context: ContextType) { - super(props, context); +class GeneralRoomSettingsTab extends React.Component { + public constructor(props: IProps) { + super(props); this.state = { isRoomPublished: false, // loaded async @@ -62,7 +59,7 @@ export default class GeneralRoomSettingsTab extends React.Component { +class NotificationsSettingsTab extends React.Component { private readonly roomProps: RoomEchoChamber; private soundUpload = createRef(); - public static contextType = MatrixClientContext; - public context!: React.ContextType; + public constructor(props: IProps) { + super(props); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); - - this.roomProps = EchoChamber.forRoom(context.getRoom(this.props.roomId)!); + this.roomProps = EchoChamber.forRoom(props.mxClient.getRoom(this.props.roomId)!); let currentSound = "default"; const soundData = Notifier.getSoundForRoom(this.props.roomId); @@ -315,3 +312,5 @@ export default class NotificationsSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class BannedUser extends React.Component { private onUnbanClick = (): void => { - this.context.unban(this.props.member.roomId, this.props.member.userId).catch((err) => { + this.props.mxClient.unban(this.props.member.roomId, this.props.member.userId).catch((err: Error) => { logger.error("Failed to unban: " + err); Modal.createDialog(ErrorDialog, { title: _t("common|error"), @@ -131,20 +128,17 @@ export class BannedUser extends React.Component { } } -interface IProps { +interface IProps extends MatrixClientProps { room: Room; } -export default class RolesRoomSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class RolesRoomSettingsTab extends React.Component { public componentDidMount(): void { - this.context.on(RoomStateEvent.Update, this.onRoomStateUpdate); + this.props.mxClient.on(RoomStateEvent.Update, this.onRoomStateUpdate); } public componentWillUnmount(): void { - const client = this.context; + const client = this.props.mxClient; if (client) { client.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate); } @@ -176,7 +170,7 @@ export default class RolesRoomSettingsTab extends React.Component { } private onPowerLevelsChanged = async (value: number, powerLevelKey: string): Promise => { - const client = this.context; + const client = this.props.mxClient; const room = this.props.room; const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); let plContent = plEvent?.getContent() ?? {}; @@ -220,7 +214,7 @@ export default class RolesRoomSettingsTab extends React.Component { }; private onUserPowerLevelChanged = async (value: number, powerLevelKey: string): Promise => { - const client = this.context; + const client = this.props.mxClient; const room = this.props.room; const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); let plContent = plEvent?.getContent() ?? {}; @@ -245,7 +239,7 @@ export default class RolesRoomSettingsTab extends React.Component { }; public render(): React.ReactNode { - const client = this.context; + const client = this.props.mxClient; const room = this.props.room; const isSpaceRoom = room.isSpaceRoom(); @@ -394,6 +388,7 @@ export default class RolesRoomSettingsTab extends React.Component { member={member} reason={banEvent?.reason} by={bannedBy!} + mxClient={this.props.mxClient} /> ); })} @@ -484,3 +479,5 @@ export default class RolesRoomSettingsTab extends React.Component { ); } } + +export default withMatrixClientHOC(RolesRoomSettingsTab); diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 6ac686e0639..2b27e2a36ba 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -44,14 +44,14 @@ import ErrorDialog from "../../../dialogs/ErrorDialog"; import SettingsFieldset from "../../SettingsFieldset"; import ExternalLink from "../../../elements/ExternalLink"; import PosthogTrackers from "../../../../../PosthogTrackers"; -import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../../../contexts/MatrixClientContext"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsTab from "../SettingsTab"; import SdkConfig from "../../../../../SdkConfig"; import { shouldForceDisableEncryption } from "../../../../../utils/crypto/shouldForceDisableEncryption"; import { Caption } from "../../../typography/Caption"; -interface IProps { +interface IProps extends MatrixClientProps { room: Room; closeSettingsFn: () => void; } @@ -64,12 +64,9 @@ interface IState { showAdvancedSection: boolean; } -export default class SecurityRoomSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); +class SecurityRoomSettingsTab extends React.Component { + public constructor(props: IProps) { + super(props); const state = this.props.room.currentState; @@ -85,13 +82,13 @@ export default class SecurityRoomSettingsTab extends React.Component this.setState({ hasAliases })); } @@ -100,7 +97,7 @@ export default class SecurityRoomSettingsTab extends React.Component { @@ -174,7 +171,7 @@ export default class SecurityRoomSettingsTab extends React.Component { - const cli = this.context; + const cli = this.props.mxClient; const response = await cli.getLocalAliases(this.props.room.roomId); const localAliases = response.aliases; return Array.isArray(localAliases) && localAliases.length !== 0; @@ -352,7 +349,7 @@ export default class SecurityRoomSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - +class AppearanceUserSettingsTab extends React.Component { private readonly MESSAGE_PREVIEW_TEXT = _t("common|preview_message"); private unmounted = false; - public constructor(props: IProps) { + public constructor(props: MatrixClientProps) { super(props); this.state = { @@ -71,7 +66,7 @@ export default class AppearanceUserSettingsTab extends React.Component { // Fetch the current user profile for the message preview - const client = this.context; + const client = this.props.mxClient; const userId = client.getUserId()!; const profileInfo = await client.getProfileInfo(userId); if (this.unmounted) return; @@ -174,3 +169,5 @@ export default class AppearanceUserSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - - public constructor(props: IProps) { +class HelpUserSettingsTab extends React.Component { + public constructor(props: MatrixClientProps) { super(props); this.state = { @@ -69,7 +64,7 @@ export default class HelpUserSettingsTab extends React.Component private getVersionInfo(): { appVersion: string; cryptoVersion: string } { const brand = SdkConfig.get().brand; const appVersion = this.state.appVersion || "unknown"; - const cryptoVersion = this.context.getCrypto()?.getVersion() ?? ""; + const cryptoVersion = this.props.mxClient.getCrypto()?.getVersion() ?? ""; return { appVersion: `${_t("setting|help_about|brand_version", { brand })} ${appVersion}`, @@ -83,8 +78,8 @@ export default class HelpUserSettingsTab extends React.Component // Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly // stopping in the middle of the logs. logger.log("Clear cache & reload clicked"); - this.context.stopClient(); - this.context.store.deleteAllData().then(() => { + this.props.mxClient.stopClient(); + this.props.mxClient.store.deleteAllData().then(() => { PlatformPeg.get()?.reload(); }); }; @@ -285,19 +280,19 @@ export default class HelpUserSettingsTab extends React.Component {_t( "setting|help_about|homeserver", { - homeserverUrl: this.context.getHomeserverUrl(), + homeserverUrl: this.props.mxClient.getHomeserverUrl(), }, { code: (sub) => {sub}, }, )} - {this.context.getIdentityServerUrl() && ( + {this.props.mxClient.getIdentityServerUrl() && ( {_t( "setting|help_about|identity_server", { - identityServerUrl: this.context.getIdentityServerUrl(), + identityServerUrl: this.props.mxClient.getIdentityServerUrl(), }, { code: (sub) => {sub}, @@ -311,8 +306,8 @@ export default class HelpUserSettingsTab extends React.Component {_t("common|access_token")} {_t("setting|help_about|access_token_detail")} - this.context.getAccessToken()}> - {this.context.getAccessToken()} + this.props.mxClient.getAccessToken()}> + {this.props.mxClient.getAccessToken()} @@ -325,3 +320,5 @@ export default class HelpUserSettingsTab extends React.Component ); } } + +export default withMatrixClientHOC(HelpUserSettingsTab); diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index c75539025a4..9e03d8eca2b 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -30,7 +30,7 @@ import { requestMediaPermissions } from "../../../../../utils/media/requestMedia import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection from "../../shared/SettingsSubsection"; -import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import { MatrixClientProps, withMatrixClientHOC } from "../../../../../contexts/MatrixClientContext"; interface IState { mediaDevices: IMediaDevices | null; @@ -57,11 +57,8 @@ const mapDeviceKindToHandlerValue = (deviceKind: MediaDeviceKindEnum): string | } }; -export default class VoiceUserSettingsTab extends React.Component<{}, IState> { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - - public constructor(props: {}) { +class VoiceUserSettingsTab extends React.Component { + public constructor(props: MatrixClientProps) { super(props); this.state = { @@ -242,3 +239,5 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { ); } } + +export default withMatrixClientHOC(VoiceUserSettingsTab); diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 2ded20912d2..8aeeecc55c0 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -46,7 +46,7 @@ import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; -import RoomAliasField from "../elements/RoomAliasField"; +import RoomAliasField, { RoomAliasField as RoomAliasFieldClass } from "../elements/RoomAliasField"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import defaultDispatcher from "../../../dispatcher/dispatcher"; @@ -128,7 +128,7 @@ interface ISpaceCreateFormProps extends BProps { busy: boolean; alias: string; nameFieldRef: RefObject; - aliasFieldRef: RefObject; + aliasFieldRef: RefObject; showAliasField?: boolean; children?: ReactNode; onSubmit(e: SyntheticEvent): void; @@ -225,7 +225,7 @@ const SpaceCreateMenu: React.FC<{ const [name, setName] = useState(""); const spaceNameField = useRef(null); const [alias, setAlias] = useState(""); - const spaceAliasField = useRef(null); + const spaceAliasField = useRef(null); const [avatar, setAvatar] = useState(undefined); const [topic, setTopic] = useState(""); diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 6b43399c28b..e7f905068ac 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -38,7 +38,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import MatrixClientContext, { withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; @@ -190,6 +190,7 @@ interface IItemProps extends InputHTMLAttributes { parents?: Set; innerRef?: LegacyRef; dragHandleProps?: DraggableProvidedDragHandleProps | null; + mxClient: React.ContextType; } interface IItemState { @@ -198,9 +199,7 @@ interface IItemState { childSpaces: Room[]; } -export class SpaceItem extends React.PureComponent { - public static contextType = MatrixClientContext; - +class InnerSpaceItem extends React.PureComponent { private buttonRef = createRef(); public constructor(props: IItemProps) { @@ -392,6 +391,8 @@ export class SpaceItem extends React.PureComponent { } } +export const SpaceItem = withMatrixClientHOC(InnerSpaceItem); + interface ITreeLevelProps { spaces: Room[]; activeSpaces: SpaceKey[]; diff --git a/src/stores/room-list/previews/PollStartEventPreview.ts b/src/stores/room-list/previews/PollStartEventPreview.ts index 793a8230716..fe956542079 100644 --- a/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/src/stores/room-list/previews/PollStartEventPreview.ts @@ -22,12 +22,8 @@ import { IPreview } from "./IPreview"; import { TagID } from "../models"; import { _t, sanitizeForTranslation } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; export class PollStartEventPreview implements IPreview { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 36ade5d5947..4775945dbcb 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -71,11 +71,11 @@ interface IAppTileProps { // Note: these are only the props we care about app: IApp | IWidget; room?: Room; // without a room it is a user widget - userId: string; - creatorUserId: string; - waitForIframeLoad: boolean; + userId?: string; + creatorUserId?: string; + waitForIframeLoad?: boolean; whitelistCapabilities?: string[]; - userWidget: boolean; + userWidget?: boolean; stickyPromise?: () => Promise; } diff --git a/test/components/views/messages/ReactionsRowButton-test.tsx b/test/components/views/messages/ReactionsRowButton-test.tsx index b3ec6313d12..eb499b6330a 100644 --- a/test/components/views/messages/ReactionsRowButton-test.tsx +++ b/test/components/views/messages/ReactionsRowButton-test.tsx @@ -20,7 +20,7 @@ import { render } from "@testing-library/react"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { getMockClientWithEventEmitter } from "../../../test-utils"; -import ReactionsRowButton, { IProps } from "../../../../src/components/views/messages/ReactionsRowButton"; +import ReactionsRowButton from "../../../../src/components/views/messages/ReactionsRowButton"; describe("ReactionsRowButton", () => { const userId = "@alice:server"; @@ -31,7 +31,7 @@ describe("ReactionsRowButton", () => { }); const room = new Room(roomId, mockClient, userId); - const createProps = (relationContent: IContent): IProps => ({ + const createProps = (relationContent: IContent): React.ComponentProps => ({ mxEvent: new MatrixEvent({ room_id: roomId, event_id: "$test:example.com", diff --git a/test/components/views/settings/tabs/room/NotificationSettingsTab-test.tsx b/test/components/views/settings/tabs/room/NotificationSettingsTab-test.tsx index 0ed688b032f..eb619c4b885 100644 --- a/test/components/views/settings/tabs/room/NotificationSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/NotificationSettingsTab-test.tsx @@ -19,15 +19,17 @@ import { render, RenderResult, screen } from "@testing-library/react"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import userEvent from "@testing-library/user-event"; -import NotificationSettingsTab from "../../../../../../src/components/views/settings/tabs/room/NotificationSettingsTab"; -import { mkStubRoom, stubClient } from "../../../../../test-utils"; +import _NotificationSettingsTab from "../../../../../../src/components/views/settings/tabs/room/NotificationSettingsTab"; +import { mkStubRoom, stubClient, wrapInMatrixClientContext } from "../../../../../test-utils"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; import { EchoChamber } from "../../../../../../src/stores/local-echo/EchoChamber"; import { RoomEchoChamber } from "../../../../../../src/stores/local-echo/RoomEchoChamber"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; -describe("NotificatinSettingsTab", () => { +const NotificationSettingsTab = wrapInMatrixClientContext(_NotificationSettingsTab); + +describe("NotificationSettingsTab", () => { const roomId = "!room:example.com"; let cli: MatrixClient; let roomProps: RoomEchoChamber; @@ -41,8 +43,6 @@ describe("NotificatinSettingsTab", () => { cli = MatrixClientPeg.safeGet(); const room = mkStubRoom(roomId, "test room", cli); roomProps = EchoChamber.forRoom(room); - - NotificationSettingsTab.contextType = React.createContext(cli); }); it("should prevent »Settings« link click from bubbling up to radio buttons", async () => { From 4aaf049cac518c61fe4eca9907009123c58c0e2e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 16:04:39 +0100 Subject: [PATCH 6/8] Fix missed accessors Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ThreadView.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 10 +++++----- .../views/context_menus/MessageContextMenu.tsx | 2 +- src/components/views/right_panel/TimelineCard.tsx | 2 +- .../settings/tabs/room/NotificationSettingsTab.tsx | 2 +- .../views/settings/tabs/user/VoiceUserSettingsTab.tsx | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 1a29fd61c80..57c77e1ed8e 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -429,7 +429,7 @@ class ThreadView extends React.Component { return ( { private readMarkerTimeout(readMarkerPosition: number | null): number { return readMarkerPosition === 0 - ? this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs - : this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs; + ? this.props.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs + : this.props.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs; } private async updateReadMarkerOnUserActivity(): Promise { @@ -1978,7 +1978,7 @@ export class TimelinePanel extends React.Component { !!ev.status || // local echo (ignoreOwn && ev.getSender() === myUserId); // own message const isWithoutTile = - !haveRendererForEvent(ev, MatrixClientPeg.safeGet(), this.context?.showHiddenEvents) || + !haveRendererForEvent(ev, MatrixClientPeg.safeGet(), this.props.context?.showHiddenEvents) || shouldHideEvent(ev, this.context); if (isWithoutTile || !node) { @@ -2144,10 +2144,10 @@ export class TimelinePanel extends React.Component { onScroll={this.onMessageListScroll} onFillRequest={this.onMessageListFillRequest} onUnfillRequest={this.onMessageListUnfillRequest} - isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour} + isTwelveHour={this.props.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour} alwaysShowTimestamps={ this.props.alwaysShowTimestamps ?? - this.context?.alwaysShowTimestamps ?? + this.props.context?.alwaysShowTimestamps ?? this.state.alwaysShowTimestamps } className={this.props.className} diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 9c87d738949..f7fd4926e08 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -391,7 +391,7 @@ class MessageContextMenu extends React.Component { const permalink = this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId()!); // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; - const { timelineRenderingType, canReact, canSendMessages } = this.context; + const { timelineRenderingType, canReact, canSendMessages } = this.props.context; const isThread = timelineRenderingType === TimelineRenderingType.Thread || timelineRenderingType === TimelineRenderingType.ThreadsList; diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index 6e21b5895c2..9b1bf339480 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -222,7 +222,7 @@ class TimelineCard extends React.Component { return ( { type = "audio/ogg"; } - const { content_uri: url } = await this.context.uploadContent(this.state.uploadedFile, { + const { content_uri: url } = await this.mxClient.uploadContent(this.state.uploadedFile, { type, }); diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 9e03d8eca2b..37fe3d4f522 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -114,11 +114,11 @@ class VoiceUserSettingsTab extends React.Component { }; private changeWebRtcMethod = (p2p: boolean): void => { - this.context.setForceTURN(!p2p); + this.mxClient.setForceTURN(!p2p); }; private changeFallbackICEServerAllowed = (allow: boolean): void => { - this.context.setFallbackICEServerAllowed(allow); + this.mxClient.setFallbackICEServerAllowed(allow); }; private renderDeviceOptions(devices: Array, category: MediaDeviceKindEnum): Array { From 6850ff61f12d18fcf79fec7e5fff1331a4a03b59 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 16:08:15 +0100 Subject: [PATCH 7/8] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/tabs/room/NotificationSettingsTab.tsx | 2 +- .../views/settings/tabs/user/VoiceUserSettingsTab.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx index 83aba27f47e..8aa7a0c022f 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx @@ -112,7 +112,7 @@ class NotificationsSettingsTab extends React.Component { type = "audio/ogg"; } - const { content_uri: url } = await this.mxClient.uploadContent(this.state.uploadedFile, { + const { content_uri: url } = await this.props.mxClient.uploadContent(this.state.uploadedFile, { type, }); diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 37fe3d4f522..37a97e06827 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -114,11 +114,11 @@ class VoiceUserSettingsTab extends React.Component { }; private changeWebRtcMethod = (p2p: boolean): void => { - this.mxClient.setForceTURN(!p2p); + this.props.mxClient.setForceTURN(!p2p); }; private changeFallbackICEServerAllowed = (allow: boolean): void => { - this.mxClient.setFallbackICEServerAllowed(allow); + this.props.mxClient.setFallbackICEServerAllowed(allow); }; private renderDeviceOptions(devices: Array, category: MediaDeviceKindEnum): Array { From c7db01814c010b3bc580aea410a4989411e3c377 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 May 2024 15:22:56 +0100 Subject: [PATCH 8/8] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/contexts/MatrixClientContext.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/contexts/MatrixClientContext.tsx b/src/contexts/MatrixClientContext.tsx index 273bf773d31..fb4d3aeffd1 100644 --- a/src/contexts/MatrixClientContext.tsx +++ b/src/contexts/MatrixClientContext.tsx @@ -32,22 +32,24 @@ export function useMatrixClientContext(): MatrixClient { return useContext(MatrixClientContext); } -const matrixHOC = ( +/** + * A higher order component that injects the MatrixClient into props.mxClient of the wrapped component. + * Preferred over using `static contextType` as the types for this are quite broken in React 17. + * Inherently no different to wrapping in MatrixClientContext.Consumer but saves a lot of boilerplate. + * @param ComposedComponent the ComponentClass you wish to wrap in the HOC + * @returns a new component that takes the same props as the original component, but with an additional mxClient prop + */ +export const withMatrixClientHOC = ( ComposedComponent: ComponentClass, ): (( props: Omit & React.RefAttributes>, ) => React.ReactElement | null) => { type ComposedComponentInstance = InstanceType; - // eslint-disable-next-line react-hooks/rules-of-hooks - - const TypedComponent = ComposedComponent; - return forwardRef>((props, ref) => { const client = useContext(MatrixClientContext); // @ts-ignore - return ; + return ; }); }; -export const withMatrixClientHOC = matrixHOC;