1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-08-07 21:23:00 +03:00

Remove legacy consumers of the RoomContext in favour of HOCs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2024-04-19 14:31:48 +01:00
parent f4296b4893
commit 9fe036ae1d
52 changed files with 509 additions and 367 deletions

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -56,8 +57,6 @@ interface IState {
* Component which shows the filtered file using a TimelinePanel
*/
class FilePanel extends React.Component<IProps, IState> {
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<string>();
@@ -267,7 +266,7 @@ class FilePanel extends React.Component<IProps, IState> {
return (
<RoomContext.Provider
value={{
...this.context,
...this.props.context,
timelineRenderingType: TimelineRenderingType.File,
narrow: this.state.narrow,
}}
@@ -299,7 +298,7 @@ class FilePanel extends React.Component<IProps, IState> {
return (
<RoomContext.Provider
value={{
...this.context,
...this.props.context,
timelineRenderingType: TimelineRenderingType.File,
}}
>
@@ -312,4 +311,6 @@ class FilePanel extends React.Component<IProps, IState> {
}
}
export default FilePanel;
export default forwardRef<FilePanel, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <FilePanel {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<string, LegacyCallEventGrouper>;
context: React.ContextType<typeof RoomContext>;
}
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<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
class MessagePanel extends React.Component<IProps, IState> {
public static defaultProps = {
disableGrouping: false,
};
@@ -256,13 +253,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
private scrollPanel = createRef<ScrollPanel>();
private readonly showTypingNotificationsWatcherRef: string;
private eventTiles: Record<string, UnwrappedEventTile> = {};
private eventTiles: Record<string, React.ComponentRef<typeof EventTile>> = {};
// A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination.
public grouperKeyMap = new WeakMap<MatrixEvent, string>();
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
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<IProps, IState> {
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<IProps, IState> {
return this.eventTiles[eventId]?.ref?.current ?? undefined;
}
public getTileForEventId(eventId?: string): UnwrappedEventTile | undefined {
public getTileForEventId(eventId?: string): React.ComponentRef<typeof EventTile> | undefined {
if (!this.eventTiles || !eventId) {
return undefined;
}
@@ -454,7 +451,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
};
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<IProps, IState> {
// 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<IProps, IState> {
}
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<IProps, IState> {
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<IProps, IState> {
}
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<IProps, IState> {
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<IProps, IState> {
return receiptsByEvent;
}
private collectEventTile = (eventId: string, node: UnwrappedEventTile): void => {
private collectEventTile = (eventId: string, node: React.ComponentRef<typeof EventTile>): void => {
this.eventTiles[eventId] = node;
};
@@ -1033,7 +1044,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
if (
this.props.room &&
this.state.showTypingNotifications &&
this.context.timelineRenderingType === TimelineRenderingType.Room
this.props.context.timelineRenderingType === TimelineRenderingType.Room
) {
whoIsTyping = (
<WhoIsTypingTile
@@ -1053,7 +1064,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
}
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<IProps, IState> {
}
}
export default forwardRef<MessagePanel, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <MessagePanel {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));
/**
* 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 {

View File

@@ -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<typeof RoomContext>;
}
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<IProps, IState> {
public static contextType = RoomContext;
class NotificationPanel extends React.PureComponent<IProps, IState> {
private card = React.createRef<HTMLDivElement>();
public constructor(props: IProps) {
@@ -86,7 +85,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
return (
<RoomContext.Provider
value={{
...this.context,
...this.props.context,
timelineRenderingType: TimelineRenderingType.Notification,
narrow: this.state.narrow,
}}
@@ -114,3 +113,9 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
);
}
}
export default forwardRef<NotificationPanel, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <NotificationPanel {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -323,7 +323,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
<RoomHeader room={room} />
) : (
<LegacyRoomHeader
room={context.room}
room={context.room!}
searchInfo={undefined}
inRoom={true}
onSearchClick={null}
@@ -410,7 +410,7 @@ class RoomView extends React.Component<IRoomProps, IRoomState> {
private roomView = createRef<HTMLDivElement>();
private searchResultsPanel = createRef<ScrollPanel>();
private messagePanel: TimelinePanel | null = null;
private messagePanel: React.ComponentRef<typeof TimelinePanel> | null = null;
private roomViewBody = createRef<HTMLDivElement>();
public constructor(props: IRoomProps) {
@@ -1958,7 +1958,7 @@ class RoomView extends React.Component<IRoomProps, IRoomState> {
* 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<typeof TimelinePanel> | undefined;
if (this.searchResultsPanel.current) {
panel = this.searchResultsPanel.current;
} else if (this.messagePanel) {
@@ -1980,7 +1980,7 @@ class RoomView extends React.Component<IRoomProps, IRoomState> {
// 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<typeof TimelinePanel> | null): void => {
this.messagePanel = r;
};

View File

@@ -230,7 +230,7 @@ const EmptyThread: React.FC<EmptyThreadIProps> = ({ hasThreads, filterOption, sh
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => {
const mxClient = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext);
const timelinePanel = useRef<TimelinePanel | null>(null);
const timelinePanel = useRef<React.ComponentRef<typeof TimelinePanel> | null>(null);
const card = useRef<HTMLDivElement | null>(null);
const closeButonRef = useRef<HTMLDivElement | null>(null);

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -81,13 +82,10 @@ interface IState {
narrow: boolean;
}
export default class ThreadView extends React.Component<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
class ThreadView extends React.Component<IProps, IState> {
private dispatcherRef: string | null = null;
private readonly layoutWatcherRef: string;
private timelinePanel = createRef<TimelinePanel>();
private timelinePanel = createRef<React.ComponentRef<typeof TimelinePanel>>();
private card = createRef<HTMLDivElement>();
// Set by setEventId in ctor.
@@ -398,12 +396,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
<TimelinePanel
key={this.state.thread.id}
ref={this.timelinePanel}
showReadReceipts={this.context.showReadReceipts}
showReadReceipts={this.props.context.showReadReceipts}
manageReadReceipts={true}
manageReadMarkers={true}
sendReadReceiptOnLoad={true}
timelineSet={this.state.thread.timelineSet}
showUrlPreview={this.context.showUrlPreview}
showUrlPreview={this.props.context.showUrlPreview}
// ThreadView doesn't support IRC layout at this time
layout={this.state.layout === Layout.Bubble ? Layout.Bubble : Layout.Group}
hideThreadedMessages={false}
@@ -474,3 +472,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
);
}
}
export default forwardRef<ThreadView, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <ThreadView {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -244,10 +245,7 @@ interface IEventIndexOpts {
*
* Also responsible for handling and sending read receipts.
*/
class TimelinePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
export class TimelinePanel extends React.Component<IProps, IState> {
// a map from room id to read marker event timestamp
public static roomReadMarkerTsMap: Record<string, number> = {};
@@ -264,7 +262,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
private lastRRSentEventId: string | null | undefined = undefined;
private lastRMSentEventId: string | null | undefined = undefined;
private readonly messagePanel = createRef<MessagePanel>();
private readonly messagePanel = createRef<React.ComponentRef<typeof MessagePanel>>();
private readonly dispatcherRef: string;
private timelineWindow?: TimelineWindow;
private overlayTimelineWindow?: TimelineWindow;
@@ -276,9 +274,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
private initialReadMarkerId: string | null = null;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context);
this.context = context;
public constructor(props: IProps) {
super(props);
debuglog("mounting");
@@ -500,7 +497,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
}
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
/* 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<TimelinePanel, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <TimelinePanel {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<Props> = ({ roomView, resize
<RoomHeader room={context.room!} />
) : (
<LegacyRoomHeader
room={context.room}
room={context.room!}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
@@ -77,7 +77,7 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
subtitle={_t("room|waiting_for_join_subtitle", { brand })}
/>
<NewRoomIntro />
<UnwrappedEventTile mxEvent={inviteEvent} />
<EventTile mxEvent={inviteEvent} />
</ScrollPanel>
</div>
</main>

View File

@@ -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<typeof MessagePanel>, _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<typeof MessagePanel>,
public readonly firstEventAndShouldShow: WrappedEvent,
public readonly prevEvent: MatrixEvent | null,
public readonly lastShownEvent: MatrixEvent | undefined,

View File

@@ -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<typeof MessagePanel>,
{ event }: WrappedEvent,
): boolean {
return event.getType() === EventType.RoomCreate;
};

View File

@@ -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<typeof MessagePanel>,
{ 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<typeof MessagePanel>,
public readonly firstEventAndShouldShow: WrappedEvent,
public readonly prevEvent: MatrixEvent | null,
public readonly lastShownEvent: MatrixEvent | undefined,

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -131,11 +132,8 @@ interface IState {
reactionPickerDisplayed: boolean;
}
export default class MessageContextMenu extends React.Component<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
private reactButtonRef = createRef<any>(); // XXX Ref to a functional component
class MessageContextMenu extends React.Component<IProps, IState> {
private reactButtonRef = createRef<HTMLElement>();
public constructor(props: IProps) {
super(props);
@@ -315,7 +313,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
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<IProps, IState>
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<IProps, IState>
);
}
}
export default forwardRef<MessageContextMenu, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <MessageContextMenu {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -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<ComponentProps<typeof GenericEventListSummary>, "s
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
// The currently selected layout
layout: Layout;
layout?: Layout;
context: React.ContextType<typeof RoomContext>;
}
interface IUserEvents {
@@ -77,12 +78,9 @@ enum TransitionType {
const SEP = ",";
export default class EventListSummary extends React.Component<
class EventListSummary extends React.Component<
IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>
> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
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<EventListSummary, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <EventListSummary {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -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<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
private unmounted = false;
private room: Room;
private blockquoteRef = React.createRef<HTMLQuoteElement>();
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context);
public constructor(props: IProps) {
super(props);
this.state = {
events: [],

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -36,11 +37,8 @@ interface IState {
}
class ReactionPicker extends React.Component<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
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<IProps, IState> {
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<FocusComposerPayload>({
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<IProps, IState> {
dis.dispatch({ action: "message_sent" });
dis.dispatch<FocusComposerPayload>({
action: Action.FocusAComposer,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
return true;
}
@@ -123,7 +121,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
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<IProps, IState> {
}
}
export default ReactionPicker;
export default forwardRef<ReactionPicker, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <ReactionPicker {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -163,7 +163,7 @@ interface CallEventProps {
/**
* An event tile representing an active or historical Element call.
*/
export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
export const CallEvent = forwardRef<HTMLDivElement, CallEventProps>(({ mxEvent }, ref) => {
const client = useContext(MatrixClientContext);
const call = useCall(mxEvent.getRoomId()!);
const latestEvent = client

View File

@@ -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<any> | LegacyRef<any>;
// 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;

View File

@@ -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<typeof RoomContext>;
}
interface IState {
error?: boolean;
playback?: Playback;
}
export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
public constructor(props: IBodyProps) {
export class MAudioBody extends React.PureComponent<Props, IState> {
public constructor(props: Props) {
super(props);
this.state = {};
@@ -88,9 +89,9 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
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<IBodyProps, IState>
);
}
}
export default forwardRef<MAudioBody, Omit<Props, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <MAudioBody {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
interface IState {
decryptedBlob?: Blob;
}
export default class MFileBody extends React.Component<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
class MFileBody extends React.Component<IProps, IState> {
public static defaultProps = {
showGenericPlaceholder: true,
};
@@ -226,11 +224,11 @@ export default class MFileBody extends React.Component<IProps, IState> {
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<IProps, IState> {
<span className="mx_MFileBody_download_icon" />
{_t("timeline|m.file|download_label", { text: this.linkText })}
</a>
{this.context.timelineRenderingType === TimelineRenderingType.File && (
{this.props.context.timelineRenderingType === TimelineRenderingType.File && (
<div className="mx_MImageBody_size">
{this.content.info?.size ? fileSize(this.content.info.size) : ""}
</div>
@@ -368,3 +366,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
}
}
}
export default forwardRef<MFileBody, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <MFileBody {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
interface IState {
contentUrl: string | null;
thumbUrl: string | null;
@@ -63,17 +67,14 @@ interface IState {
placeholder: Placeholder;
}
export default class MImageBody extends React.Component<IBodyProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
export class MImageBody extends React.Component<Props, IState> {
private unmounted = true;
private image = createRef<HTMLImageElement>();
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<IBodyProps, IState> {
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<IBodyProps, IState> {
* 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 <MFileBody {...this.props} showGenericPlaceholder={false} />;
}
@@ -648,6 +651,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
}
export default forwardRef<MImageBody, Omit<Props, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <MImageBody {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));
interface PlaceholderIProps {
hover?: boolean;
maxWidth?: number;

View File

@@ -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<typeof RoomContext>;
}
class MImageReplyBody extends MImageBody {
public onClick = (ev: React.MouseEvent): void => {
ev.preventDefault();
};
@@ -43,3 +49,9 @@ export default class MImageReplyBody extends MImageBody {
return <div className="mx_MImageReplyBody">{thumbnail}</div>;
}
}
export default forwardRef<MImageReplyBody, Omit<Props, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <MImageReplyBody {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
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<MStickerBody, Omit<Props, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <MStickerBody {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
interface IState {
decryptedUrl: string | null;
decryptedThumbnailUrl: string | null;
@@ -40,14 +44,11 @@ interface IState {
blurhashUrl: string | null;
}
export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
class MVideoBody extends React.PureComponent<Props, IState> {
private videoRef = React.createRef<HTMLVideoElement>();
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<IBodyProps, IState>
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<IBodyProps, IState>
);
}
}
export default forwardRef<MVideoBody, Omit<Props, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <MVideoBody {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
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<MVoiceMessageBody, Omit<Props, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <MVoiceMessageBody {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
public static contextType = RoomContext;
class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
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<IMessageAction
dis.dispatch({
action: "reply_to_event",
event: this.props.mxEvent,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
};
@@ -326,7 +325,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
editEvent(
MatrixClientPeg.safeGet(),
this.props.mxEvent,
this.context.timelineRenderingType,
this.props.context.timelineRenderingType,
this.props.getRelationsForEvent,
);
};
@@ -334,7 +333,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
private readonly forbiddenThreadHeadMsgType = [MsgType.KeyVerificationRequest];
private get showReplyInThreadAction(): boolean {
const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread;
const inNotThreadTimeline = this.props.context.timelineRenderingType !== TimelineRenderingType.Thread;
const isAllowedMessageType =
!this.forbiddenThreadHeadMsgType.includes(this.props.mxEvent.getContent().msgtype as MsgType) &&
@@ -448,7 +447,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
// The only catch is we do the reply button first so that we can make sure the react
// button is the very first button without having to do length checks for `splice()`.
if (this.context.canSendMessages) {
if (this.props.context.canSendMessages) {
if (this.showReplyInThreadAction) {
toolbarOpts.splice(0, 0, threadTooltipButton);
}
@@ -467,7 +466,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
);
}
// We hide the react button in search results as we don't show reactions in results
if (this.context.canReact && !this.context.search) {
if (this.props.context.canReact && !this.props.context.search) {
toolbarOpts.splice(
0,
0,
@@ -494,7 +493,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
}
} else if (
// Show thread icon even for deleted messages, but only within main timeline
this.context.timelineRenderingType === TimelineRenderingType.Room &&
this.props.context.timelineRenderingType === TimelineRenderingType.Room &&
this.props.mxEvent.getThread()
) {
toolbarOpts.unshift(threadTooltipButton);
@@ -560,3 +559,9 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
);
}
}
export default forwardRef<MessageActionBar, Omit<IMessageActionBarProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <MessageActionBar {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -51,8 +51,8 @@ import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoStat
// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
overrideBodyTypes?: Record<string, typeof React.Component>;
overrideEventTypes?: Record<string, typeof React.Component>;
overrideBodyTypes?: Record<string, ComponentType>;
overrideEventTypes?: Record<string, ComponentType>;
// helper function to access relations for this event
getRelationsForEvent?: GetRelationsForEvent;
@@ -64,7 +64,9 @@ export interface IOperableEventTile {
getEventTileOps(): IEventTileOps | null;
}
const baseBodyTypes = new Map<string, typeof React.Component>([
type ComponentType = React.ComponentType<IBodyProps & React.RefAttributes<any>>;
const baseBodyTypes = new Map<string, ComponentType>([
[MsgType.Text, TextualBody],
[MsgType.Notice, TextualBody],
[MsgType.Emote, TextualBody],
@@ -73,7 +75,7 @@ const baseBodyTypes = new Map<string, typeof React.Component>([
[MsgType.Audio, MVoiceOrAudioBody],
[MsgType.Video, MVideoBody],
]);
const baseEvTypes = new Map<string, React.ComponentType<IBodyProps>>([
const baseEvTypes = new Map<string, ComponentType>([
[EventType.Sticker, MStickerBody],
[M_POLL_START.name, MPollBody],
[M_POLL_START.altName, MPollBody],
@@ -84,10 +86,10 @@ const baseEvTypes = new Map<string, React.ComponentType<IBodyProps>>([
]);
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
private body: React.RefObject<ComponentType | IOperableEventTile> = createRef();
private mediaHelper?: MediaEventHelper;
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries());
private bodyTypes = new Map<string, ComponentType>(baseBodyTypes.entries());
private evTypes = new Map<string, ComponentType>(baseEvTypes.entries());
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
@@ -121,12 +123,12 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
}
private updateComponentMaps(): void {
this.bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
this.bodyTypes = new Map<string, ComponentType>(baseBodyTypes.entries());
for (const [bodyType, bodyComponent] of Object.entries(this.props.overrideBodyTypes ?? {})) {
this.bodyTypes.set(bodyType, bodyComponent);
}
this.evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries());
this.evTypes = new Map<string, ComponentType>(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<IProps> implements IMe
const content = this.props.mxEvent.getContent();
const type = this.props.mxEvent.getType();
const msgtype = content.msgtype;
let BodyType: React.ComponentType<IBodyProps> = RedactedBody;
let BodyType: ComponentType = RedactedBody;
if (!this.props.mxEvent.isRedacted()) {
// only resolve BodyType if event is not redacted
if (this.props.mxEvent.isDecryptionFailure()) {

View File

@@ -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<IProps> = ({ mxEvent, reactions }) => {
const ReactButton: React.FC<Omit<IProps, "context">> = ({ 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<typeof RoomContext>;
}
interface IState {
@@ -81,13 +82,9 @@ interface IState {
showAll: boolean;
}
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context);
this.context = context;
class ReactionsRow extends React.PureComponent<IProps, IState> {
public constructor(props: IProps) {
super(props);
this.state = {
myReactions: this.getMyReactions(),
@@ -151,7 +148,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
}
let addReactionButton: JSX.Element | undefined;
if (this.context.canReact) {
if (this.props.context.canReact) {
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
}
@@ -239,3 +236,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
);
}
}
export default forwardRef<ReactionsRow, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <ReactionsRow {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
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<IBodyProps, IState> {
class TextualBody extends React.Component<Props, IState> {
private readonly contentRef = createRef<HTMLSpanElement>();
private unmounted = false;
private pills: Element[] = [];
private tooltips: Element[] = [];
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
public constructor(props: IBodyProps) {
public constructor(props: Props) {
super(props);
this.state = {
@@ -429,7 +430,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
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<IBodyProps, IState> {
);
}
}
export default forwardRef<TextualBody, Omit<Props, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <TextualBody {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
export default class TextualEvent extends React.Component<IProps> {
public static contextType = RoomContext;
class TextualEvent extends React.Component<IProps> {
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 <div className="mx_TextualEvent">{text}</div>;
}
}
export default forwardRef<TextualEvent, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <TextualEvent {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -75,12 +76,10 @@ interface IState {
showReadReceipts?: boolean;
}
export default class TimelineCard extends React.Component<IProps, IState> {
public static contextType = RoomContext;
class TimelineCard extends React.Component<IProps, IState> {
private dispatcherRef?: string;
private layoutWatcherRef?: string;
private timelinePanel = React.createRef<TimelinePanel>();
private timelinePanel = React.createRef<React.ComponentRef<typeof TimelinePanel>>();
private card = React.createRef<HTMLDivElement>();
private readReceiptsSettingWatcher: string | undefined;
@@ -224,7 +223,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
<RoomContext.Provider
value={{
...this.context,
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
timelineRenderingType: this.props.timelineRenderingType ?? this.props.context.timelineRenderingType,
liveTimeline: this.props.timelineSet?.getLiveTimeline(),
narrow: this.state.narrow,
}}
@@ -246,7 +245,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
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<IProps, IState> {
);
}
}
export default forwardRef<TimelineCard, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <TimelineCard {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -49,14 +50,12 @@ interface IState {
forceComplete: boolean;
}
export default class Autocomplete extends React.PureComponent<IProps, IState> {
export class Autocomplete extends React.PureComponent<IProps, IState> {
public autocompleter?: Autocompleter;
public queryRequested?: string;
public debounceCompletionsRequest?: number;
private containerRef = createRef<HTMLDivElement>();
public static contextType = RoomContext;
public constructor(props: IProps) {
super(props);
@@ -80,7 +79,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
}
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<IProps, IState> {
) : null;
}
}
export default forwardRef<Autocomplete, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <Autocomplete {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -123,7 +123,7 @@ interface IState {
export default class BasicMessageEditor extends React.Component<IProps, IState> {
public readonly editorRef = createRef<HTMLDivElement>();
private autocompleteRef = createRef<Autocomplete>();
private autocompleteRef = createRef<React.ComponentRef<typeof Autocomplete>>();
private formatBarRef = createRef<MessageComposerFormatBar>();
private modifiedFlag = false;

View File

@@ -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<typeof RoomContext>;
}
interface IState {
saveDisabled: boolean;
}
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
private readonly editorRef = createRef<BasicMessageComposer>();
private readonly dispatcherRef: string;
private readonly replyToEvent?: MatrixEvent;
private model!: EditorModel;
public constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
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<IEditMessageComposerProps, ISt
}
private getRoom(): Room {
if (!this.context.room) {
if (!this.props.context.room) {
throw new Error(`Cannot render without room`);
}
return this.context.room;
return this.props.context.room;
}
private onKeyDown = (event: KeyboardEvent): void => {
@@ -191,7 +188,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
dis.dispatch({
action: Action.EditEvent,
event: previousEvent,
timelineRenderingType: this.context.timelineRenderingType,
timelineRenderingType: this.props.context.timelineRenderingType,
});
event.preventDefault();
}
@@ -211,7 +208,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
dis.dispatch({
action: Action.EditEvent,
event: nextEvent,
timelineRenderingType: this.context.timelineRenderingType,
timelineRenderingType: this.props.context.timelineRenderingType,
});
} else {
this.cancelEdit();
@@ -230,16 +227,16 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
dis.dispatch({
action: Action.EditEvent,
event: null,
timelineRenderingType: this.context.timelineRenderingType,
timelineRenderingType: this.props.context.timelineRenderingType,
});
dis.dispatch({
action: Action.FocusSendMessageComposer,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
}
private get editorRoomKey(): string {
return editorRoomKey(this.props.editState.getEvent().getRoomId()!, this.context.timelineRenderingType);
return editorRoomKey(this.props.editState.getEvent().getRoomId()!, this.props.context.timelineRenderingType);
}
private get editorStateKey(): string {
@@ -247,7 +244,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
}
private get events(): MatrixEvent[] {
const liveTimelineEvents = this.context.liveTimeline?.getEvents();
const liveTimelineEvents = this.props.context.liveTimeline?.getEvents();
const room = this.getRoom();
if (!liveTimelineEvents || !room) return [];
const pendingEvents = room.getPendingEvents();
@@ -368,7 +365,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
// re-focus the composer after QuestionDialog is closed
dis.dispatch({
action: Action.FocusAComposer,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
// if !sendAnyway bail to let the user edit the composer and try again
if (!sendAnyway) return;
@@ -462,7 +459,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
if (!this.editorRef.current) return;
if (payload.action === Action.ComposerInsert) {
if (payload.timelineRenderingType !== this.context.timelineRenderingType) return;
if (payload.timelineRenderingType !== this.props.context.timelineRenderingType) return;
if (payload.composerType !== ComposerType.Edit) return;
if (payload.userId) {
@@ -506,4 +503,10 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
}
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
export default EditMessageComposerWithMatrixClient;
export default forwardRef<EditMessageComposer, Omit<IEditMessageComposerProps, "context" | "mxClient">>(
(props, ref) => (
<RoomContext.Consumer>
{(context) => <EditMessageComposerWithMatrixClient {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
),
);

View File

@@ -266,6 +266,10 @@ interface IState {
threadNotification?: NotificationCountType;
}
interface Props extends EventTileProps {
context: React.ContextType<typeof RoomContext>;
}
/**
* 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<EventTileProps, IState> {
class UnwrappedEventTile extends React.Component<Props, IState> {
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
private tile = createRef<IEventTileType>();
@@ -297,13 +301,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
layout: Layout.Group,
};
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
private unmounted = false;
public constructor(props: EventTileProps, context: React.ContextType<typeof RoomContext>) {
super(props, context);
public constructor(props: Props) {
super(props);
const thread = this.thread;
@@ -508,7 +509,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
);
}
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 (
<a className="mx_ThreadSummary_icon" href={this.props.highlightLink}>
@@ -676,8 +680,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
*/
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<EventTileProps, IState>
dis.dispatch<ComposerInsertPayload>({
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<EventTileProps, IState>
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<EventTileProps, IState>
} = 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<EventTileProps, IState>
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<EventTileProps, IState>
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<EventTileProps, IState>
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<EventTileProps, IState>
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<EventTileProps, IState>
const viewUserOnClick =
!this.props.inhibitInteraction &&
![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes(
this.context.timelineRenderingType,
this.props.context.timelineRenderingType,
);
avatar = (
<div className="mx_EventTile_avatar">
@@ -1074,13 +1079,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
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 = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} />;
} else if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) {
} else if (this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList) {
sender = <SenderProfile mxEvent={this.props.mxEvent} withTooltip />;
} else {
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
@@ -1113,7 +1118,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
// 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<EventTileProps, IState>
const messageTimestamp = (
<MessageTimestamp
showRelative={this.context.timelineRenderingType === TimelineRenderingType.ThreadsList}
showRelative={this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList}
showTwelveHour={this.props.isTwelveHour}
ts={ts}
receivedTs={getLateEventInfo(this.props.mxEvent)?.received_ts}
@@ -1180,7 +1185,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
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<EventTileProps, IState>
// 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<EventTileProps, IState>
onHeightChanged: () => this.props.onHeightChanged,
permalinkCreator: this.props.permalinkCreator!,
},
this.context.showHiddenEvents,
this.props.context.showHiddenEvents,
)}
{actionBar}
<a href={permalink} onClick={this.onPermalinkClicked}>
@@ -1268,7 +1273,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
"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<EventTileProps, IState>
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<EventTileProps, IState>
</div>
{this.renderThreadPanelSummary()}
</div>
{this.context.timelineRenderingType === TimelineRenderingType.ThreadsList && (
{this.props.context.timelineRenderingType === TimelineRenderingType.ThreadsList && (
<EventTileThreadToolbar
viewInRoom={this.viewInRoom}
copyLinkToThread={this.copyLinkToThread}
@@ -1371,7 +1376,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
onHeightChanged: this.props.onHeightChanged,
permalinkCreator: this.props.permalinkCreator,
},
this.context.showHiddenEvents,
this.props.context.showHiddenEvents,
)}
</div>,
<a
@@ -1419,7 +1424,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{groupPadlock}
{replyChain}
{renderTile(
this.context.timelineRenderingType,
this.props.context.timelineRenderingType,
{
...this.props,
@@ -1434,7 +1439,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
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<UnwrappedEventTile, EventTileProps>((props, ref
return (
<>
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
<UnwrappedEventTile ref={ref} {...props} />
<RoomContext.Consumer>
{(context) => <UnwrappedEventTile {...props} ref={ref} context={context} />}
</RoomContext.Consumer>
</TileErrorBoundary>
</>
);

View File

@@ -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<RightPanelPhases>;
@@ -487,6 +487,7 @@ export interface IProps {
viewingCall: boolean;
activeCall: Call | null;
additionalButtons?: ViewRoomOpts["buttons"];
context: React.ContextType<typeof RoomContext>;
}
interface IState {
@@ -498,7 +499,7 @@ interface IState {
/**
* @deprecated use `src/components/views/rooms/RoomHeader.tsx` instead
*/
export default class RoomHeader extends React.Component<IProps, IState> {
class LegacyRoomHeader extends React.Component<IProps, IState> {
public static defaultProps: Partial<IProps> = {
inRoom: false,
excludedRightPanelPhaseButtons: [],
@@ -506,13 +507,11 @@ export default class RoomHeader extends React.Component<IProps, IState> {
enableRoomOptionsMenu: true,
};
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
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<IProps, IState> {
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(<CallButtons key="calls" room={this.props.room} />);
}
@@ -866,3 +865,9 @@ export default class RoomHeader extends React.Component<IProps, IState> {
);
}
}
export default forwardRef<LegacyRoomHeader, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <LegacyRoomHeader {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
interface IState {
@@ -113,16 +114,13 @@ interface IState {
export class MessageComposer extends React.Component<IProps, IState> {
private tooltipId = `mx_MessageComposer_${Math.random()}`;
private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
private messageComposerInput = createRef<React.ComponentRef<typeof SendMessageComposer>>();
private voiceRecordingButton = createRef<React.ComponentRef<typeof VoiceRecordComposerTile>>();
private ref: React.RefObject<HTMLDivElement> = createRef();
private instanceId: number;
private _voiceRecording: Optional<VoiceMessageRecording>;
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
public static defaultProps = {
compact: false,
showVoiceBroadcastButton: false,
@@ -189,7 +187,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<ViewRoomPayload>({
@@ -324,7 +322,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
dis.dispatch<ComposerInsertPayload>({
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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 ? (
<a
@@ -667,4 +665,8 @@ export class MessageComposer extends React.Component<IProps, IState> {
}
const MessageComposerWithMatrixClient = withMatrixClientHOC(MessageComposer);
export default MessageComposerWithMatrixClient;
export default forwardRef<MessageComposer, Omit<IProps, "context" | "mxClient">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <MessageComposerWithMatrixClient {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
export default class ReplyPreview extends React.Component<IProps> {
public static contextType = RoomContext;
class ReplyPreview extends React.Component<IProps> {
public render(): JSX.Element | null {
if (!this.props.replyToEvent) return null;
@@ -50,7 +49,7 @@ export default class ReplyPreview extends React.Component<IProps> {
<span>{_t("composer|replying_title")}</span>
<AccessibleButton
className="mx_ReplyPreview_header_cancel"
onClick={() => cancelQuoting(this.context.timelineRenderingType)}
onClick={() => cancelQuoting(this.props.context.timelineRenderingType)}
/>
</div>
<ReplyTile mxEvent={this.props.replyToEvent} permalinkCreator={this.props.permalinkCreator} />
@@ -59,3 +58,7 @@ export default class ReplyPreview extends React.Component<IProps> {
);
}
}
export default forwardRef<ReplyPreview, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>{(context) => <ReplyPreview {...props} context={context} ref={ref} />}</RoomContext.Consumer>
));

View File

@@ -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<IProps> {
);
}
const msgtypeOverrides: Record<string, typeof React.Component> = {
const msgtypeOverrides: Record<string, React.ComponentType<EventTileTypeProps>> = {
[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<string, typeof React.Component> = {
const evOverrides: Record<string, React.ComponentType<EventTileTypeProps>> = {
// Use MImageReplyBody so that the sticker isn't taking up a lot of space
[EventType.Sticker]: MImageReplyBody,
};

View File

@@ -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<typeof RoomContext>;
}
export default class SearchResultTile extends React.Component<IProps> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
class SearchResultTile extends React.Component<IProps> {
// A map of <callId, LegacyCallEventGrouper>
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
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<IProps> {
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<IProps> {
prevEv,
mxEv,
cli,
this.context?.showHiddenEvents,
this.props.context?.showHiddenEvents,
TimelineRenderingType.Search,
);
@@ -108,7 +106,7 @@ export default class SearchResultTile extends React.Component<IProps> {
mxEv,
nextEv,
cli,
this.context?.showHiddenEvents,
this.props.context?.showHiddenEvents,
TimelineRenderingType.Search,
);
}
@@ -140,3 +138,9 @@ export default class SearchResultTile extends React.Component<IProps> {
);
}
}
export default forwardRef<SearchResultTile, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <SearchResultTile {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -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<typeof RoomContext>;
}
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
private readonly editorRef = createRef<BasicMessageComposer>();
private model: EditorModel;
@@ -267,9 +265,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
includeReplyLegacyFallback: true,
};
public constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
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<ISendMessageComposerPro
case KeyBindingAction.EditPrevMessage:
// selection must be collapsed and caret at start
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
const events = this.context.liveTimeline
const events = this.props.context.liveTimeline
?.getEvents()
.concat(replyingToThread ? [] : this.props.room.getPendingEvents());
const editEvent = events
@@ -352,17 +349,17 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
dis.dispatch({
action: Action.EditEvent,
event: editEvent,
timelineRenderingType: this.context.timelineRenderingType,
timelineRenderingType: this.props.context.timelineRenderingType,
});
}
}
break;
case KeyBindingAction.CancelReplyOrEdit:
if (!!this.context.replyToEvent) {
if (!!this.props.context.replyToEvent) {
dis.dispatch({
action: "reply_to_event",
event: null,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
event.preventDefault();
event.stopPropagation();
@@ -395,7 +392,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
dis.dispatch({
action: "reply_to_event",
event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
if (parts) {
this.model.reset(parts);
@@ -405,7 +402,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
}
private sendQuickReaction(): void {
const timeline = this.context.liveTimeline;
const timeline = this.props.context.liveTimeline;
if (!timeline) return;
const events = timeline.getEvents();
const reaction = this.model.parts[1].text;
@@ -518,7 +515,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
// re-focus the composer after QuestionDialog is closed
dis.dispatch({
action: Action.FocusAComposer,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
// if !sendAnyway bail to let the user edit the composer and try again
if (!sendAnyway) return;
@@ -563,7 +560,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
dis.dispatch({
action: "reply_to_event",
event: null,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
}
dis.dispatch({ action: "message_sent" });
@@ -593,7 +590,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
if (shouldSend && SettingsStore.getValue("scrollToBottomOnMessageSent")) {
dis.dispatch({
action: "scroll_to_bottom",
timelineRenderingType: this.context.timelineRenderingType,
timelineRenderingType: this.props.context.timelineRenderingType,
});
}
}
@@ -631,7 +628,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
dis.dispatch({
action: "reply_to_event",
event: this.props.room.findEventById(replyEventId),
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
}
return parts;
@@ -665,12 +662,12 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
switch (payload.action) {
case "reply_to_event":
case Action.FocusSendMessageComposer:
if ((payload.context ?? TimelineRenderingType.Room) === this.context.timelineRenderingType) {
if ((payload.context ?? TimelineRenderingType.Room) === this.props.context.timelineRenderingType) {
this.editorRef.current?.focus();
}
break;
case Action.ComposerInsert:
if (payload.timelineRenderingType !== this.context.timelineRenderingType) break;
if (payload.timelineRenderingType !== this.props.context.timelineRenderingType) break;
if (payload.composerType !== ComposerType.Send) break;
if (payload.userId) {
@@ -695,7 +692,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
this.props.room.roomId,
this.props.relation,
this.props.mxClient,
this.context.timelineRenderingType,
this.props.context.timelineRenderingType,
);
return true; // to skip internal onPaste handler
}
@@ -734,7 +731,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
this.props.room.roomId,
this.props.relation,
this.props.mxClient,
this.context.replyToEvent,
this.props.context.replyToEvent,
);
},
(error) => {
@@ -789,4 +786,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
}
const SendMessageComposerWithMatrixClient = withMatrixClientHOC(SendMessageComposer);
export default SendMessageComposerWithMatrixClient;
export default forwardRef<SendMessageComposer, Omit<ISendMessageComposerProps, "context" | "mxClient">>(
(props, ref) => (
<RoomContext.Consumer>
{(context) => <SendMessageComposerWithMatrixClient {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
),
);

View File

@@ -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<typeof RoomContext>;
}
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<IProps, IState> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
private voiceRecordingId: string;
public constructor(props: IProps) {
@@ -141,7 +140,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
defaultDispatcher.dispatch({
action: "reply_to_event",
event: null,
context: this.context.timelineRenderingType,
context: this.props.context.timelineRenderingType,
});
}
@@ -323,3 +322,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
);
}
}
export default forwardRef<VoiceRecordComposerTile, Omit<IProps, "context">>((props, ref) => (
<RoomContext.Consumer>
{(context) => <VoiceRecordComposerTile {...props} context={context} ref={ref} />}
</RoomContext.Consumer>
));

View File

@@ -59,7 +59,7 @@ interface WysiwygAutocompleteProps {
const WysiwygAutocomplete = forwardRef(
(
{ suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps,
ref: ForwardedRef<Autocomplete>,
ref: ForwardedRef<React.ComponentRef<typeof Autocomplete>>,
): JSX.Element | null => {
const { room } = useRoomContext();
const client = useMatrixClientContext();

View File

@@ -58,7 +58,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
eventRelation,
}: WysiwygComposerProps) {
const { room } = useRoomContext();
const autocompleteRef = useRef<Autocomplete | null>(null);
const autocompleteRef = useRef<React.ComponentRef<typeof Autocomplete> | null>(null);
const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation);
const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({

View File

@@ -37,7 +37,7 @@ import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsCli
export function useInputEventProcessor(
onSend: () => void,
autocompleteRef: React.RefObject<Autocomplete>,
autocompleteRef: React.RefObject<React.ComponentRef<typeof Autocomplete>>,
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<Autocomplete>,
autocompleteRef: React.RefObject<React.ComponentRef<typeof Autocomplete>>,
): KeyboardEvent | null {
const { editorStateTransfer } = composerContext;
const isEditing = Boolean(editorStateTransfer);

View File

@@ -55,7 +55,7 @@ export function usePlainTextListeners(
eventRelation?: IEventRelation,
): {
ref: RefObject<HTMLDivElement>;
autocompleteRef: React.RefObject<Autocomplete>;
autocompleteRef: React.RefObject<React.ComponentRef<typeof Autocomplete>>;
content?: string;
onBeforeInput(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>): void;
onInput(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>): void;
@@ -72,7 +72,7 @@ export function usePlainTextListeners(
const mxClient = useMatrixClientContext();
const ref = useRef<HTMLDivElement | null>(null);
const autocompleteRef = useRef<Autocomplete | null>(null);
const autocompleteRef = useRef<React.ComponentRef<typeof Autocomplete> | null>(null);
const [content, setContent] = useState<string | undefined>(initialContent);
const send = useCallback(() => {

View File

@@ -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<Autocomplete>,
autocompleteRef: RefObject<React.ComponentRef<typeof Autocomplete>>,
// we get a React Keyboard event from plain text composer, a Keyboard Event from the rich text composer
event: KeyboardEvent | React.KeyboardEvent<HTMLDivElement>,
): boolean {

View File

@@ -28,7 +28,7 @@ export interface ICallback {
}
export type UpdateCallback = (data: ICallback) => void;
export type GetAutocompleterComponent = () => Autocomplete | null;
export type GetAutocompleterComponent = () => React.ComponentRef<typeof Autocomplete> | null;
export type UpdateQuery = (test: string) => Promise<void>;
export default class AutocompleteWrapperModel {

View File

@@ -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>; // `any` because it's effectively impossible to convince TS of a reasonable type
timestamp?: JSX.Element;
maxImageHeight?: number; // pixels
overrideBodyTypes?: Record<string, typeof React.Component>;
overrideEventTypes?: Record<string, typeof React.Component>;
overrideBodyTypes?: Record<string, React.ComponentType<IBodyProps>>;
overrideEventTypes?: Record<string, React.ComponentType<IBodyProps>>;
}
type FactoryProps = Omit<EventTileTypeProps, "ref">;
type FactoryProps = EventTileTypeProps;
type Factory<X = FactoryProps> = (ref: Optional<React.RefObject<any>>, props: X) => JSX.Element;
export const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;

View File

@@ -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<typeof TimelinePanel> => {
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<typeof TimelinePanel>;
const ev1 = new MatrixEvent({
event_id: "ev1",
@@ -203,7 +203,7 @@ describe("TimelinePanel", () => {
});
const renderTimelinePanel = async (): Promise<void> => {
const ref = createRef<TimelinePanel>();
const ref = createRef<React.ComponentRef<typeof TimelinePanel>>();
render(
<TimelinePanel
timelineSet={timelineSet}
@@ -239,7 +239,7 @@ describe("TimelinePanel", () => {
});
afterEach(() => {
TimelinePanel.roomReadMarkerTsMap = {};
TimelinePanelClass.roomReadMarkerTsMap = {};
});
it("when there is no event, it should not send any receipt", async () => {

View File

@@ -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<RoomHeaderProps> = {}, roomContext: Partial<IRoomState> = {}) => {
const renderHeader = (
props: Partial<React.ComponentProps<typeof RoomHeader>> = {},
roomContext: Partial<IRoomState> = {},
) => {
render(
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
<RoomHeader
@@ -843,7 +846,7 @@ function createRoom(info: IRoomCreationInfo) {
}
function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): RenderResult {
const props: RoomHeaderProps = {
const props: React.ComponentProps<typeof RoomHeader> = {
room,
inRoom: true,
onSearchClick: () => {},

View File

@@ -43,7 +43,7 @@ jest.mock("../../../../src/stores/VoiceRecordingStore", () => ({
}));
describe("<VoiceRecordComposerTile/>", () => {
let voiceRecordComposerTile: RefObject<VoiceRecordComposerTile>;
let voiceRecordComposerTile: RefObject<React.ComponentRef<typeof VoiceRecordComposerTile>>;
let mockRecorder: VoiceMessageRecording;
let mockUpload: IUpload;
let mockClient: MatrixClient;

View File

@@ -60,7 +60,7 @@ describe("WysiwygAutocomplete", () => {
jest.restoreAllMocks();
});
const autocompleteRef = createRef<Autocomplete>();
const autocompleteRef = createRef<React.ComponentRef<typeof Autocomplete>>();
const getCompletionsSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockResolvedValue([
{
completions: mockCompletion,