Use context provided RoomViewStore within the RoomView component hierarchy (#31077)

* Update ContentMessages.ts

Update ContentMessages.ts

* update PlaybackQueue.ts

* Update SpaceHierarchy.tsx

* Update ThreadView.tsx

* Update RoomCallBanner.tsx

* Update useRoomCall.tsx

* Update DateSeparator.tsx

* Update TimelineCard.tsx

* Update UserInfoBasicOptions

* Update slask-commands/utils.ts

* lint

* Update PlaybackQueue, MVoiceMessageBody and UserInfoBasicOptionsView tests.

* Update RoomHeader-test.tsx

* lint

* Add ts docs

* Update utils-test.tsx

* Update message-test.ts

* coverage

* lint

* Improve naming

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
David Langley
2025-10-29 09:40:21 +00:00
committed by GitHub
parent 209dfece21
commit ae2acdf311
38 changed files with 520 additions and 104 deletions

View File

@@ -1128,6 +1128,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
[payload.file],
roomId,
undefined,
this.state.replyToEvent,
this.context.client,
);
}
@@ -2047,6 +2048,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
Array.from(dataTransfer.files),
roomId,
undefined,
this.state.replyToEvent,
this.context.client,
TimelineRenderingType.Room,
);

View File

@@ -67,10 +67,11 @@ import { type JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomRea
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { getTopic } from "../../hooks/room/useTopic";
import { SdkContextClass } from "../../contexts/SDKContext";
import { getDisplayAliasForAliasSet } from "../../Rooms";
import SettingsStore from "../../settings/SettingsStore";
import { filterBoolean } from "../../utils/arrays.ts";
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
import RoomContext from "../../contexts/RoomContext.ts";
interface IProps {
space: Room;
@@ -404,7 +405,20 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
});
};
export const joinRoom = async (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): Promise<unknown> => {
/**
* Join a room.
* @param cli The Matrix client
* @param roomViewStore The RoomViewStore instance
* @param hierarchy The RoomHierarchy instance
* @param roomId The ID of the room to join
* @returns A promise that resolves when the room has been joined
*/
export const joinRoom = async (
cli: MatrixClient,
roomViewStore: RoomViewStore,
hierarchy: RoomHierarchy,
roomId: string,
): Promise<unknown> => {
// Don't let the user view a room they won't be able to either peek or join:
// fail earlier so they don't have to click back to the directory.
if (cli.isGuest()) {
@@ -418,10 +432,10 @@ export const joinRoom = async (cli: MatrixClient, hierarchy: RoomHierarchy, room
});
} catch (err: unknown) {
if (err instanceof MatrixError) {
SdkContextClass.instance.roomViewStore.showJoinRoomError(err, roomId);
roomViewStore.showJoinRoomError(err, roomId);
} else {
logger.warn("Got a non-MatrixError while joining room", err);
SdkContextClass.instance.roomViewStore.showJoinRoomError(
roomViewStore.showJoinRoomError(
new MatrixError({
error: _t("error|unknown"),
}),
@@ -761,6 +775,7 @@ const ManageButtons: React.FC<IManageButtonsProps> = ({ hierarchy, selected, set
const SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, additionalButtons }) => {
const cli = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext);
const [query, setQuery] = useState(initialText);
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
@@ -855,10 +870,10 @@ const SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, a
onJoinRoomClick={async (roomId, parents) => {
for (const parent of parents) {
if (cli.getRoom(parent)?.getMyMembership() !== KnownMembership.Join) {
await joinRoom(cli, hierarchy, parent);
await joinRoom(cli, roomContext.roomViewStore, hierarchy, parent);
}
}
await joinRoom(cli, hierarchy, roomId);
await joinRoom(cli, roomContext.roomViewStore, hierarchy, roomId);
}}
/>
</>

View File

@@ -49,7 +49,6 @@ import { type ButtonEvent } from "../views/elements/AccessibleButton";
import Spinner from "../views/elements/Spinner";
import { type ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
import Heading from "../views/typography/Heading";
import { SdkContextClass } from "../../contexts/SDKContext";
import { type ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
@@ -124,7 +123,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
const roomId = this.props.mxEvent.getRoomId();
SettingsStore.unwatchSetting(this.layoutWatcherRef);
const hasRoomChanged = SdkContextClass.instance.roomViewStore.getRoomId() !== roomId;
const hasRoomChanged = this.context.roomViewStore.getRoomId() !== roomId;
if (this.props.initialEvent && !hasRoomChanged) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@@ -334,6 +333,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
Array.from(dataTransfer.files),
roomId,
this.threadRelation,
this.context.replyToEvent,
MatrixClientPeg.safeGet(),
TimelineRenderingType.Thread,
);

View File

@@ -17,7 +17,6 @@ import PosthogTrackers from "../../../../PosthogTrackers";
import { ShareDialog } from "../../../views/dialogs/ShareDialog";
import { type ComposerInsertPayload } from "../../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../../dispatcher/actions";
import { SdkContextClass } from "../../../../contexts/SDKContext";
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
import MultiInviter from "../../../../utils/MultiInviter";
import { type ViewRoomPayload } from "../../../../dispatcher/payloads/ViewRoomPayload";
@@ -41,7 +40,7 @@ export interface UserInfoBasicOptionsState {
// Method called when a share user button is clicked, will display modal with profile to share
onShareUserClick: () => void;
// Method called when a invite button is clicked, will display modal to invite user
onInviteUserButton: (evt: Event) => Promise<void>;
onInviteUserButton: (fallbackRoomId: string, evt: Event) => Promise<void>;
// Method called when the DM button is clicked, will open a DM with the selected member
onOpenDmForUser: (member: Member) => Promise<void>;
}
@@ -91,12 +90,9 @@ export const useUserInfoBasicOptionsViewModel = (room: Room, member: User | Room
});
};
const onInviteUserButton = async (ev: Event): Promise<void> => {
const onInviteUserButton = async (fallbackRoomId: string, ev: Event): Promise<void> => {
try {
const roomId =
member instanceof RoomMember && member.roomId
? member.roomId
: SdkContextClass.instance.roomViewStore.getRoomId();
const roomId = member instanceof RoomMember && member.roomId ? member.roomId : fallbackRoomId;
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
const inviter = new MultiInviter(cli, roomId || "");

View File

@@ -20,7 +20,7 @@ import { useCall } from "../../../hooks/useCall";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
import { SessionDuration } from "../voip/CallDuration";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext";
interface RoomCallBannerProps {
roomId: Room["roomId"];
@@ -83,7 +83,7 @@ interface Props {
const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
const call = useCall(roomId);
const { roomViewStore } = useScopedRoomContext("roomViewStore");
// this section is to check if we have a live location share. If so, we dont show the call banner
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
@@ -100,7 +100,7 @@ const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
}
// Check if the call is already showing. No banner is needed in this case.
if (SdkContextClass.instance.roomViewStore.isViewingCall()) {
if (roomViewStore.isViewingCall()) {
return null;
}

View File

@@ -13,14 +13,20 @@ import { type Command, CommandCategories, Commands } from "../../../SlashCommand
import InfoDialog from "./InfoDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
/**
* Props for {@link SlashCommandHelpDialog}
* @param roomId - The room ID to check whether commands are enabled
* @param onFinished - Callback called when the dialog is closed
*/
interface IProps {
roomId: string;
onFinished(): void;
}
const SlashCommandHelpDialog: React.FC<IProps> = ({ onFinished }) => {
const SlashCommandHelpDialog: React.FC<IProps> = ({ roomId, onFinished }) => {
const categories: Record<string, Command[]> = {};
Commands.forEach((cmd) => {
if (!cmd.isEnabled(MatrixClientPeg.get())) return;
if (!cmd.isEnabled(MatrixClientPeg.get(), roomId)) return;
if (!categories[cmd.category]) {
categories[cmd.category] = [];
}

View File

@@ -31,8 +31,8 @@ import IconizedContextMenu, {
} from "../context_menus/IconizedContextMenu";
import JumpToDatePicker from "./JumpToDatePicker";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { SdkContextClass } from "../../../contexts/SDKContext";
import TimelineSeparator from "./TimelineSeparator";
import RoomContext from "../../../contexts/RoomContext";
interface IProps {
roomId: string;
@@ -51,6 +51,8 @@ interface IState {
* Has additional jump to date functionality when labs flag is enabled
*/
export default class DateSeparator extends React.Component<IProps, IState> {
public static contextType = RoomContext;
declare public context: React.ContextType<typeof RoomContext>;
private settingWatcherRef?: string;
public constructor(props: IProps) {
@@ -143,7 +145,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
// Only try to navigate to the room if the user is still viewing the same
// room. We don't want to jump someone back to a room after a slow request
// if they've already navigated away to another room.
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
const currentRoomId = this.context.roomViewStore.getRoomId();
if (currentRoomId === roomIdForJumpRequest) {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@@ -169,7 +171,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
// don't want to worry someone about an error in a room they no longer care
// about after a slow request if they've already navigated away to another
// room.
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
const currentRoomId = this.context.roomViewStore.getRoomId();
if (currentRoomId === roomIdForJumpRequest) {
let friendlyErrorMessage = "An error occured while trying to find and jump to the given date.";
let submitDebugLogsContent: JSX.Element = <></>;

View File

@@ -17,11 +17,18 @@ import MediaProcessingError from "./shared/MediaProcessingError";
import { isVoiceMessage } from "../../../utils/EventUtils";
import { PlaybackQueue } from "../../../audio/PlaybackQueue";
import { type Playback } from "../../../audio/Playback";
import RoomContext from "../../../contexts/RoomContext";
export default class MVoiceMessageBody extends MAudioBody {
public static contextType = RoomContext;
declare public context: React.ContextType<typeof RoomContext>;
protected onMount(playback: Playback): void {
if (isVoiceMessage(this.props.mxEvent)) {
PlaybackQueue.forRoom(this.props.mxEvent.getRoomId()!).unsortedEnqueue(this.props.mxEvent, playback);
PlaybackQueue.forRoom(this.props.mxEvent.getRoomId()!, this.context.roomViewStore).unsortedEnqueue(
this.props.mxEvent,
playback,
);
}
}

View File

@@ -37,7 +37,6 @@ import JumpToBottomButton from "../rooms/JumpToBottomButton";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import Measured from "../elements/Measured";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps {
@@ -88,7 +87,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.dispatcherRef = dis.register(this.onAction);
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[, , , value]) =>
this.setState({ showReadReceipts: value as boolean }),
@@ -99,7 +98,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
@@ -109,9 +108,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
private onRoomViewStoreUpdate = async (_initial?: boolean): Promise<void> => {
const newState: Pick<IState, any> = {
initialEventId: SdkContextClass.instance.roomViewStore.getInitialEventId(),
isInitialEventHighlighted: SdkContextClass.instance.roomViewStore.isInitialEventHighlighted(),
replyToEvent: SdkContextClass.instance.roomViewStore.getQuotingEvent(),
initialEventId: this.context.roomViewStore.getInitialEventId(),
isInitialEventHighlighted: this.context.roomViewStore.isInitialEventHighlighted(),
replyToEvent: this.context.roomViewStore.getQuotingEvent(),
};
this.setState(newState);

View File

@@ -88,7 +88,7 @@ export const UserInfoBasicOptionsView: React.FC<{
role="button"
onSelect={async (ev) => {
ev.preventDefault();
vm.onInviteUserButton(ev);
vm.onInviteUserButton(room.roomId, ev);
}}
label={_t("action|invite")}
Icon={InviteIcon}

View File

@@ -244,7 +244,10 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
if (isTyping && this.props.model.parts[0].type === "command") {
const { cmd } = parseCommandString(this.props.model.parts[0].text);
const command = CommandMap.get(cmd!);
if (!command?.isEnabled(MatrixClientPeg.get()) || command.category !== CommandCategories.messages) {
if (
!command?.isEnabled(MatrixClientPeg.get(), this.props.room.roomId) ||
command.category !== CommandCategories.messages
) {
isTyping = false;
}
}

View File

@@ -312,7 +312,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
if (this.isContentModified(newContent)) {
const roomId = editedEvent.getRoomId()!;
if (!containsEmote(this.model) && isSlashCommand(this.model)) {
const [cmd, args, commandText] = getSlashCommand(this.model);
const [cmd, args, commandText] = getSlashCommand(roomId, this.model);
if (cmd) {
const threadId = editedEvent?.getThread()?.id || null;
const [content, commandSuccessful] = await runSlashCommand(

View File

@@ -170,7 +170,7 @@ interface IUploadButtonProps {
// We put the file input outside the UploadButton component so that it doesn't get killed when the context menu closes.
const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, relation, children }) => {
const cli = useContext(MatrixClientContext);
const roomContext = useScopedRoomContext("timelineRenderingType");
const roomContext = useScopedRoomContext("timelineRenderingType", "replyToEvent");
const uploadInput = useRef<HTMLInputElement>(null);
const onUploadClick = (): void => {
@@ -195,6 +195,7 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
Array.from(ev.target.files!),
roomId,
relation,
roomContext.replyToEvent,
cli,
roomContext.timelineRenderingType,
);

View File

@@ -356,7 +356,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
let content: RoomMessageEventContent | null = null;
if (!containsEmote(model) && isSlashCommand(this.model)) {
const [cmd, args, commandText] = getSlashCommand(this.model);
const [cmd, args, commandText] = getSlashCommand(this.props.room.roomId, this.model);
if (cmd) {
const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation?.event_id : null;
@@ -565,6 +565,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
Array.from(data.files),
this.props.room.roomId,
this.props.relation,
this.context.replyToEvent,
this.props.mxClient,
this.context.timelineRenderingType,
);

View File

@@ -148,7 +148,14 @@ export function handleClipboardEvent(
// it puts the filename in as text/plain which we want to ignore.
if (data.files.length && !data.types.includes("text/rtf")) {
ContentMessages.sharedInstance()
.sendContentListToRoom(Array.from(data.files), room.roomId, eventRelation, mxClient, timelineRenderingType)
.sendContentListToRoom(
Array.from(data.files),
room.roomId,
eventRelation,
roomContext.replyToEvent,
mxClient,
timelineRenderingType,
)
.catch(handleError);
return true;
}

View File

@@ -76,7 +76,7 @@ export async function sendMessage(
// Slash command handling here approximates what can be found in SendMessageComposer.sendMessage()
// but note that the /me and // special cases are handled by the call to createMessageContent
if (message.startsWith("/") && !message.startsWith("//") && !message.startsWith(EMOTE_PREFIX)) {
const { cmd, args } = getCommand(message);
const { cmd, args } = getCommand(roomId, message);
if (cmd) {
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
let commandSuccessful: boolean;