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:
co-authored by
Michael Telatynski
parent
209dfece21
commit
ae2acdf311
+11
-2
@@ -55,7 +55,6 @@ import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"
|
|||||||
import { createThumbnail } from "./utils/image-media";
|
import { createThumbnail } from "./utils/image-media";
|
||||||
import { attachMentions, attachRelation } from "./utils/messages.ts";
|
import { attachMentions, attachRelation } from "./utils/messages.ts";
|
||||||
import { doMaybeLocalRoomAction } from "./utils/local-room";
|
import { doMaybeLocalRoomAction } from "./utils/local-room";
|
||||||
import { SdkContextClass } from "./contexts/SDKContext";
|
|
||||||
import { blobIsAnimated } from "./utils/Image.ts";
|
import { blobIsAnimated } from "./utils/Image.ts";
|
||||||
|
|
||||||
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
||||||
@@ -428,10 +427,21 @@ export default class ContentMessages {
|
|||||||
return this.mediaConfig?.["m.upload.size"] ?? null;
|
return this.mediaConfig?.["m.upload.size"] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a list of files to a room.
|
||||||
|
* @param files - The files to send.
|
||||||
|
* @param roomId - The ID of the room to send the files to.
|
||||||
|
* @param relation - The relation to the event being replied to.
|
||||||
|
* @param replyToEvent - The event being replied to, if any.
|
||||||
|
* @param matrixClient - The Matrix client to use for sending the files.
|
||||||
|
* @param context - The context in which the files are being sent.
|
||||||
|
* @returns A promise that resolves when the files have been sent.
|
||||||
|
*/
|
||||||
public async sendContentListToRoom(
|
public async sendContentListToRoom(
|
||||||
files: File[],
|
files: File[],
|
||||||
roomId: string,
|
roomId: string,
|
||||||
relation: IEventRelation | undefined,
|
relation: IEventRelation | undefined,
|
||||||
|
replyToEvent: MatrixEvent | undefined,
|
||||||
matrixClient: MatrixClient,
|
matrixClient: MatrixClient,
|
||||||
context = TimelineRenderingType.Room,
|
context = TimelineRenderingType.Room,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -440,7 +450,6 @@ export default class ContentMessages {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyToEvent = SdkContextClass.instance.roomViewStore.getQuotingEvent();
|
|
||||||
if (!this.mediaConfig) {
|
if (!this.mediaConfig) {
|
||||||
// hot-path optimization to not flash a spinner if we don't need to
|
// hot-path optimization to not flash a spinner if we don't need to
|
||||||
const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner");
|
const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner");
|
||||||
|
|||||||
@@ -732,8 +732,8 @@ export const Commands = [
|
|||||||
new Command({
|
new Command({
|
||||||
command: "help",
|
command: "help",
|
||||||
description: _td("slash_command|help"),
|
description: _td("slash_command|help"),
|
||||||
runFn: function () {
|
runFn: function (cli, roomId, threadId, args) {
|
||||||
Modal.createDialog(SlashCommandHelpDialog);
|
Modal.createDialog(SlashCommandHelpDialog, { roomId });
|
||||||
return success();
|
return success();
|
||||||
},
|
},
|
||||||
category: CommandCategories.advanced,
|
category: CommandCategories.advanced,
|
||||||
@@ -967,14 +967,15 @@ interface ICmd {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the given text for /commands and returns a parsed command that can be used for running the operation.
|
* Process the given text for /commands and returns a parsed command that can be used for running the operation.
|
||||||
|
* @param {string} roomId The room ID where the command was issued.
|
||||||
* @param {string} input The raw text input by the user.
|
* @param {string} input The raw text input by the user.
|
||||||
* @return {ICmd} The parsed command object.
|
* @return {ICmd} The parsed command object.
|
||||||
* Returns an empty object if the input didn't match a command.
|
* Returns an empty object if the input didn't match a command.
|
||||||
*/
|
*/
|
||||||
export function getCommand(input: string): ICmd {
|
export function getCommand(roomId: string, input: string): ICmd {
|
||||||
const { cmd, args } = parseCommandString(input);
|
const { cmd, args } = parseCommandString(input);
|
||||||
|
|
||||||
if (cmd && CommandMap.has(cmd) && CommandMap.get(cmd)!.isEnabled(MatrixClientPeg.get())) {
|
if (cmd && CommandMap.has(cmd) && CommandMap.get(cmd)!.isEnabled(MatrixClientPeg.get(), roomId)) {
|
||||||
return {
|
return {
|
||||||
cmd: CommandMap.get(cmd),
|
cmd: CommandMap.get(cmd),
|
||||||
args,
|
args,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
|
|||||||
import { arrayFastClone } from "../utils/arrays";
|
import { arrayFastClone } from "../utils/arrays";
|
||||||
import { PlaybackManager } from "./PlaybackManager";
|
import { PlaybackManager } from "./PlaybackManager";
|
||||||
import { isVoiceMessage } from "../utils/EventUtils";
|
import { isVoiceMessage } from "../utils/EventUtils";
|
||||||
import { SdkContextClass } from "../contexts/SDKContext";
|
import { type RoomViewStore } from "../stores/RoomViewStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio playback queue management for a given room. This keeps track of where the user
|
* Audio playback queue management for a given room. This keeps track of where the user
|
||||||
@@ -38,10 +38,18 @@ export class PlaybackQueue {
|
|||||||
private currentPlaybackId: string | null = null; // event ID, broken out from above for ease of use
|
private currentPlaybackId: string | null = null; // event ID, broken out from above for ease of use
|
||||||
private recentFullPlays = new Set<string>(); // event IDs
|
private recentFullPlays = new Set<string>(); // event IDs
|
||||||
|
|
||||||
public constructor(private room: Room) {
|
/**
|
||||||
|
* Create a PlaybackQueue for a given room.
|
||||||
|
* @param room The room
|
||||||
|
* @param roomViewStore The RoomViewStore instance
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private room: Room,
|
||||||
|
private roomViewStore: RoomViewStore,
|
||||||
|
) {
|
||||||
this.loadClocks();
|
this.loadClocks();
|
||||||
|
|
||||||
SdkContextClass.instance.roomViewStore.addRoomListener(this.room.roomId, (isActive) => {
|
this.roomViewStore.addRoomListener(this.room.roomId, (isActive) => {
|
||||||
if (!isActive) return;
|
if (!isActive) return;
|
||||||
|
|
||||||
// Reset the state of the playbacks before they start mounting and enqueuing updates.
|
// Reset the state of the playbacks before they start mounting and enqueuing updates.
|
||||||
@@ -53,14 +61,20 @@ export class PlaybackQueue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static forRoom(roomId: string): PlaybackQueue {
|
/**
|
||||||
|
* Get the PlaybackQueue for a given room, creating it if necessary.
|
||||||
|
* @param roomId The ID of the room
|
||||||
|
* @param roomViewStore The RoomViewStore instance
|
||||||
|
* @returns The PlaybackQueue for the room
|
||||||
|
*/
|
||||||
|
public static forRoom(roomId: string, roomViewStore: RoomViewStore): PlaybackQueue {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const room = cli.getRoom(roomId);
|
const room = cli.getRoom(roomId);
|
||||||
if (!room) throw new Error("Unknown room");
|
if (!room) throw new Error("Unknown room");
|
||||||
if (PlaybackQueue.queues.has(room.roomId)) {
|
if (PlaybackQueue.queues.has(room.roomId)) {
|
||||||
return PlaybackQueue.queues.get(room.roomId)!;
|
return PlaybackQueue.queues.get(room.roomId)!;
|
||||||
}
|
}
|
||||||
const queue = new PlaybackQueue(room);
|
const queue = new PlaybackQueue(room, roomViewStore);
|
||||||
PlaybackQueue.queues.set(room.roomId, queue);
|
PlaybackQueue.queues.set(room.roomId, queue);
|
||||||
return queue;
|
return queue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
|
|||||||
|
|
||||||
export default class CommandProvider extends AutocompleteProvider {
|
export default class CommandProvider extends AutocompleteProvider {
|
||||||
public matcher: QueryMatcher<Command>;
|
public matcher: QueryMatcher<Command>;
|
||||||
|
private room: Room;
|
||||||
public constructor(room: Room, renderingType?: TimelineRenderingType) {
|
public constructor(room: Room, renderingType?: TimelineRenderingType) {
|
||||||
super({ commandRegex: COMMAND_RE, renderingType });
|
super({ commandRegex: COMMAND_RE, renderingType });
|
||||||
this.matcher = new QueryMatcher(Commands, {
|
this.matcher = new QueryMatcher(Commands, {
|
||||||
@@ -33,6 +33,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||||||
funcs: [({ aliases }) => aliases.join(" ")], // aliases
|
funcs: [({ aliases }) => aliases.join(" ")], // aliases
|
||||||
context: renderingType,
|
context: renderingType,
|
||||||
});
|
});
|
||||||
|
this.room = room;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCompletions(
|
public async getCompletions(
|
||||||
@@ -51,7 +52,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||||||
if (command[0] !== command[1]) {
|
if (command[0] !== command[1]) {
|
||||||
// The input looks like a command with arguments, perform exact match
|
// The input looks like a command with arguments, perform exact match
|
||||||
const name = command[1].slice(1); // strip leading `/`
|
const name = command[1].slice(1); // strip leading `/`
|
||||||
if (CommandMap.has(name) && CommandMap.get(name)!.isEnabled(cli)) {
|
if (CommandMap.has(name) && CommandMap.get(name)!.isEnabled(cli, this.room.roomId)) {
|
||||||
// some commands, namely `me` don't suit having the usage shown whilst typing their arguments
|
// some commands, namely `me` don't suit having the usage shown whilst typing their arguments
|
||||||
if (CommandMap.get(name)!.hideCompletionAfterSpace) return [];
|
if (CommandMap.get(name)!.hideCompletionAfterSpace) return [];
|
||||||
matches = [CommandMap.get(name)!];
|
matches = [CommandMap.get(name)!];
|
||||||
@@ -70,7 +71,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||||||
return matches
|
return matches
|
||||||
.filter((cmd) => {
|
.filter((cmd) => {
|
||||||
const display = !cmd.renderingTypes || cmd.renderingTypes.includes(this.renderingType);
|
const display = !cmd.renderingTypes || cmd.renderingTypes.includes(this.renderingType);
|
||||||
return cmd.isEnabled(cli) && display;
|
return cmd.isEnabled(cli, this.room.roomId) && display;
|
||||||
})
|
})
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
let completion = result.getCommand() + " ";
|
let completion = result.getCommand() + " ";
|
||||||
|
|||||||
@@ -1128,6 +1128,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
[payload.file],
|
[payload.file],
|
||||||
roomId,
|
roomId,
|
||||||
undefined,
|
undefined,
|
||||||
|
this.state.replyToEvent,
|
||||||
this.context.client,
|
this.context.client,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2047,6 +2048,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
Array.from(dataTransfer.files),
|
Array.from(dataTransfer.files),
|
||||||
roomId,
|
roomId,
|
||||||
undefined,
|
undefined,
|
||||||
|
this.state.replyToEvent,
|
||||||
this.context.client,
|
this.context.client,
|
||||||
TimelineRenderingType.Room,
|
TimelineRenderingType.Room,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,10 +67,11 @@ import { type JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomRea
|
|||||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||||
import { getTopic } from "../../hooks/room/useTopic";
|
import { getTopic } from "../../hooks/room/useTopic";
|
||||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
|
||||||
import { getDisplayAliasForAliasSet } from "../../Rooms";
|
import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { filterBoolean } from "../../utils/arrays.ts";
|
import { filterBoolean } from "../../utils/arrays.ts";
|
||||||
|
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
|
||||||
|
import RoomContext from "../../contexts/RoomContext.ts";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
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:
|
// 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.
|
// fail earlier so they don't have to click back to the directory.
|
||||||
if (cli.isGuest()) {
|
if (cli.isGuest()) {
|
||||||
@@ -418,10 +432,10 @@ export const joinRoom = async (cli: MatrixClient, hierarchy: RoomHierarchy, room
|
|||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof MatrixError) {
|
if (err instanceof MatrixError) {
|
||||||
SdkContextClass.instance.roomViewStore.showJoinRoomError(err, roomId);
|
roomViewStore.showJoinRoomError(err, roomId);
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Got a non-MatrixError while joining room", err);
|
logger.warn("Got a non-MatrixError while joining room", err);
|
||||||
SdkContextClass.instance.roomViewStore.showJoinRoomError(
|
roomViewStore.showJoinRoomError(
|
||||||
new MatrixError({
|
new MatrixError({
|
||||||
error: _t("error|unknown"),
|
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 SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, additionalButtons }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const roomContext = useContext(RoomContext);
|
||||||
const [query, setQuery] = useState(initialText);
|
const [query, setQuery] = useState(initialText);
|
||||||
|
|
||||||
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
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) => {
|
onJoinRoomClick={async (roomId, parents) => {
|
||||||
for (const parent of parents) {
|
for (const parent of parents) {
|
||||||
if (cli.getRoom(parent)?.getMyMembership() !== KnownMembership.Join) {
|
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);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import { type ButtonEvent } from "../views/elements/AccessibleButton";
|
|||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { type ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
import { type ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import Heading from "../views/typography/Heading";
|
import Heading from "../views/typography/Heading";
|
||||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
|
||||||
import { type ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
|
import { type ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
|
||||||
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
|
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();
|
const roomId = this.props.mxEvent.getRoomId();
|
||||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||||
|
|
||||||
const hasRoomChanged = SdkContextClass.instance.roomViewStore.getRoomId() !== roomId;
|
const hasRoomChanged = this.context.roomViewStore.getRoomId() !== roomId;
|
||||||
if (this.props.initialEvent && !hasRoomChanged) {
|
if (this.props.initialEvent && !hasRoomChanged) {
|
||||||
dis.dispatch<ViewRoomPayload>({
|
dis.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
@@ -334,6 +333,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
Array.from(dataTransfer.files),
|
Array.from(dataTransfer.files),
|
||||||
roomId,
|
roomId,
|
||||||
this.threadRelation,
|
this.threadRelation,
|
||||||
|
this.context.replyToEvent,
|
||||||
MatrixClientPeg.safeGet(),
|
MatrixClientPeg.safeGet(),
|
||||||
TimelineRenderingType.Thread,
|
TimelineRenderingType.Thread,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import PosthogTrackers from "../../../../PosthogTrackers";
|
|||||||
import { ShareDialog } from "../../../views/dialogs/ShareDialog";
|
import { ShareDialog } from "../../../views/dialogs/ShareDialog";
|
||||||
import { type ComposerInsertPayload } from "../../../../dispatcher/payloads/ComposerInsertPayload";
|
import { type ComposerInsertPayload } from "../../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { Action } from "../../../../dispatcher/actions";
|
import { Action } from "../../../../dispatcher/actions";
|
||||||
import { SdkContextClass } from "../../../../contexts/SDKContext";
|
|
||||||
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
|
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
|
||||||
import MultiInviter from "../../../../utils/MultiInviter";
|
import MultiInviter from "../../../../utils/MultiInviter";
|
||||||
import { type ViewRoomPayload } from "../../../../dispatcher/payloads/ViewRoomPayload";
|
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
|
// Method called when a share user button is clicked, will display modal with profile to share
|
||||||
onShareUserClick: () => void;
|
onShareUserClick: () => void;
|
||||||
// Method called when a invite button is clicked, will display modal to invite user
|
// 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
|
// Method called when the DM button is clicked, will open a DM with the selected member
|
||||||
onOpenDmForUser: (member: Member) => Promise<void>;
|
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 {
|
try {
|
||||||
const roomId =
|
const roomId = member instanceof RoomMember && member.roomId ? member.roomId : fallbackRoomId;
|
||||||
member instanceof RoomMember && member.roomId
|
|
||||||
? member.roomId
|
|
||||||
: SdkContextClass.instance.roomViewStore.getRoomId();
|
|
||||||
|
|
||||||
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||||
const inviter = new MultiInviter(cli, roomId || "");
|
const inviter = new MultiInviter(cli, roomId || "");
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useCall } from "../../../hooks/useCall";
|
|||||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
|
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
|
||||||
import { SessionDuration } from "../voip/CallDuration";
|
import { SessionDuration } from "../voip/CallDuration";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext";
|
||||||
|
|
||||||
interface RoomCallBannerProps {
|
interface RoomCallBannerProps {
|
||||||
roomId: Room["roomId"];
|
roomId: Room["roomId"];
|
||||||
@@ -83,7 +83,7 @@ interface Props {
|
|||||||
|
|
||||||
const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
|
const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
|
||||||
const call = useCall(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
|
// this section is to check if we have a live location share. If so, we dont show the call banner
|
||||||
const isMonitoringLiveLocation = useEventEmitterState(
|
const isMonitoringLiveLocation = useEventEmitterState(
|
||||||
OwnBeaconStore.instance,
|
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.
|
// Check if the call is already showing. No banner is needed in this case.
|
||||||
if (SdkContextClass.instance.roomViewStore.isViewingCall()) {
|
if (roomViewStore.isViewingCall()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,20 @@ import { type Command, CommandCategories, Commands } from "../../../SlashCommand
|
|||||||
import InfoDialog from "./InfoDialog";
|
import InfoDialog from "./InfoDialog";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
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 {
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
onFinished(): void;
|
onFinished(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SlashCommandHelpDialog: React.FC<IProps> = ({ onFinished }) => {
|
const SlashCommandHelpDialog: React.FC<IProps> = ({ roomId, onFinished }) => {
|
||||||
const categories: Record<string, Command[]> = {};
|
const categories: Record<string, Command[]> = {};
|
||||||
Commands.forEach((cmd) => {
|
Commands.forEach((cmd) => {
|
||||||
if (!cmd.isEnabled(MatrixClientPeg.get())) return;
|
if (!cmd.isEnabled(MatrixClientPeg.get(), roomId)) return;
|
||||||
if (!categories[cmd.category]) {
|
if (!categories[cmd.category]) {
|
||||||
categories[cmd.category] = [];
|
categories[cmd.category] = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ import IconizedContextMenu, {
|
|||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import JumpToDatePicker from "./JumpToDatePicker";
|
import JumpToDatePicker from "./JumpToDatePicker";
|
||||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
|
||||||
import TimelineSeparator from "./TimelineSeparator";
|
import TimelineSeparator from "./TimelineSeparator";
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -51,6 +51,8 @@ interface IState {
|
|||||||
* Has additional jump to date functionality when labs flag is enabled
|
* Has additional jump to date functionality when labs flag is enabled
|
||||||
*/
|
*/
|
||||||
export default class DateSeparator extends React.Component<IProps, IState> {
|
export default class DateSeparator extends React.Component<IProps, IState> {
|
||||||
|
public static contextType = RoomContext;
|
||||||
|
declare public context: React.ContextType<typeof RoomContext>;
|
||||||
private settingWatcherRef?: string;
|
private settingWatcherRef?: string;
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
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
|
// 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
|
// 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.
|
// if they've already navigated away to another room.
|
||||||
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
const currentRoomId = this.context.roomViewStore.getRoomId();
|
||||||
if (currentRoomId === roomIdForJumpRequest) {
|
if (currentRoomId === roomIdForJumpRequest) {
|
||||||
dispatcher.dispatch<ViewRoomPayload>({
|
dispatcher.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
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
|
// 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
|
// about after a slow request if they've already navigated away to another
|
||||||
// room.
|
// room.
|
||||||
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
const currentRoomId = this.context.roomViewStore.getRoomId();
|
||||||
if (currentRoomId === roomIdForJumpRequest) {
|
if (currentRoomId === roomIdForJumpRequest) {
|
||||||
let friendlyErrorMessage = "An error occured while trying to find and jump to the given date.";
|
let friendlyErrorMessage = "An error occured while trying to find and jump to the given date.";
|
||||||
let submitDebugLogsContent: JSX.Element = <></>;
|
let submitDebugLogsContent: JSX.Element = <></>;
|
||||||
|
|||||||
@@ -17,11 +17,18 @@ import MediaProcessingError from "./shared/MediaProcessingError";
|
|||||||
import { isVoiceMessage } from "../../../utils/EventUtils";
|
import { isVoiceMessage } from "../../../utils/EventUtils";
|
||||||
import { PlaybackQueue } from "../../../audio/PlaybackQueue";
|
import { PlaybackQueue } from "../../../audio/PlaybackQueue";
|
||||||
import { type Playback } from "../../../audio/Playback";
|
import { type Playback } from "../../../audio/Playback";
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
|
||||||
export default class MVoiceMessageBody extends MAudioBody {
|
export default class MVoiceMessageBody extends MAudioBody {
|
||||||
|
public static contextType = RoomContext;
|
||||||
|
declare public context: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
protected onMount(playback: Playback): void {
|
protected onMount(playback: Playback): void {
|
||||||
if (isVoiceMessage(this.props.mxEvent)) {
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import JumpToBottomButton from "../rooms/JumpToBottomButton";
|
|||||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import Measured from "../elements/Measured";
|
import Measured from "../elements/Measured";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
|
||||||
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx";
|
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -88,7 +87,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
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.dispatcherRef = dis.register(this.onAction);
|
||||||
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[, , , value]) =>
|
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[, , , value]) =>
|
||||||
this.setState({ showReadReceipts: value as boolean }),
|
this.setState({ showReadReceipts: value as boolean }),
|
||||||
@@ -99,7 +98,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
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.readReceiptsSettingWatcher);
|
||||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||||
@@ -109,9 +108,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
private onRoomViewStoreUpdate = async (_initial?: boolean): Promise<void> => {
|
private onRoomViewStoreUpdate = async (_initial?: boolean): Promise<void> => {
|
||||||
const newState: Pick<IState, any> = {
|
const newState: Pick<IState, any> = {
|
||||||
initialEventId: SdkContextClass.instance.roomViewStore.getInitialEventId(),
|
initialEventId: this.context.roomViewStore.getInitialEventId(),
|
||||||
isInitialEventHighlighted: SdkContextClass.instance.roomViewStore.isInitialEventHighlighted(),
|
isInitialEventHighlighted: this.context.roomViewStore.isInitialEventHighlighted(),
|
||||||
replyToEvent: SdkContextClass.instance.roomViewStore.getQuotingEvent(),
|
replyToEvent: this.context.roomViewStore.getQuotingEvent(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const UserInfoBasicOptionsView: React.FC<{
|
|||||||
role="button"
|
role="button"
|
||||||
onSelect={async (ev) => {
|
onSelect={async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
vm.onInviteUserButton(ev);
|
vm.onInviteUserButton(room.roomId, ev);
|
||||||
}}
|
}}
|
||||||
label={_t("action|invite")}
|
label={_t("action|invite")}
|
||||||
Icon={InviteIcon}
|
Icon={InviteIcon}
|
||||||
|
|||||||
@@ -244,7 +244,10 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||||||
if (isTyping && this.props.model.parts[0].type === "command") {
|
if (isTyping && this.props.model.parts[0].type === "command") {
|
||||||
const { cmd } = parseCommandString(this.props.model.parts[0].text);
|
const { cmd } = parseCommandString(this.props.model.parts[0].text);
|
||||||
const command = CommandMap.get(cmd!);
|
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;
|
isTyping = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||||||
if (this.isContentModified(newContent)) {
|
if (this.isContentModified(newContent)) {
|
||||||
const roomId = editedEvent.getRoomId()!;
|
const roomId = editedEvent.getRoomId()!;
|
||||||
if (!containsEmote(this.model) && isSlashCommand(this.model)) {
|
if (!containsEmote(this.model) && isSlashCommand(this.model)) {
|
||||||
const [cmd, args, commandText] = getSlashCommand(this.model);
|
const [cmd, args, commandText] = getSlashCommand(roomId, this.model);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
const threadId = editedEvent?.getThread()?.id || null;
|
const threadId = editedEvent?.getThread()?.id || null;
|
||||||
const [content, commandSuccessful] = await runSlashCommand(
|
const [content, commandSuccessful] = await runSlashCommand(
|
||||||
|
|||||||
@@ -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.
|
// 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 UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, relation, children }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const roomContext = useScopedRoomContext("timelineRenderingType");
|
const roomContext = useScopedRoomContext("timelineRenderingType", "replyToEvent");
|
||||||
const uploadInput = useRef<HTMLInputElement>(null);
|
const uploadInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const onUploadClick = (): void => {
|
const onUploadClick = (): void => {
|
||||||
@@ -195,6 +195,7 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
|
|||||||
Array.from(ev.target.files!),
|
Array.from(ev.target.files!),
|
||||||
roomId,
|
roomId,
|
||||||
relation,
|
relation,
|
||||||
|
roomContext.replyToEvent,
|
||||||
cli,
|
cli,
|
||||||
roomContext.timelineRenderingType,
|
roomContext.timelineRenderingType,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||||||
let content: RoomMessageEventContent | null = null;
|
let content: RoomMessageEventContent | null = null;
|
||||||
|
|
||||||
if (!containsEmote(model) && isSlashCommand(this.model)) {
|
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) {
|
if (cmd) {
|
||||||
const threadId =
|
const threadId =
|
||||||
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation?.event_id : null;
|
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),
|
Array.from(data.files),
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.props.relation,
|
this.props.relation,
|
||||||
|
this.context.replyToEvent,
|
||||||
this.props.mxClient,
|
this.props.mxClient,
|
||||||
this.context.timelineRenderingType,
|
this.context.timelineRenderingType,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -148,7 +148,14 @@ export function handleClipboardEvent(
|
|||||||
// it puts the filename in as text/plain which we want to ignore.
|
// it puts the filename in as text/plain which we want to ignore.
|
||||||
if (data.files.length && !data.types.includes("text/rtf")) {
|
if (data.files.length && !data.types.includes("text/rtf")) {
|
||||||
ContentMessages.sharedInstance()
|
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);
|
.catch(handleError);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export async function sendMessage(
|
|||||||
// Slash command handling here approximates what can be found in SendMessageComposer.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
|
// but note that the /me and // special cases are handled by the call to createMessageContent
|
||||||
if (message.startsWith("/") && !message.startsWith("//") && !message.startsWith(EMOTE_PREFIX)) {
|
if (message.startsWith("/") && !message.startsWith("//") && !message.startsWith(EMOTE_PREFIX)) {
|
||||||
const { cmd, args } = getCommand(message);
|
const { cmd, args } = getCommand(roomId, message);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
|
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
|
||||||
let commandSuccessful: boolean;
|
let commandSuccessful: boolean;
|
||||||
|
|||||||
@@ -38,7 +38,13 @@ export function isSlashCommand(model: EditorModel): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSlashCommand(model: EditorModel): [Command | undefined, string | undefined, string] {
|
/**
|
||||||
|
* Get the slash command and its arguments from the editor model
|
||||||
|
* @param roomId - The room ID to check whether the command is enabled
|
||||||
|
* @param model - The editor model
|
||||||
|
* @returns A tuple of the command (or undefined if not found), the arguments (or undefined), and the full command text
|
||||||
|
*/
|
||||||
|
export function getSlashCommand(roomId: string, model: EditorModel): [Command | undefined, string | undefined, string] {
|
||||||
const commandText = model.parts.reduce((text, part) => {
|
const commandText = model.parts.reduce((text, part) => {
|
||||||
// use mxid to textify user pills in a command and room alias/id for room pills
|
// use mxid to textify user pills in a command and room alias/id for room pills
|
||||||
if (part.type === Type.UserPill || part.type === Type.RoomPill) {
|
if (part.type === Type.UserPill || part.type === Type.RoomPill) {
|
||||||
@@ -46,7 +52,7 @@ export function getSlashCommand(model: EditorModel): [Command | undefined, strin
|
|||||||
}
|
}
|
||||||
return text + part.text;
|
return text + part.text;
|
||||||
}, "");
|
}, "");
|
||||||
const { cmd, args } = getCommand(commandText);
|
const { cmd, args } = getCommand(roomId, commandText);
|
||||||
return [cmd, args, commandText];
|
return [cmd, args, commandText];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { useRoomState } from "../useRoomState";
|
|||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { isManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../widgets/ManagedHybrid";
|
import { isManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../widgets/ManagedHybrid";
|
||||||
import { type IApp } from "../../stores/WidgetStore";
|
import { type IApp } from "../../stores/WidgetStore";
|
||||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
@@ -37,6 +36,7 @@ import { UIFeature } from "../../settings/UIFeature";
|
|||||||
import { type InteractionName } from "../../PosthogTrackers";
|
import { type InteractionName } from "../../PosthogTrackers";
|
||||||
import { ElementCallMemberEventType } from "../../call-types";
|
import { ElementCallMemberEventType } from "../../call-types";
|
||||||
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
|
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
|
||||||
|
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
||||||
|
|
||||||
export enum PlatformCallType {
|
export enum PlatformCallType {
|
||||||
ElementCall,
|
ElementCall,
|
||||||
@@ -98,6 +98,7 @@ export const useRoomCall = (
|
|||||||
showVideoCallButton: boolean;
|
showVideoCallButton: boolean;
|
||||||
showVoiceCallButton: boolean;
|
showVoiceCallButton: boolean;
|
||||||
} => {
|
} => {
|
||||||
|
const roomViewStore = useScopedRoomContext("roomViewStore").roomViewStore;
|
||||||
// settings
|
// settings
|
||||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||||
const widgetsFeatureEnabled = useSettingValue(UIFeature.Widgets);
|
const widgetsFeatureEnabled = useSettingValue(UIFeature.Widgets);
|
||||||
@@ -124,9 +125,9 @@ export const useRoomCall = (
|
|||||||
const hasGroupCall = groupCall !== null;
|
const hasGroupCall = groupCall !== null;
|
||||||
const hasActiveCallSession = useParticipantCount(groupCall) > 0;
|
const hasActiveCallSession = useParticipantCount(groupCall) > 0;
|
||||||
const isViewingCall = useEventEmitterState(
|
const isViewingCall = useEventEmitterState(
|
||||||
SdkContextClass.instance.roomViewStore,
|
roomViewStore,
|
||||||
UPDATE_EVENT,
|
UPDATE_EVENT,
|
||||||
() => SdkContextClass.instance.roomViewStore.isViewingCall() || isVideoRoom(room),
|
() => roomViewStore.isViewingCall() || isVideoRoom(room),
|
||||||
);
|
);
|
||||||
|
|
||||||
// room
|
// room
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ import { _t, type TranslationKey, UserFriendlyError } from "../languageHandler";
|
|||||||
import { PosthogAnalytics } from "../PosthogAnalytics";
|
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
import { CommandCategories, type RunResult } from "./interface";
|
import { CommandCategories, type RunResult } from "./interface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function signature for the run function of a {@link Command}
|
||||||
|
* @param matrixClient - The Matrix client
|
||||||
|
* @param roomId - The room ID where the command is run
|
||||||
|
* @param threadId - The thread ID where the command is run, or null for room timeline
|
||||||
|
* @param args - The arguments passed to the command
|
||||||
|
* @returns The result of running the command
|
||||||
|
*/
|
||||||
type RunFn = (
|
type RunFn = (
|
||||||
this: Command,
|
this: Command,
|
||||||
matrixClient: MatrixClient,
|
matrixClient: MatrixClient,
|
||||||
@@ -26,6 +34,19 @@ type RunFn = (
|
|||||||
args?: string,
|
args?: string,
|
||||||
) => RunResult;
|
) => RunResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for {@link Command}
|
||||||
|
* @param command - The command name, e.g. "me" for the /me command
|
||||||
|
* @param aliases - Alternative names for the command
|
||||||
|
* @param args - The arguments for the command, e.g. "<message>" for the /me command
|
||||||
|
* @param description - A translation key describing the command
|
||||||
|
* @param analyticsName - The name to use for analytics tracking
|
||||||
|
* @param runFn - The function to execute when the command is run
|
||||||
|
* @param category - The category of the command, e.g. CommandCategories.emoji
|
||||||
|
* @param hideCompletionAfterSpace - Whether to hide autocomplete after a space is typed
|
||||||
|
* @param isEnabled - A function to determine if the command is enabled in a given context
|
||||||
|
* @param renderingTypes - The rendering types (room/thread) where this command is valid
|
||||||
|
*/
|
||||||
interface ICommandOpts {
|
interface ICommandOpts {
|
||||||
command: string;
|
command: string;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
@@ -35,7 +56,7 @@ interface ICommandOpts {
|
|||||||
runFn?: RunFn;
|
runFn?: RunFn;
|
||||||
category: string;
|
category: string;
|
||||||
hideCompletionAfterSpace?: boolean;
|
hideCompletionAfterSpace?: boolean;
|
||||||
isEnabled?(matrixClient: MatrixClient | null): boolean;
|
isEnabled?(matrixClient: MatrixClient | null, roomId: string | null): boolean;
|
||||||
renderingTypes?: TimelineRenderingType[];
|
renderingTypes?: TimelineRenderingType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +70,7 @@ export class Command {
|
|||||||
public readonly hideCompletionAfterSpace: boolean;
|
public readonly hideCompletionAfterSpace: boolean;
|
||||||
public readonly renderingTypes?: TimelineRenderingType[];
|
public readonly renderingTypes?: TimelineRenderingType[];
|
||||||
public readonly analyticsName?: SlashCommandEvent["command"];
|
public readonly analyticsName?: SlashCommandEvent["command"];
|
||||||
private readonly _isEnabled?: (matrixClient: MatrixClient | null) => boolean;
|
private readonly _isEnabled?: (matrixClient: MatrixClient | null, roomId: string | null) => boolean;
|
||||||
|
|
||||||
public constructor(opts: ICommandOpts) {
|
public constructor(opts: ICommandOpts) {
|
||||||
this.command = opts.command;
|
this.command = opts.command;
|
||||||
@@ -102,7 +123,7 @@ export class Command {
|
|||||||
return _t("slash_command|usage") + ": " + this.getCommandWithArgs();
|
return _t("slash_command|usage") + ": " + this.getCommandWithArgs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEnabled(cli: MatrixClient | null): boolean {
|
public isEnabled(cli: MatrixClient | null, roomId: string | null): boolean {
|
||||||
return this._isEnabled?.(cli) ?? true;
|
return this._isEnabled?.(cli, roomId) ?? true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,13 @@ export function successSync(value: any): RunResult {
|
|||||||
return success(Promise.resolve(value));
|
return success(Promise.resolve(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const canAffectPowerlevels = (cli: MatrixClient | null): boolean => {
|
/**
|
||||||
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
* Check whether the user can affect power levels in the given room
|
||||||
|
* @param cli - The Matrix client
|
||||||
|
* @param roomId - The room ID
|
||||||
|
* @returns True if the user can affect power levels, false otherwise
|
||||||
|
*/
|
||||||
|
export const canAffectPowerlevels = (cli: MatrixClient | null, roomId: string | null): boolean => {
|
||||||
if (!cli || !roomId) return false;
|
if (!cli || !roomId) return false;
|
||||||
const room = cli?.getRoom(roomId);
|
const room = cli?.getRoom(roomId);
|
||||||
return !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getSafeUserId()) && !isLocalRoom(room);
|
return !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getSafeUserId()) && !isLocalRoom(room);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe("SlashCommands", () => {
|
|||||||
|
|
||||||
describe("/topic", () => {
|
describe("/topic", () => {
|
||||||
it("sets topic", async () => {
|
it("sets topic", async () => {
|
||||||
const command = getCommand("/topic pizza");
|
const command = getCommand(roomId, "/topic pizza");
|
||||||
expect(command.cmd).toBeDefined();
|
expect(command.cmd).toBeDefined();
|
||||||
expect(command.args).toBeDefined();
|
expect(command.args).toBeDefined();
|
||||||
await command.cmd!.run(client, "room-id", null, command.args);
|
await command.cmd!.run(client, "room-id", null, command.args);
|
||||||
@@ -75,7 +75,7 @@ describe("SlashCommands", () => {
|
|||||||
|
|
||||||
it("should show topic modal if no args passed", async () => {
|
it("should show topic modal if no args passed", async () => {
|
||||||
const spy = jest.spyOn(Modal, "createDialog");
|
const spy = jest.spyOn(Modal, "createDialog");
|
||||||
const command = getCommand("/topic")!;
|
const command = getCommand(roomId, "/topic")!;
|
||||||
await command.cmd!.run(client, roomId, null);
|
await command.cmd!.run(client, roomId, null);
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -109,12 +109,12 @@ describe("SlashCommands", () => {
|
|||||||
describe("isEnabled", () => {
|
describe("isEnabled", () => {
|
||||||
it("should return true for Room", () => {
|
it("should return true for Room", () => {
|
||||||
setCurrentRoom();
|
setCurrentRoom();
|
||||||
expect(command.isEnabled(client)).toBe(true);
|
expect(command.isEnabled(client, roomId)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for LocalRoom", () => {
|
it("should return false for LocalRoom", () => {
|
||||||
setCurrentLocalRoom();
|
setCurrentLocalRoom();
|
||||||
expect(command.isEnabled(client)).toBe(false);
|
expect(command.isEnabled(client, roomId)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -126,7 +126,7 @@ describe("SlashCommands", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should be enabled by default", () => {
|
it("should be enabled by default", () => {
|
||||||
expect(command.isEnabled(client)).toBe(true);
|
expect(command.isEnabled(client, roomId)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,11 +199,11 @@ describe("SlashCommands", () => {
|
|||||||
room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar");
|
room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar");
|
||||||
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
||||||
|
|
||||||
const command = getCommand("/part #foo:bar");
|
const command = getCommand(room1.roomId, "/part #foo:bar");
|
||||||
expect(command.cmd).toBeDefined();
|
expect(command.cmd).toBeDefined();
|
||||||
expect(command.args).toBeDefined();
|
expect(command.args).toBeDefined();
|
||||||
await command.cmd!.run(client, "room-id", null, command.args);
|
await command.cmd!.run(client, room1.roomId, null, command.args);
|
||||||
expect(client.leaveRoomChain).toHaveBeenCalledWith("room-id", expect.anything());
|
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should part room matching alt alias if found", async () => {
|
it("should part room matching alt alias if found", async () => {
|
||||||
@@ -213,11 +213,11 @@ describe("SlashCommands", () => {
|
|||||||
room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]);
|
room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]);
|
||||||
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
||||||
|
|
||||||
const command = getCommand("/part #foo:bar");
|
const command = getCommand(room1.roomId, "/part #foo:bar");
|
||||||
expect(command.cmd).toBeDefined();
|
expect(command.cmd).toBeDefined();
|
||||||
expect(command.args).toBeDefined();
|
expect(command.args).toBeDefined();
|
||||||
await command.cmd!.run(client, "room-id", null, command.args!);
|
await command.cmd!.run(client, room1.roomId, null, command.args!);
|
||||||
expect(client.leaveRoomChain).toHaveBeenCalledWith("room-id", expect.anything());
|
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { PlaybackQueue } from "../../../src/audio/PlaybackQueue";
|
|||||||
import { type Playback, PlaybackState } from "../../../src/audio/Playback";
|
import { type Playback, PlaybackState } from "../../../src/audio/Playback";
|
||||||
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
||||||
import { MockedPlayback } from "./MockedPlayback";
|
import { MockedPlayback } from "./MockedPlayback";
|
||||||
|
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||||
|
|
||||||
describe("PlaybackQueue", () => {
|
describe("PlaybackQueue", () => {
|
||||||
let playbackQueue: PlaybackQueue;
|
let playbackQueue: PlaybackQueue;
|
||||||
@@ -21,7 +22,7 @@ describe("PlaybackQueue", () => {
|
|||||||
mockRoom = {
|
mockRoom = {
|
||||||
getMember: jest.fn(),
|
getMember: jest.fn(),
|
||||||
} as unknown as Mocked<Room>;
|
} as unknown as Mocked<Room>;
|
||||||
playbackQueue = new PlaybackQueue(mockRoom);
|
playbackQueue = new PlaybackQueue(mockRoom, SdkContextClass.instance.roomViewStore);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@@ -75,7 +76,7 @@ describe("PlaybackQueue", () => {
|
|||||||
`mx_voice_message_clocks_${mockRoom.roomId}`,
|
`mx_voice_message_clocks_${mockRoom.roomId}`,
|
||||||
JSON.stringify(Array.from(clockStates.entries())),
|
JSON.stringify(Array.from(clockStates.entries())),
|
||||||
);
|
);
|
||||||
playbackQueue = new PlaybackQueue(mockRoom);
|
playbackQueue = new PlaybackQueue(mockRoom, SdkContextClass.instance.roomViewStore);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(playbackQueue.clockStates.has("a")).toBe(true);
|
expect(playbackQueue.clockStates.has("a")).toBe(true);
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import CommandProvider from "../../../src/autocomplete/CommandProvider";
|
||||||
|
import { stubClient } from "../../test-utils";
|
||||||
|
import { Command } from "../../../src/slash-commands/command";
|
||||||
|
import { CommandCategories } from "../../../src/slash-commands/interface";
|
||||||
|
import { _td } from "../../../src/languageHandler";
|
||||||
|
import * as SlashCommands from "../../../src/SlashCommands";
|
||||||
|
|
||||||
|
describe("CommandProvider", () => {
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stubClient();
|
||||||
|
room = {
|
||||||
|
roomId: "!room:server",
|
||||||
|
} as Room;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter out disabled commands when arguments are provided", async () => {
|
||||||
|
// Create a disabled command
|
||||||
|
const disabledCommand = new Command({
|
||||||
|
command: "disabled",
|
||||||
|
args: "<arg>",
|
||||||
|
description: _td("slash_command|spoiler"),
|
||||||
|
runFn: jest.fn(),
|
||||||
|
category: CommandCategories.messages,
|
||||||
|
isEnabled: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an enabled command
|
||||||
|
const enabledCommand = new Command({
|
||||||
|
command: "enabled",
|
||||||
|
args: "<arg>",
|
||||||
|
description: _td("slash_command|shrug"),
|
||||||
|
runFn: jest.fn(),
|
||||||
|
category: CommandCategories.messages,
|
||||||
|
isEnabled: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the Commands array and CommandMap
|
||||||
|
Object.defineProperty(SlashCommands, "Commands", {
|
||||||
|
value: [disabledCommand, enabledCommand],
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCommandMap = new Map<string, Command>();
|
||||||
|
mockCommandMap.set("disabled", disabledCommand);
|
||||||
|
mockCommandMap.set("enabled", enabledCommand);
|
||||||
|
|
||||||
|
Object.defineProperty(SlashCommands, "CommandMap", {
|
||||||
|
value: mockCommandMap,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const provider = new CommandProvider(room);
|
||||||
|
|
||||||
|
// When we search for a disabled command with arguments
|
||||||
|
const completions = await provider.getCompletions("/disabled test", {
|
||||||
|
beginning: true,
|
||||||
|
start: 0,
|
||||||
|
end: 14,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then we should get no completions because the command is disabled
|
||||||
|
expect(completions).toEqual([]);
|
||||||
|
|
||||||
|
// When we search for an enabled command with arguments
|
||||||
|
const enabledCompletions = await provider.getCompletions("/enabled test", {
|
||||||
|
beginning: true,
|
||||||
|
start: 0,
|
||||||
|
end: 13,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then we should get the completion because the command is enabled
|
||||||
|
// The completion preserves the arguments when the command matches
|
||||||
|
expect(enabledCompletions.length).toBe(1);
|
||||||
|
expect(enabledCompletions[0].completion).toBe("/enabled test");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,18 +9,19 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||||
import { type HierarchyRoom, JoinRule, type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
import { type HierarchyRoom, JoinRule, MatrixError, type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import { mkStubRoom, stubClient } from "../../../test-utils";
|
import { mkStubRoom, stubClient } from "../../../test-utils";
|
||||||
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
import SpaceHierarchy, { showRoom, toLocalRoom } from "../../../../src/components/structures/SpaceHierarchy";
|
import SpaceHierarchy, { showRoom, toLocalRoom, joinRoom } from "../../../../src/components/structures/SpaceHierarchy";
|
||||||
import { Action } from "../../../../src/dispatcher/actions";
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import { type RoomViewStore } from "../../../../src/stores/RoomViewStore";
|
||||||
|
|
||||||
describe("SpaceHierarchy", () => {
|
describe("SpaceHierarchy", () => {
|
||||||
describe("showRoom", () => {
|
describe("showRoom", () => {
|
||||||
@@ -66,6 +67,59 @@ describe("SpaceHierarchy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("joinRoom", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
let hierarchy: RoomHierarchy;
|
||||||
|
let roomViewStore: RoomViewStore;
|
||||||
|
let room: Room;
|
||||||
|
const roomId = "!room:server";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stubClient();
|
||||||
|
client = MatrixClientPeg.safeGet();
|
||||||
|
room = new Room("space-id", client, "@alice:example.com");
|
||||||
|
hierarchy = new RoomHierarchy(room);
|
||||||
|
roomViewStore = {
|
||||||
|
showJoinRoomError: jest.fn(),
|
||||||
|
} as unknown as RoomViewStore;
|
||||||
|
|
||||||
|
jest.spyOn(client, "isGuest").mockReturnValue(false);
|
||||||
|
jest.spyOn(dispatcher, "dispatch");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle MatrixError exceptions when joining room", async () => {
|
||||||
|
// Mock joinRoom to throw a MatrixError
|
||||||
|
const matrixError = new MatrixError({ errcode: "M_FORBIDDEN", error: "Access denied" });
|
||||||
|
mocked(client.joinRoom).mockRejectedValue(matrixError);
|
||||||
|
|
||||||
|
// Attempt to join the room
|
||||||
|
await expect(joinRoom(client, roomViewStore, hierarchy, roomId)).rejects.toThrow(matrixError);
|
||||||
|
|
||||||
|
// Verify that showJoinRoomError was called with the MatrixError
|
||||||
|
expect(roomViewStore.showJoinRoomError).toHaveBeenCalledWith(matrixError, roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle non-MatrixError exceptions when joining room", async () => {
|
||||||
|
// Mock joinRoom to throw a non-MatrixError
|
||||||
|
const customError = new Error("Custom error");
|
||||||
|
mocked(client.joinRoom).mockRejectedValue(customError);
|
||||||
|
|
||||||
|
// Attempt to join the room
|
||||||
|
await expect(joinRoom(client, roomViewStore, hierarchy, roomId)).rejects.toThrow("Custom error");
|
||||||
|
|
||||||
|
// Verify that showJoinRoomError was called with a MatrixError wrapper
|
||||||
|
expect(roomViewStore.showJoinRoomError).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
errcode: undefined,
|
||||||
|
data: expect.objectContaining({
|
||||||
|
error: "Unknown error",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
roomId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("toLocalRoom", () => {
|
describe("toLocalRoom", () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
const client = MatrixClientPeg.safeGet();
|
const client = MatrixClientPeg.safeGet();
|
||||||
|
|||||||
+1
-1
@@ -204,7 +204,7 @@ describe("<UserOptionsSection />", () => {
|
|||||||
jest.spyOn(Modal, "createDialog");
|
jest.spyOn(Modal, "createDialog");
|
||||||
|
|
||||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||||
result.current.onInviteUserButton(new Event("click"));
|
result.current.onInviteUserButton("roomId", new Event("click"));
|
||||||
|
|
||||||
// check that we have called .invite
|
// check that we have called .invite
|
||||||
expect(spy).toHaveBeenCalledWith([defaultMember.userId]);
|
expect(spy).toHaveBeenCalledWith([defaultMember.userId]);
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ import { CallStore } from "../../../../../src/stores/CallStore";
|
|||||||
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||||
import { ConnectionState } from "../../../../../src/models/Call";
|
import { ConnectionState } from "../../../../../src/models/Call";
|
||||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext";
|
||||||
|
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||||
|
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||||
|
|
||||||
describe("<RoomCallBanner />", () => {
|
describe("<RoomCallBanner />", () => {
|
||||||
let client: Mocked<MatrixClient>;
|
let client: Mocked<MatrixClient>;
|
||||||
@@ -42,6 +44,15 @@ describe("<RoomCallBanner />", () => {
|
|||||||
roomId: "!1:example.org",
|
roomId: "!1:example.org",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockRoomViewStore = {
|
||||||
|
isViewingCall: jest.fn().mockReturnValue(false),
|
||||||
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let roomContext: IRoomState;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
|
|
||||||
@@ -59,6 +70,16 @@ describe("<RoomCallBanner />", () => {
|
|||||||
|
|
||||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||||
|
|
||||||
|
// Reset the mock RoomViewStore
|
||||||
|
mockRoomViewStore.isViewingCall.mockReturnValue(false);
|
||||||
|
|
||||||
|
// Create a stable room context for this test
|
||||||
|
roomContext = {
|
||||||
|
...RoomContext,
|
||||||
|
roomId: room.roomId,
|
||||||
|
roomViewStore: mockRoomViewStore,
|
||||||
|
} as unknown as IRoomState;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -66,7 +87,11 @@ describe("<RoomCallBanner />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const renderBanner = async (props = {}): Promise<void> => {
|
const renderBanner = async (props = {}): Promise<void> => {
|
||||||
render(<RoomCallBanner {...defaultProps} {...props} />);
|
render(
|
||||||
|
<ScopedRoomContextProvider {...roomContext}>
|
||||||
|
<RoomCallBanner {...defaultProps} {...props} />
|
||||||
|
</ScopedRoomContextProvider>,
|
||||||
|
);
|
||||||
await act(() => Promise.resolve()); // Let effects settle
|
await act(() => Promise.resolve()); // Let effects settle
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,8 +142,7 @@ describe("<RoomCallBanner />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't show banner if the call is shown", async () => {
|
it("doesn't show banner if the call is shown", async () => {
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall");
|
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
||||||
mocked(SdkContextClass.instance.roomViewStore.isViewingCall).mockReturnValue(true);
|
|
||||||
await renderBanner();
|
await renderBanner();
|
||||||
const banner = await screen.queryByText("Video call");
|
const banner = await screen.queryByText("Video call");
|
||||||
expect(banner).toBeFalsy();
|
expect(banner).toBeFalsy();
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen } from "jest-matrix-react";
|
||||||
|
|
||||||
|
import SlashCommandHelpDialog from "../../../../../src/components/views/dialogs/SlashCommandHelpDialog";
|
||||||
|
import { stubClient } from "../../../../test-utils";
|
||||||
|
import { Command } from "../../../../../src/slash-commands/command";
|
||||||
|
import { CommandCategories } from "../../../../../src/slash-commands/interface";
|
||||||
|
import { _t, _td } from "../../../../../src/languageHandler";
|
||||||
|
import * as SlashCommands from "../../../../../src/SlashCommands";
|
||||||
|
|
||||||
|
describe("SlashCommandHelpDialog", () => {
|
||||||
|
const roomId = "!room:server";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stubClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter out disabled commands", () => {
|
||||||
|
// Create commands with some enabled and some disabled
|
||||||
|
const enabledCommand = new Command({
|
||||||
|
command: "enabled",
|
||||||
|
args: "<arg>",
|
||||||
|
description: _td("slash_command|spoiler"),
|
||||||
|
runFn: jest.fn(),
|
||||||
|
category: CommandCategories.messages,
|
||||||
|
isEnabled: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledCommand = new Command({
|
||||||
|
command: "disabled",
|
||||||
|
args: "<arg>",
|
||||||
|
description: _td("slash_command|shrug"),
|
||||||
|
runFn: jest.fn(),
|
||||||
|
category: CommandCategories.messages,
|
||||||
|
isEnabled: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the Commands array by replacing the property
|
||||||
|
Object.defineProperty(SlashCommands, "Commands", {
|
||||||
|
value: [enabledCommand, disabledCommand],
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
render(<SlashCommandHelpDialog roomId={roomId} onFinished={onFinished} />);
|
||||||
|
|
||||||
|
// The enabled command should be visible
|
||||||
|
expect(screen.getByText("/enabled")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// The disabled command should not be visible
|
||||||
|
expect(screen.queryByText("/disabled")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should group commands by category", () => {
|
||||||
|
const messageCommand = new Command({
|
||||||
|
command: "msg",
|
||||||
|
args: "",
|
||||||
|
description: _td("slash_command|plain"),
|
||||||
|
runFn: jest.fn(),
|
||||||
|
category: CommandCategories.messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminCommand = new Command({
|
||||||
|
command: "admin",
|
||||||
|
args: "",
|
||||||
|
description: _td("slash_command|upgraderoom"),
|
||||||
|
runFn: jest.fn(),
|
||||||
|
category: CommandCategories.admin,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(SlashCommands, "Commands", {
|
||||||
|
value: [messageCommand, adminCommand],
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
render(<SlashCommandHelpDialog roomId={roomId} onFinished={onFinished} />);
|
||||||
|
|
||||||
|
// Both category headers should be present
|
||||||
|
expect(screen.getByText(_t(CommandCategories.messages))).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(_t(CommandCategories.admin))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,7 +14,6 @@ import { type TimestampToEventResponse, ConnectionError, HTTPError, MatrixError
|
|||||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../src/dispatcher/actions";
|
import { Action } from "../../../../../src/dispatcher/actions";
|
||||||
import { type ViewRoomPayload } from "../../../../../src/dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../../../../src/dispatcher/payloads/ViewRoomPayload";
|
||||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
|
||||||
import { formatFullDateNoTime } from "../../../../../src/DateUtils";
|
import { formatFullDateNoTime } from "../../../../../src/DateUtils";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
||||||
@@ -26,6 +25,9 @@ import {
|
|||||||
waitEnoughCyclesForModal,
|
waitEnoughCyclesForModal,
|
||||||
} from "../../../../test-utils";
|
} from "../../../../test-utils";
|
||||||
import DateSeparator from "../../../../../src/components/views/messages/DateSeparator";
|
import DateSeparator from "../../../../../src/components/views/messages/DateSeparator";
|
||||||
|
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext";
|
||||||
|
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||||
|
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||||
|
|
||||||
jest.mock("../../../../../src/settings/SettingsStore");
|
jest.mock("../../../../../src/settings/SettingsStore");
|
||||||
|
|
||||||
@@ -40,13 +42,25 @@ describe("DateSeparator", () => {
|
|||||||
roomId,
|
roomId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockRoomViewStore = {
|
||||||
|
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRoomContext = {
|
||||||
|
...RoomContext,
|
||||||
|
roomId,
|
||||||
|
roomViewStore: mockRoomViewStore,
|
||||||
|
} as unknown as IRoomState;
|
||||||
|
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
timestampToEvent: jest.fn(),
|
timestampToEvent: jest.fn(),
|
||||||
});
|
});
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
render(
|
render(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
<DateSeparator {...defaultProps} {...props} />
|
<ScopedRoomContextProvider {...defaultRoomContext}>
|
||||||
|
<DateSeparator {...defaultProps} {...props} />
|
||||||
|
</ScopedRoomContextProvider>
|
||||||
</MatrixClientContext.Provider>,
|
</MatrixClientContext.Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,7 +88,7 @@ describe("DateSeparator", () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(roomId);
|
mockRoomViewStore.getRoomId.mockReturnValue(roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -200,7 +214,7 @@ describe("DateSeparator", () => {
|
|||||||
// network request is taking a while, so we got bored, switched rooms; we
|
// network request is taking a while, so we got bored, switched rooms; we
|
||||||
// shouldn't jump back to the previous room after the network request
|
// shouldn't jump back to the previous room after the network request
|
||||||
// happens to finish later.
|
// happens to finish later.
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!some-other-room");
|
mockRoomViewStore.getRoomId.mockReturnValue("!some-other-room");
|
||||||
|
|
||||||
// Jump to "last week"
|
// Jump to "last week"
|
||||||
mockClient.timestampToEvent.mockResolvedValue({
|
mockClient.timestampToEvent.mockResolvedValue({
|
||||||
@@ -230,7 +244,7 @@ describe("DateSeparator", () => {
|
|||||||
// network request is taking a while, so we got bored, switched rooms; we
|
// network request is taking a while, so we got bored, switched rooms; we
|
||||||
// shouldn't jump back to the previous room after the network request
|
// shouldn't jump back to the previous room after the network request
|
||||||
// happens to finish later.
|
// happens to finish later.
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!some-other-room");
|
mockRoomViewStore.getRoomId.mockReturnValue("!some-other-room");
|
||||||
|
|
||||||
// Try to jump to "last week" but we want an error to occur and ensure that
|
// Try to jump to "last week" but we want an error to occur and ensure that
|
||||||
// we don't show an error dialog for it since we already switched away to
|
// we don't show an error dialog for it since we already switched away to
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper
|
|||||||
import MVoiceMessageBody from "../../../../../src/components/views/messages/MVoiceMessageBody";
|
import MVoiceMessageBody from "../../../../../src/components/views/messages/MVoiceMessageBody";
|
||||||
import { PlaybackQueue } from "../../../../../src/audio/PlaybackQueue";
|
import { PlaybackQueue } from "../../../../../src/audio/PlaybackQueue";
|
||||||
import { createTestClient } from "../../../../test-utils";
|
import { createTestClient } from "../../../../test-utils";
|
||||||
|
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||||
|
|
||||||
describe("<MVvoiceMessageBody />", () => {
|
describe("<MVvoiceMessageBody />", () => {
|
||||||
let event: MatrixEvent;
|
let event: MatrixEvent;
|
||||||
@@ -25,7 +26,7 @@ describe("<MVvoiceMessageBody />", () => {
|
|||||||
|
|
||||||
const matrixClient = createTestClient();
|
const matrixClient = createTestClient();
|
||||||
const room = new Room("!TESTROOM", matrixClient, "@alice:example.org");
|
const room = new Room("!TESTROOM", matrixClient, "@alice:example.org");
|
||||||
const playbackQueue = new PlaybackQueue(room);
|
const playbackQueue = new PlaybackQueue(room, SdkContextClass.instance.roomViewStore);
|
||||||
|
|
||||||
jest.spyOn(PlaybackQueue, "forRoom").mockReturnValue(playbackQueue);
|
jest.spyOn(PlaybackQueue, "forRoom").mockReturnValue(playbackQueue);
|
||||||
jest.spyOn(playbackQueue, "unsortedEnqueue").mockReturnValue(undefined);
|
jest.spyOn(playbackQueue, "unsortedEnqueue").mockReturnValue(undefined);
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ describe("<UserOptionsSection />", () => {
|
|||||||
onInsertPillButton: () => jest.fn(),
|
onInsertPillButton: () => jest.fn(),
|
||||||
onReadReceiptButton: () => jest.fn(),
|
onReadReceiptButton: () => jest.fn(),
|
||||||
onShareUserClick: () => jest.fn(),
|
onShareUserClick: () => jest.fn(),
|
||||||
onInviteUserButton: (evt: Event) => Promise.resolve(),
|
onInviteUserButton: (fallbackRoomId: string, evt: Event) => Promise.resolve(),
|
||||||
onOpenDmForUser: (member: Member) => Promise.resolve(),
|
onOpenDmForUser: (member: Member) => Promise.resolve(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import * as TestUtils from "../../../../test-utils";
|
|||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||||
import EditorModel from "../../../../../src/editor/model";
|
import EditorModel from "../../../../../src/editor/model";
|
||||||
import { createPartCreator, createRenderer } from "../../../editor/mock";
|
import { createPartCreator, createRenderer } from "../../../editor/mock";
|
||||||
|
import { CommandPartCreator } from "../../../../../src/editor/parts";
|
||||||
|
import DocumentOffset from "../../../../../src/editor/offset";
|
||||||
|
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
|
|
||||||
describe("BasicMessageComposer", () => {
|
describe("BasicMessageComposer", () => {
|
||||||
@@ -103,6 +106,25 @@ describe("BasicMessageComposer", () => {
|
|||||||
const placeholder = input[0].style.getPropertyValue("--placeholder");
|
const placeholder = input[0].style.getPropertyValue("--placeholder");
|
||||||
expect(placeholder).toMatch("'w\\\\e'");
|
expect(placeholder).toMatch("'w\\\\e'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not consider typing for unknown or disabled slash commands", async () => {
|
||||||
|
// create a command part which represents a slash command the client doesn't recognise
|
||||||
|
const commandPc = new CommandPartCreator(room as unknown as Room, client as unknown as MatrixClient, null);
|
||||||
|
const commandPart = commandPc.command("/unknown do stuff");
|
||||||
|
const model = new EditorModel([commandPart], commandPc, renderer);
|
||||||
|
|
||||||
|
// spy on typingStore.setSelfTyping
|
||||||
|
const spy = jest.spyOn(SdkContextClass.instance.typingStore, "setSelfTyping");
|
||||||
|
|
||||||
|
render(<BasicMessageComposer model={model} room={room} />);
|
||||||
|
|
||||||
|
// simulate typing by updating the model - this will call the component's update callback
|
||||||
|
await model.update(commandPart.text, "insertText", new DocumentOffset(commandPart.text.length, true));
|
||||||
|
|
||||||
|
// Since the command is not in CommandMap, it should not be considered typing
|
||||||
|
expect(spy).toHaveBeenCalledWith(room.roomId, null, false);
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function generateMockDataTransferForString(string: string): DataTransfer {
|
function generateMockDataTransferForString(string: string): DataTransfer {
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ import { filterConsole, stubClient } from "../../../../../test-utils";
|
|||||||
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
||||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||||
|
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext";
|
||||||
|
import { type IRoomState } from "../../../../../../src/components/structures/RoomView";
|
||||||
|
import RoomContext from "../../../../../../src/contexts/RoomContext";
|
||||||
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
||||||
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||||
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
|
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
|
||||||
@@ -52,7 +55,6 @@ import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
|||||||
import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
import { _t } from "../../../../../../src/languageHandler";
|
import { _t } from "../../../../../../src/languageHandler";
|
||||||
import { SdkContextClass } from "../../../../../../src/contexts/SDKContext";
|
|
||||||
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
||||||
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
||||||
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
||||||
@@ -65,14 +67,6 @@ jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function getWrapper(): RenderOptions {
|
|
||||||
return {
|
|
||||||
wrapper: ({ children }) => (
|
|
||||||
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>{children}</MatrixClientContext.Provider>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("RoomHeader", () => {
|
describe("RoomHeader", () => {
|
||||||
filterConsole(
|
filterConsole(
|
||||||
"[getType] Room !1:example.org does not have an m.room.create event",
|
"[getType] Room !1:example.org does not have an m.room.create event",
|
||||||
@@ -84,6 +78,25 @@ describe("RoomHeader", () => {
|
|||||||
|
|
||||||
let setCardSpy: jest.SpyInstance | undefined;
|
let setCardSpy: jest.SpyInstance | undefined;
|
||||||
|
|
||||||
|
const mockRoomViewStore = {
|
||||||
|
isViewingCall: jest.fn().mockReturnValue(false),
|
||||||
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let roomContext: IRoomState;
|
||||||
|
|
||||||
|
function getWrapper(): RenderOptions {
|
||||||
|
return {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
|
||||||
|
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org", {
|
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org", {
|
||||||
@@ -99,6 +112,16 @@ describe("RoomHeader", () => {
|
|||||||
// Mock CallStore.instance.getCall to return null by default
|
// Mock CallStore.instance.getCall to return null by default
|
||||||
// Individual tests can override this when they need a specific Call object
|
// Individual tests can override this when they need a specific Call object
|
||||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
||||||
|
|
||||||
|
// Reset the mock RoomViewStore
|
||||||
|
mockRoomViewStore.isViewingCall.mockReturnValue(false);
|
||||||
|
|
||||||
|
// Create a stable room context for this test
|
||||||
|
roomContext = {
|
||||||
|
...RoomContext,
|
||||||
|
roomId: ROOM_ID,
|
||||||
|
roomViewStore: mockRoomViewStore,
|
||||||
|
} as unknown as IRoomState;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -581,7 +604,7 @@ describe("RoomHeader", () => {
|
|||||||
it("close lobby button is shown", async () => {
|
it("close lobby button is shown", async () => {
|
||||||
mockRoomMembers(room, 3);
|
mockRoomMembers(room, 3);
|
||||||
|
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
getByLabelText(document.body, "Close lobby");
|
getByLabelText(document.body, "Close lobby");
|
||||||
});
|
});
|
||||||
@@ -590,21 +613,21 @@ describe("RoomHeader", () => {
|
|||||||
mockRoomMembers(room, 3);
|
mockRoomMembers(room, 3);
|
||||||
// Mock CallStore to return a call with 3 participants
|
// Mock CallStore to return a call with 3 participants
|
||||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
||||||
|
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
getByLabelText(document.body, "Close lobby");
|
getByLabelText(document.body, "Close lobby");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("don't show external conference button if the call is not shown", () => {
|
it("don't show external conference button if the call is not shown", () => {
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(false);
|
mockRoomViewStore.isViewingCall.mockReturnValue(false);
|
||||||
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||||
return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" };
|
return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" };
|
||||||
});
|
});
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
expect(screen.queryByLabelText(_t("voip|get_call_link"))).not.toBeInTheDocument();
|
expect(screen.queryByLabelText(_t("voip|get_call_link"))).not.toBeInTheDocument();
|
||||||
|
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
||||||
|
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
|
|||||||
@@ -85,12 +85,13 @@ describe("handleClipboardEvent", () => {
|
|||||||
clipboardData: { files: ["something here"], types: [] },
|
clipboardData: { files: ["something here"], types: [] },
|
||||||
});
|
});
|
||||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||||
|
const mockReplyToEvent = {} as unknown as MatrixEvent;
|
||||||
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
||||||
originalEvent.clipboardData?.files,
|
originalEvent.clipboardData?.files,
|
||||||
mockRoom.roomId,
|
mockRoom.roomId,
|
||||||
undefined, // this is the event relation, an optional arg
|
undefined, // this is the event relation, an optional arg
|
||||||
|
mockReplyToEvent,
|
||||||
mockClient,
|
mockClient,
|
||||||
mockRoomState.timelineRenderingType,
|
mockRoomState.timelineRenderingType,
|
||||||
);
|
);
|
||||||
@@ -103,6 +104,7 @@ describe("handleClipboardEvent", () => {
|
|||||||
clipboardData: { files: ["something here"], types: [] },
|
clipboardData: { files: ["something here"], types: [] },
|
||||||
});
|
});
|
||||||
const mockEventRelation = {} as unknown as IEventRelation;
|
const mockEventRelation = {} as unknown as IEventRelation;
|
||||||
|
const mockReplyToEvent = {} as unknown as MatrixEvent;
|
||||||
const output = handleClipboardEvent(
|
const output = handleClipboardEvent(
|
||||||
originalEvent,
|
originalEvent,
|
||||||
originalEvent.clipboardData,
|
originalEvent.clipboardData,
|
||||||
@@ -116,6 +118,7 @@ describe("handleClipboardEvent", () => {
|
|||||||
originalEvent.clipboardData?.files,
|
originalEvent.clipboardData?.files,
|
||||||
mockRoom.roomId,
|
mockRoom.roomId,
|
||||||
mockEventRelation, // this is the event relation, an optional arg
|
mockEventRelation, // this is the event relation, an optional arg
|
||||||
|
mockReplyToEvent,
|
||||||
mockClient,
|
mockClient,
|
||||||
mockRoomState.timelineRenderingType,
|
mockRoomState.timelineRenderingType,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ describe("message", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(getCommandSpy).toHaveBeenCalledWith(validCommand);
|
expect(getCommandSpy).toHaveBeenCalledWith(mockRoom.roomId, validCommand);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not call getCommand for valid command with invalid prefix", async () => {
|
it("does not call getCommand for valid command with invalid prefix", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user