Compare commits

..

2 Commits

Author SHA1 Message Date
Travis Ralston
527b44bb7a Make it work 2025-08-28 18:36:10 -06:00
Travis Ralston
b934d6b290 Break a bunch of rules and render an indicator in the view model 2025-08-28 15:41:58 -06:00
57 changed files with 302 additions and 896 deletions

View File

@@ -2,7 +2,6 @@
## Checklist
- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.

View File

@@ -1,32 +1,3 @@
Changes in [1.11.112](https://github.com/element-hq/element-web/releases/tag/v1.11.112) (2025-09-16)
====================================================================================================
Fix [CVE-2025-59161](https://www.cve.org/CVERecord?id=CVE-2025-59161) / [GHSA-m6c8-98f4-75rr](https://github.com/element-hq/element-web/security/advisories/GHSA-m6c8-98f4-75rr)
Changes in [1.11.111](https://github.com/element-hq/element-web/releases/tag/v1.11.111) (2025-09-10)
====================================================================================================
## ✨ Features
* Do not hide media from your own user by default ([#29797](https://github.com/element-hq/element-web/pull/29797)). Contributed by @Half-Shot.
* Remember whether sidebar is shown for calls when switching rooms ([#30262](https://github.com/element-hq/element-web/pull/30262)). Contributed by @bojidar-bg.
* Open the proper integration settings on integrations disabled error ([#30538](https://github.com/element-hq/element-web/pull/30538)). Contributed by @Half-Shot.
* Show a "progress" dialog while invites are being sent ([#30561](https://github.com/element-hq/element-web/pull/30561)). Contributed by @richvdh.
* Move the room list to the new ListView(backed by react-virtuoso) ([#30515](https://github.com/element-hq/element-web/pull/30515)). Contributed by @langleyd.
## 🐛 Bug Fixes
* [Backport staging] Ensure container starts if it is mounted with an empty /modules directory. ([#30705](https://github.com/element-hq/element-web/pull/30705)). Contributed by @RiotRobot.
* Fix room joining over federation not specifying vias or using aliases ([#30641](https://github.com/element-hq/element-web/pull/30641)). Contributed by @t3chguy.
* Fix stable-suffixed MSC4133 support ([#30649](https://github.com/element-hq/element-web/pull/30649)). Contributed by @dbkr.
* Fix i18n of message when a setting is disabled ([#30646](https://github.com/element-hq/element-web/pull/30646)). Contributed by @dbkr.
* ListView should not handle the arrow keys if there is a modifier applied ([#30633](https://github.com/element-hq/element-web/pull/30633)). Contributed by @langleyd.
* Make BaseDialog's div keyboard focusable and fix test. ([#30631](https://github.com/element-hq/element-web/pull/30631)). Contributed by @langleyd.
* Fix: Allow triple-click text selection to flow around pills ([#30349](https://github.com/element-hq/element-web/pull/30349)). Contributed by @AlirezaMrtz.
* Watch for a 'join' action to know when the call is connected ([#29492](https://github.com/element-hq/element-web/pull/29492)). Contributed by @robintown.
* Fix: add missing tooltip and aria-label to lock icon next to composer ([#30623](https://github.com/element-hq/element-web/pull/30623)). Contributed by @florianduros.
* Don't render context menu when scrolling ([#30613](https://github.com/element-hq/element-web/pull/30613)). Contributed by @langleyd.
Changes in [1.11.110](https://github.com/element-hq/element-web/releases/tag/v1.11.110) (2025-08-27)
====================================================================================================
## ✨ Features

View File

@@ -14,9 +14,10 @@ entrypoint_log() {
mkdir -p /tmp/element-web-config
cp /app/config*.json /tmp/element-web-config/
# If the module directory exists AND the module directory has modules in it
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
# If there are modules to be loaded
if [ -d "/modules" ]; then
cd /modules
for MODULE in *
do
# If the module has a package.json, use its main field as the entrypoint

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.112",
"version": "1.11.110",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -134,7 +134,7 @@
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "38.2.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",

View File

@@ -908,37 +908,23 @@ test.describe("Timeline", () => {
});
});
test(
"should be able to hide an image",
{ tag: "@screenshot" },
async ({ page, app, homeserver, room, context }) => {
await app.viewRoomById(room.roomId);
const bot = new Bot(page, homeserver, {});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);
await sendImage(bot, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
// Check that the image is now hidden.
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
},
);
test("should be able to hide a video", async ({ page, app, homeserver, room, context }) => {
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
await sendImage(app.client, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
const bot = new Bot(page, homeserver, {});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);
// Check that the image is now hidden.
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
});
const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, {
test("should be able to hide a video", async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
msgtype: "m.video" as MsgType,
body: "bbb.webm",
url: upload.content_uri,

View File

@@ -68,8 +68,7 @@ type ElectronChannel =
| "openDesktopCapturerSourcePicker"
| "userAccessToken"
| "homeserverUrl"
| "serverSupportedVersions"
| "showToast";
| "serverSupportedVersions";
declare global {
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},

View File

@@ -112,7 +112,6 @@ export enum LegacyCallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
ShownSidebarsChanged = "shown_sidebars_changed",
CallState = "call_state",
ProtocolSupport = "protocol_support",
}
@@ -121,7 +120,6 @@ type EventEmitterMap = {
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
[LegacyCallHandlerEvent.ShownSidebarsChanged]: (sidebarsShown: Map<string, boolean>) => void;
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
};
@@ -146,8 +144,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
private silencedCalls = new Set<string>(); // callIds
private shownSidebars = new Map<string, boolean>(); // callId (call) -> sidebar show
private backgroundAudio = new BackgroundAudio();
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
@@ -244,15 +240,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
return false;
}
public setCallSidebarShown(callId: string, sidebarShown: boolean): void {
this.shownSidebars.set(callId, sidebarShown);
this.emit(LegacyCallHandlerEvent.ShownSidebarsChanged, this.shownSidebars);
}
public isCallSidebarShown(callId?: string): boolean {
return !!callId && (this.shownSidebars.get(callId) ?? true);
}
private async checkProtocols(maxTries: number): Promise<void> {
try {
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();

View File

@@ -6,8 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
type CacheResult = { roomId: string; viaServers: string[] };
/**
* This is meant to be a cache of room alias to room ID so that moving between
* rooms happens smoothly (for example using browser back / forward buttons).
@@ -18,12 +16,12 @@ type CacheResult = { roomId: string; viaServers: string[] };
* A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend.
*/
const cache = new Map<string, CacheResult>();
const aliasToIDMap = new Map<string, string>();
export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
cache.set(alias, { roomId, viaServers });
export function storeRoomAliasInCache(alias: string, id: string): void {
aliasToIDMap.set(alias, id);
}
export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
return cache.get(alias);
export function getCachedRoomIDForAlias(alias: string): string | undefined {
return aliasToIDMap.get(alias);
}

View File

@@ -51,7 +51,7 @@ import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration";
import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import { calculateRoomVia, makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from "../../settings/watchers/FontWatcher";
import { storeRoomAliasInCache } from "../../RoomAliasCache";
@@ -1026,7 +1026,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
presentedId = theAlias;
// Store display alias of the presented room in cache to speed future
// navigation.
storeRoomAliasInCache(theAlias, room.roomId, calculateRoomVia(room));
storeRoomAliasInCache(theAlias, room.roomId);
}
// Store this as the ID of the last room accessed. This is so that we can

View File

@@ -245,7 +245,6 @@ class PipContainerInner extends React.Component<IProps, IState> {
secondaryCall={this.state.secondaryCall}
pipMode={pipMode}
onResize={onResize}
sidebarShown={false}
/>
));
}

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { logger } from "@sentry/browser";
import { type Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import React, { type ReactNode } from "react";
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
import { _t } from "../../../../../languageHandler";
@@ -29,7 +30,7 @@ export interface BanButtonState {
}
/**
* The view model for the room ban button used in the UserInfoAdminToolsContainer
* @param {RoomAdminToolsProps} props - the object containing the necceray props for banButton the view model
* @param {RoomAdminToolsProps} props - the object containing the necessary props for banButton the view model
* @param {Room} props.room - the room to ban/unban the user in
* @param {RoomMember} props.member - the member to ban/unban
* @param {boolean} props.isUpdating - whether the operation is currently in progress
@@ -67,8 +68,15 @@ export const useBanButtonViewModel = (props: RoomAdminToolsProps): BanButtonStat
: _t("user_info|ban_room_confirm_title", { roomName: room.name }),
askReason: !isBanned,
danger: !isBanned,
children: [] as ReactNode,
};
const msc4333Config = cli.msc4333Moderation.getModerationConfigFor(props.room.roomId);
if (msc4333Config && !isBanned) {
const managementRoom = cli.getRoom(msc4333Config.managementRoomId);
commonProps["children"] = [<p key={1}>You are banning this user using {managementRoom?.name} (via {msc4333Config.botUserId}).</p>];
}
let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>;
if (room.isSpaceRoom()) {
@@ -119,10 +127,18 @@ export const useBanButtonViewModel = (props: RoomAdminToolsProps): BanButtonStat
}
const fn = (roomId: string): Promise<unknown> => {
if (isBanned) {
return cli.unban(roomId, member.userId);
} else {
return cli.ban(roomId, member.userId, reason || undefined);
try {
if (isBanned) {
return cli.unban(roomId, member.userId);
} else {
if (msc4333Config) {
return cli.sendMessage(msc4333Config.managementRoomId, msc4333Config.banCommand.render(member.userId, roomId, reason || ""))
}
return cli.ban(roomId, member.userId, reason || undefined);
}
} catch (e) {
console.error(e);
throw e; // re-throw
}
};

View File

@@ -1,53 +1,55 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback } from "react";
import React from "react";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { UserTab } from "./UserTab";
interface IProps {
onFinished(): void;
}
export const IntegrationsDisabledDialog: React.FC<IProps> = ({ onFinished }) => {
const onOpenSettingsClick = useCallback(() => {
onFinished();
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
}, [onFinished]);
export default class IntegrationsDisabledDialog extends React.Component<IProps> {
private onAcknowledgeClick = (): void => {
this.props.onFinished();
};
return (
<BaseDialog
className="mx_IntegrationsDisabledDialog"
hasCancel={true}
onFinished={onFinished}
title={_t("integrations|disabled_dialog_title")}
>
<div className="mx_IntegrationsDisabledDialog_content">
<p>
{_t("integrations|disabled_dialog_description", {
manageIntegrations: _t("integration_manager|manage_title"),
})}
</p>
</div>
<DialogButtons
primaryButton={_t("common|settings")}
onPrimaryButtonClick={onOpenSettingsClick}
cancelButton={_t("action|ok")}
onCancel={onFinished}
/>
</BaseDialog>
);
};
private onOpenSettingsClick = (): void => {
this.props.onFinished();
dis.fire(Action.ViewUserSettings);
};
public render(): React.ReactNode {
return (
<BaseDialog
className="mx_IntegrationsDisabledDialog"
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("integrations|disabled_dialog_title")}
>
<div className="mx_IntegrationsDisabledDialog_content">
<p>
{_t("integrations|disabled_dialog_description", {
manageIntegrations: _t("integration_manager|manage_title"),
})}
</p>
</div>
<DialogButtons
primaryButton={_t("common|settings")}
onPrimaryButtonClick={this.onOpenSettingsClick}
cancelButton={_t("action|ok")}
onCancel={this.onAcknowledgeClick}
/>
</BaseDialog>
);
}
}

View File

@@ -53,7 +53,7 @@ import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
import { getCachedRoomIdForAlias } from "../../../../RoomAliasCache";
import { getCachedRoomIDForAlias } from "../../../../RoomAliasCache";
import { showStartChatInviteDialog } from "../../../../RoomInvite";
import { SettingLevel } from "../../../../settings/SettingLevel";
import SettingsStore from "../../../../settings/SettingsStore";
@@ -912,7 +912,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
if (
trimmedQuery.startsWith("#") &&
trimmedQuery.includes(":") &&
(!getCachedRoomIdForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIdForAlias(trimmedQuery)!.roomId))
(!getCachedRoomIDForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIDForAlias(trimmedQuery)))
) {
joinRoomSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">

View File

@@ -151,7 +151,7 @@ interface Props extends ReplacerOptions {
const EventContentBody = memo(
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => {
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
const [mediaIsVisible] = useMediaVisible(mxEvent);
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
const replacer = useReplacer(content, mxEvent, options);
const linkifyOptions = useMemo(

View File

@@ -25,7 +25,7 @@ interface IProps {
* Quick action button for marking a media event as hidden.
*/
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent);
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
if (!mediaIsVisible) {
return;

View File

@@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
// Wrap MImageBody component so we can use a hook here.
const MImageBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
}
}
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -20,7 +20,7 @@ class MStickerBodyInner extends MImageBodyInner {
protected onClick = (ev: React.MouseEvent): void => {
ev.preventDefault();
if (!this.props.mediaVisible) {
this.props.setMediaVisible(true);
this.props.setMediaVisible?.(true);
}
};
@@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
}
const MStickerBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {
// Wrap MVideoBody component so we can use a hook here.
const MVideoBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -30,7 +30,7 @@ interface IProps {
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
const cli = useContext(MatrixClientContext);
const [expanded, toggleExpanded] = useStateToggle();
const [mediaVisible] = useMediaVisible(mxEvent);
const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(

View File

@@ -50,10 +50,6 @@ interface IProps {
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
showApps?: boolean;
sidebarShown: boolean;
setSidebarShown?: (sidebarShown: boolean) => void;
}
interface IState {
@@ -66,6 +62,7 @@ interface IState {
primaryFeed?: CallFeed;
secondaryFeed?: CallFeed;
sidebarFeeds: Array<CallFeed>;
sidebarShown: boolean;
}
function getFullScreenElement(): Element | null {
@@ -100,6 +97,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
primaryFeed: primary,
secondaryFeed: secondary,
sidebarFeeds: sidebar,
sidebarShown: true,
};
}
@@ -271,9 +269,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
isScreensharing = await this.props.call.setScreensharingEnabled(true);
}
this.props.setSidebarShown?.(true);
this.setState({
sidebarShown: true,
screensharing: isScreensharing,
});
};
@@ -323,12 +320,12 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
};
private onToggleSidebar = (): void => {
this.props.setSidebarShown?.(!this.props.sidebarShown);
this.setState({ sidebarShown: !this.state.sidebarShown });
};
private renderCallControls(): JSX.Element {
const { call, pipMode, sidebarShown } = this.props;
const { callState, micMuted, vidMuted, screensharing, secondaryFeed, sidebarFeeds } = this.state;
const { call, pipMode } = this.props;
const { callState, micMuted, vidMuted, screensharing, sidebarShown, secondaryFeed, sidebarFeeds } = this.state;
// If SDPStreamMetadata isn't supported don't show video mute button in voice calls
const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack;
@@ -340,8 +337,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
(call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) &&
call.state === CallState.Connected;
// Show the sidebar button only if there is something to hide/show
const sidebarButtonShown =
!pipMode && ((secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0);
const sidebarButtonShown = (secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0;
// The dial pad & 'more' button actions are only relevant in a connected call
const contextMenuButtonShown = callState === CallState.Connected;
const dialpadButtonShown = callState === CallState.Connected && call.opponentSupportsDTMF();
@@ -376,7 +372,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
}
private renderToast(): JSX.Element | null {
const { call, sidebarShown } = this.props;
const { call } = this.props;
const someoneIsScreensharing = call.getFeeds().some((feed) => {
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
});
@@ -384,7 +380,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
if (!someoneIsScreensharing) return null;
const isScreensharing = call.isScreensharing();
const { primaryFeed } = this.state;
const { primaryFeed, sidebarShown } = this.state;
const sharerName = primaryFeed?.getMember()?.name;
if (!sharerName) return null;
@@ -397,8 +393,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
}
private renderContent(): JSX.Element {
const { pipMode, call, onResize, sidebarShown } = this.props;
const { isLocalOnHold, isRemoteOnHold, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
const { pipMode, call, onResize } = this.props;
const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined;
@@ -541,8 +537,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
}
public render(): React.ReactNode {
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader, sidebarShown } = this.props;
const { sidebarFeeds } = this.state;
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader } = this.props;
const { sidebarShown, sidebarFeeds } = this.state;
const client = MatrixClientPeg.safeGet();
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);

View File

@@ -25,7 +25,6 @@ interface IProps {
interface IState {
call: MatrixCall | null;
sidebarShown: boolean;
}
/*
@@ -35,23 +34,19 @@ interface IState {
export default class LegacyCallViewForRoom extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
const call = this.getCall();
this.state = {
call,
sidebarShown: !!call && LegacyCallHandler.instance.isCallSidebarShown(call.callId),
call: this.getCall(),
};
}
public componentDidMount(): void {
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCall);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
}
public componentWillUnmount(): void {
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCall);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
}
private updateCall = (): void => {
@@ -59,10 +54,6 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
if (newCall !== this.state.call) {
this.setState({ call: newCall });
}
const newSidebarShown = !!newCall && LegacyCallHandler.instance.isCallSidebarShown(newCall.callId);
if (newSidebarShown !== this.state.sidebarShown) {
this.setState({ sidebarShown: newSidebarShown });
}
};
private getCall(): MatrixCall | null {
@@ -84,11 +75,6 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
this.props.resizeNotifier.stopResizing();
};
private setSidebarShown = (sidebarShown: boolean): void => {
if (!this.state.call) return;
LegacyCallHandler.instance.setCallSidebarShown(this.state.call.callId, sidebarShown);
};
public render(): React.ReactNode {
if (!this.state.call) return null;
@@ -113,13 +99,7 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
className="mx_LegacyCallViewForRoom_ResizeWrapper"
handleClasses={{ bottom: "mx_LegacyCallViewForRoom_ResizeHandle" }}
>
<LegacyCallView
call={this.state.call}
pipMode={false}
showApps={this.props.showApps}
sidebarShown={this.state.sidebarShown}
setSidebarShown={this.setSidebarShown}
/>
<LegacyCallView call={this.state.call} pipMode={false} showApps={this.props.showApps} />
</Resizable>
</div>
);

View File

@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { useCallback } from "react";
import { JoinRule, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../settings/SettingLevel";
import { useSettingValue } from "./useSettings";
@@ -19,25 +19,14 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul
/**
* Should the media event be visible in the client, or hidden.
*
* This function uses the `mediaPreviewConfig` setting to determine the rules for the room
* along with the `showMediaEventIds` setting for specific events.
*
* A function may be provided to alter the visible state.
*
* @param The event that contains the media. If not provided, the global rule is used.
*
* @returns Returns a tuple of:
* A boolean describing the hidden status.
* A function to show or hide the event.
* @param eventId The eventId of the media event.
* @returns A boolean describing the hidden status, and a function to set the visiblity.
*/
export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void] {
const eventId = mxEvent?.getId();
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", mxEvent?.getRoomId());
export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] {
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId);
const client = useMatrixClientContext();
const eventVisibility = useSettingValue("showMediaEventIds");
const room = client.getRoom(mxEvent?.getRoomId()) ?? undefined;
const joinRule = useRoomState(room, (state) => state.getJoinRule());
const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule());
const setMediaVisible = useCallback(
(visible: boolean) => {
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
@@ -54,9 +43,6 @@ export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: bool
// Always prefer the explicit per-event user preference here.
if (explicitEventVisiblity !== undefined) {
return [explicitEventVisiblity, setMediaVisible];
} else if (mxEvent?.getSender() === client.getUserId()) {
// If this event is ours and we've not set an explicit visibility, default to on.
return [true, setMediaVisible];
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Off) {
return [false, setMediaVisible];
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.On) {

View File

@@ -13,7 +13,7 @@ import SdkConfig from "../SdkConfig";
import Modal from "../Modal";
import { IntegrationManagerInstance, Kind } from "./IntegrationManagerInstance";
import IntegrationsImpossibleDialog from "../components/views/dialogs/IntegrationsImpossibleDialog";
import { IntegrationsDisabledDialog } from "../components/views/dialogs/IntegrationsDisabledDialog";
import IntegrationsDisabledDialog from "../components/views/dialogs/IntegrationsDisabledDialog";
import WidgetUtils from "../utils/WidgetUtils";
import { MatrixClientPeg } from "../MatrixClientPeg";

View File

@@ -9,31 +9,33 @@ import { type NavigationApi as INavigationApi } from "@element-hq/element-web-mo
import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
import { getCachedRoomIDForAlias } from "../RoomAliasCache.ts";
import { MatrixClientPeg } from "../MatrixClientPeg.ts";
import dispatcher from "../dispatcher/dispatcher.ts";
import { Action } from "../dispatcher/actions.ts";
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
import SettingsStore from "../settings/SettingsStore.ts";
export class NavigationApi implements INavigationApi {
public async toMatrixToLink(link: string, join = false): Promise<void> {
navigateToPermalink(link);
const parts = parsePermalink(link);
if (parts?.roomIdOrAlias) {
if (parts.roomIdOrAlias.startsWith("#")) {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_alias: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: join,
metricsTrigger: undefined,
});
} else {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: join,
metricsTrigger: undefined,
if (parts?.roomIdOrAlias && join) {
let roomId: string | undefined = parts.roomIdOrAlias;
if (roomId.startsWith("#")) {
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
if (!roomId) {
// alias resolution failed
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
roomId = result.room_id;
}
}
if (roomId) {
dispatcher.dispatch({
action: Action.JoinRoom,
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
roomId,
});
}
}

View File

@@ -28,12 +28,13 @@ import dispatcher from "../dispatcher/dispatcher";
import { navigateToPermalink } from "../utils/permalinks/navigator";
import { parsePermalink } from "../utils/permalinks/Permalinks";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { getCachedRoomIDForAlias } from "../RoomAliasCache";
import { Action } from "../dispatcher/actions";
import { type OverwriteLoginPayload } from "../dispatcher/payloads/OverwriteLoginPayload";
import { type ActionPayload } from "../dispatcher/payloads";
import SettingsStore from "../settings/SettingsStore";
import WidgetStore, { type IApp } from "../stores/WidgetStore";
import { type Container, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
/**
* Glue between the `ModuleApi` interface and the react-sdk. Anticipates one instance
@@ -182,22 +183,28 @@ export class ProxiedModuleApi implements ModuleApi {
navigateToPermalink(uri);
const parts = parsePermalink(uri);
if (parts?.roomIdOrAlias) {
if (parts.roomIdOrAlias.startsWith("#")) {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_alias: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: andJoin ?? false,
metricsTrigger: undefined,
});
} else {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: andJoin ?? false,
metricsTrigger: undefined,
if (parts?.roomIdOrAlias && andJoin) {
let roomId: string | undefined = parts.roomIdOrAlias;
let servers = parts.viaServers;
if (roomId.startsWith("#")) {
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
if (!roomId) {
// alias resolution failed
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
roomId = result.room_id;
if (!servers) servers = result.servers; // use provided servers first, if available
}
}
dispatcher.dispatch({
action: Action.ViewRoom,
room_id: roomId,
via_servers: servers,
});
if (andJoin) {
dispatcher.dispatch({
action: Action.JoinRoom,
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
});
}
}

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import { STABLE_MSC4133_EXTENDED_PROFILES, UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
import { type MediaPreviewConfig } from "../@types/media_preview.ts";
// Import i18n.tsx instead of languageHandler to avoid circular deps
@@ -844,7 +844,7 @@ export const SETTINGS: Settings = {
controller: new ServerSupportUnstableFeatureController(
"userTimezonePublish",
defaultWatchManager,
[[UNSTABLE_MSC4133_EXTENDED_PROFILES], [STABLE_MSC4133_EXTENDED_PROFILES]],
[[UNSTABLE_MSC4133_EXTENDED_PROFILES]],
undefined,
_td("labs|extended_profiles_msc_support"),
),

View File

@@ -340,7 +340,7 @@ export default class SettingsStore {
}
/**
* Retrieves the internationalised reason a setting is disabled if one is assigned.
* Retrieves the reason a setting is disabled if one is assigned.
* If a setting is not disabled, or no reason is given by the `SettingController`,
* this will return undefined.
* @param {string} settingName The setting to look up.

View File

@@ -11,7 +11,6 @@ import MatrixClientBackedController from "./MatrixClientBackedController";
import { type WatchManager } from "../WatchManager";
import SettingsStore from "../SettingsStore";
import { type SettingKey } from "../Settings.tsx";
import { _t, type TranslationKey } from "../../languageHandler.tsx";
/**
* Disables a given setting if the server unstable feature it requires is not supported
@@ -34,7 +33,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
private readonly watchers: WatchManager,
private readonly unstableFeatureGroups: string[][],
private readonly stableVersion?: string,
private readonly disabledMessage?: TranslationKey,
private readonly disabledMessage?: string,
private readonly forcedValue: any = false,
) {
super();
@@ -97,7 +96,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
public get settingDisabled(): boolean | string {
if (this.disabled) {
return this.disabledMessage ? _t(this.disabledMessage) : true;
return this.disabledMessage ?? true;
}
return false;
}

View File

@@ -143,7 +143,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, true, msc3946ProcessDynamicPredecessor);
const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, false, msc3946ProcessDynamicPredecessor);
if (history && history.length > 1) {
room = history[history.length - 1]; // Last room is most recent in history

View File

@@ -26,7 +26,7 @@ import { type MatrixDispatcher } from "../dispatcher/dispatcher";
import { MatrixClientPeg } from "../MatrixClientPeg";
import Modal from "../Modal";
import { _t } from "../languageHandler";
import { getCachedRoomIdForAlias, storeRoomAliasInCache } from "../RoomAliasCache";
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from "../RoomAliasCache";
import { Action } from "../dispatcher/actions";
import { retry } from "../utils/promise";
import { TimelineRenderingType } from "../contexts/RoomContext";
@@ -438,7 +438,6 @@ export class RoomViewStore extends EventEmitter {
action: Action.JoinRoom,
roomId: payload.room_id,
metricsTrigger: payload.metricsTrigger as JoinRoomPayload["metricsTrigger"],
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
});
}
@@ -446,16 +445,10 @@ export class RoomViewStore extends EventEmitter {
await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false);
}
} else if (payload.room_alias) {
let roomId: string;
let viaServers: string[] | undefined;
// Try the room alias to room ID navigation cache first to avoid
// blocking room navigation on the homeserver.
const cachedResult = getCachedRoomIdForAlias(payload.room_alias);
if (cachedResult) {
roomId = cachedResult.roomId;
viaServers = cachedResult.viaServers;
} else {
let roomId = getCachedRoomIDForAlias(payload.room_alias);
if (!roomId) {
// Room alias cache miss, so let's ask the homeserver. Resolve the alias
// and then do a second dispatch with the room ID acquired.
this.setState({
@@ -474,9 +467,8 @@ export class RoomViewStore extends EventEmitter {
});
try {
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(payload.room_alias);
storeRoomAliasInCache(payload.room_alias, result.room_id, result.servers);
storeRoomAliasInCache(payload.room_alias, result.room_id);
roomId = result.room_id;
viaServers = result.servers;
} catch (err) {
logger.error("RVS failed to get room id for alias: ", err);
this.dis?.dispatch<ViewRoomErrorPayload>({
@@ -493,7 +485,6 @@ export class RoomViewStore extends EventEmitter {
this.dis?.dispatch({
...payload,
room_id: roomId,
via_servers: viaServers,
});
}
}
@@ -518,13 +509,12 @@ export class RoomViewStore extends EventEmitter {
joining: true,
});
// take a copy of roomAlias, roomId & viaServers as they may change by the time the join is complete
const { roomAlias, roomId = payload.roomId, viaServers = [] } = this.state;
// prefer the room alias if we have one as it allows joining over federation even with no viaServers
const address = roomAlias || roomId!;
// take a copy of roomAlias & roomId as they may change by the time the join is complete
const { roomAlias, roomId } = this.state;
const address = payload.roomId || roomAlias || roomId!;
const joinOpts: IJoinRoomOpts = {
viaServers,
viaServers: this.state.viaServers || [],
...(payload.opts ?? {}),
};
if (SettingsStore.getValue("feature_share_history_on_invite")) {
@@ -557,7 +547,7 @@ export class RoomViewStore extends EventEmitter {
canAskToJoin: payload.canAskToJoin,
});
if (payload.canAskToJoin && err instanceof MatrixError && err.httpStatus === 403) {
if (payload.canAskToJoin) {
this.dis?.dispatch({ action: Action.PromptAskToJoin });
}
}

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { logger } from "matrix-js-sdk/src/logger";
import { EventType, KnownMembership } from "matrix-js-sdk/src/matrix";
import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix";
import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix";
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
import type { ActionPayload } from "../../dispatcher/payloads";
import type { FilterKey } from "./skip-list/filters";
@@ -250,15 +250,12 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
// the dead room in the list.
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
const room: Room = payload.room;
const roomUpgradeHistory = room.client.getRoomUpgradeHistory(
room.roomId,
true,
this.msc3946ProcessDynamicPredecessor,
);
const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room));
for (const predecessor of predecessors) {
this.roomSkipList.removeRoom(predecessor);
const roomState: RoomState = payload.room.currentState;
const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor);
if (predecessor) {
const prevRoom = this.matrixClient?.getRoom(predecessor.roomId);
if (prevRoom) this.roomSkipList.removeRoom(prevRoom);
else logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`);
}
}

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type MatrixClient, type Room, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix";
import { type MatrixClient, type Room, type RoomState, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import SettingsStore from "../../settings/SettingsStore";
@@ -308,22 +308,24 @@ export class RoomListStoreClass extends AsyncStoreWithClient<EmptyObject> implem
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
const newMembership = getEffectiveMembershipTag(membershipPayload.room, membershipPayload.membership);
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
// If we're joining an upgraded room, we'll want to make sure we don't proliferate the dead room in the list.
const room: Room = membershipPayload.room;
const roomUpgradeHistory = room.client.getRoomUpgradeHistory(
room.roomId,
true,
this.msc3946ProcessDynamicPredecessor,
);
const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room));
for (const predecessor of predecessors) {
const isSticky = this.algorithm.stickyRoom === predecessor;
if (isSticky) {
this.algorithm.setStickyRoom(null);
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
// the dead room in the list.
const roomState: RoomState = membershipPayload.room.currentState;
const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor);
if (predecessor) {
const prevRoom = this.matrixClient?.getRoom(predecessor.roomId);
if (prevRoom) {
const isSticky = this.algorithm.stickyRoom === prevRoom;
if (isSticky) {
this.algorithm.setStickyRoom(null);
}
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
// avoid redundant updates.
this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
} else {
logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`);
}
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
// avoid redundant updates.
this.algorithm.handleRoomUpdate(predecessor, RoomUpdateCause.RoomRemoved);
}
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);

View File

@@ -49,7 +49,7 @@ import {
UPDATE_SUGGESTED_ROOMS,
UPDATE_TOP_LEVEL_SPACES,
} from ".";
import { getCachedRoomIdForAlias } from "../../RoomAliasCache";
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import {
flattenSpaceHierarchyWithCache,
@@ -1249,8 +1249,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
let roomId = payload.room_id;
if (payload.room_alias && !roomId) {
const result = getCachedRoomIdForAlias(payload.room_alias);
if (result) roomId = result.roomId;
roomId = getCachedRoomIDForAlias(payload.room_alias);
}
if (!roomId) return; // we'll get re-fired with the room ID shortly

View File

@@ -117,7 +117,7 @@ export class MediaEventHelper implements IDestroyable {
/**
* Determine if the media event in question supports being hidden in the timeline.
* @param event Any matrix event.
* @returns `true` if the media can be hidden, otherwise `false`.
* @returns `true` if the media can be hidden, otherwise false.
*/
public static canHide(event: MatrixEvent): boolean {
if (!event) return false;

View File

@@ -40,7 +40,7 @@ export async function leaveRoomBehaviour(
let leavingAllVersions = true;
const history = matrixClient.getRoomUpgradeHistory(
roomId,
true,
false,
SettingsStore.getValue("feature_dynamic_room_predecessors"),
);
if (history && history.length > 0) {

View File

@@ -18,7 +18,6 @@ import {
} from "matrix-js-sdk/src/matrix";
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { uniqueId } from "lodash";
import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform";
import type BaseEventIndexManager from "../../indexing/BaseEventIndexManager";
@@ -44,7 +43,6 @@ import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager";
import { _t } from "../../languageHandler";
import { BadgeOverlayRenderer } from "../../favicon";
import GenericToast from "../../components/views/toasts/GenericToast.tsx";
interface SquirrelUpdate {
releaseNotes: string;
@@ -97,7 +95,6 @@ export default class ElectronPlatform extends BasePlatform {
private badgeOverlayRenderer?: BadgeOverlayRenderer;
private config!: IConfigOptions;
private supportedSettings?: Record<string, boolean>;
private clientStartedPromiseWithResolvers = Promise.withResolvers<void>();
public constructor() {
super();
@@ -185,27 +182,6 @@ export default class ElectronPlatform extends BasePlatform {
await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" });
});
this.electron.on("showToast", async (ev, { title, description, priority = 40 }) => {
await this.clientStartedPromiseWithResolvers.promise;
const key = uniqueId("electron_showToast_");
const onPrimaryClick = (): void => {
ToastStore.sharedInstance().dismissToast(key);
};
ToastStore.sharedInstance().addOrReplaceToast({
key,
title,
props: {
description,
primaryLabel: _t("action|dismiss"),
onPrimaryClick,
},
component: GenericToast,
priority,
});
});
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
this.initialised = this.initialise();
@@ -217,10 +193,6 @@ export default class ElectronPlatform extends BasePlatform {
if (["call_state"].includes(payload.action)) {
this.electron.send("app_onAction", payload);
}
if (payload.action === "client_started") {
this.clientStartedPromiseWithResolvers.resolve();
}
}
private async initialise(): Promise<void> {

View File

@@ -585,42 +585,4 @@ describe("LegacyCallHandler without third party protocols", () => {
expect(mockAudioBufferSourceNode.start).not.toHaveBeenCalled();
});
});
describe("sidebar state", () => {
const roomId = "test-room-id";
it("should default to showing sidebar", () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(true);
});
it("should remember sidebar state per call", () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(true);
callHandler.setCallSidebarShown(call.callId, false);
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(false);
call.emit(CallEvent.Hangup, call);
const call2 = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
cli.emit(CallEventHandlerEvent.Incoming, call2);
expect(callHandler.isCallSidebarShown(call2.callId)).toEqual(true);
});
});
});

View File

@@ -1,46 +0,0 @@
/*
* 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 { fireEvent, render } from "jest-matrix-react";
import { IntegrationsDisabledDialog } from "../../../../../src/components/views/dialogs/IntegrationsDisabledDialog.tsx";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher.ts";
import { Action } from "../../../../../src/dispatcher/actions.ts";
import { UserTab } from "../../../../../src/components/views/dialogs/UserTab.ts";
describe("<IntegrationsDisabledDialog />", () => {
const onFinished = jest.fn();
afterEach(() => {
jest.restoreAllMocks();
});
function renderComponent() {
return render(<IntegrationsDisabledDialog onFinished={onFinished} />);
}
it("should render as expected", () => {
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("should do nothing on clicking OK", () => {
const { getByText } = renderComponent();
fireEvent.click(getByText("OK"));
expect(onFinished).toHaveBeenCalled();
});
it("should open the correct user settings tab on clicking Settings", () => {
jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
const { getByText } = renderComponent();
fireEvent.click(getByText("Settings"));
expect(onFinished).toHaveBeenCalled();
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
});
});

View File

@@ -1,68 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<IntegrationsDisabledDialog /> should render as expected 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_IntegrationsDisabledDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Integrations are disabled
</h1>
</div>
<div
class="mx_IntegrationsDisabledDialog_content"
>
<p>
Enable 'Manage integrations' in Settings to do this.
</p>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
OK
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Settings
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View File

@@ -48,7 +48,6 @@ describe("HideActionButton", () => {
beforeEach(() => {
cli = getMockClientWithEventEmitter({
getRoom: jest.fn(),
getUserId: jest.fn(),
});
});
afterEach(() => {

View File

@@ -35,11 +35,10 @@ jest.mock("matrix-encrypt-attachment", () => ({
}));
describe("<MImageBody/>", () => {
const ourUserId = "@user:server";
const senderUserId = "@other_use:server";
const userId = "@user:server";
const deviceId = "DEADB33F";
const cli = getMockClientWithEventEmitter({
...mockClientMethodsUser(ourUserId),
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
...mockClientMethodsDevice(deviceId),
...mockClientMethodsCrypto(),
@@ -63,7 +62,7 @@ describe("<MImageBody/>", () => {
const encryptedMediaEvent = new MatrixEvent({
event_id: "$foo:bar",
room_id: "!room:server",
sender: senderUserId,
sender: userId,
type: EventType.RoomMessage,
content: {
body: "alt for a test image",
@@ -202,7 +201,7 @@ describe("<MImageBody/>", () => {
const event = new MatrixEvent({
room_id: "!room:server",
sender: senderUserId,
sender: userId,
type: EventType.RoomMessage,
content: {
body: "alt for a test image",
@@ -255,7 +254,7 @@ describe("<MImageBody/>", () => {
const event = new MatrixEvent({
room_id: "!room:server",
sender: senderUserId,
sender: userId,
type: EventType.RoomMessage,
content: {
body: "alt for a test image",
@@ -282,7 +281,7 @@ describe("<MImageBody/>", () => {
it("should show banner on hover", async () => {
const event = new MatrixEvent({
room_id: "!room:server",
sender: senderUserId,
sender: userId,
type: EventType.RoomMessage,
content: {
body: "alt for a test image",

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { EventType, getHttpUriForMxc, type IContent, type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { fireEvent, render, screen } from "jest-matrix-react";
import { fireEvent, render, screen, type RenderResult } from "jest-matrix-react";
import fetchMock from "fetch-mock-jest";
import { type MockedObject } from "jest-mock";
@@ -34,8 +34,7 @@ jest.mock("matrix-encrypt-attachment", () => ({
}));
describe("MVideoBody", () => {
const ourUserId = "@user:server";
const senderUserId = "@other_use:server";
const userId = "@user:server";
const deviceId = "DEADB33F";
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
@@ -43,7 +42,7 @@ describe("MVideoBody", () => {
beforeEach(() => {
cli = getMockClientWithEventEmitter({
...mockClientMethodsUser(ourUserId),
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
...mockClientMethodsDevice(deviceId),
...mockClientMethodsCrypto(),
@@ -68,7 +67,7 @@ describe("MVideoBody", () => {
const encryptedMediaEvent = new MatrixEvent({
room_id: "!room:server",
sender: senderUserId,
sender: userId,
type: EventType.RoomMessage,
event_id: "$foo:bar",
content: {
@@ -87,47 +86,10 @@ describe("MVideoBody", () => {
},
});
it("does not crash when given portrait dimensions", () => {
it("does not crash when given a portrait image", () => {
// Check for an unreliable crash caused by a fractional-sized
// image dimension being used for a CanvasImageData.
const content: IContent = {
info: {
"w": 720,
"h": 1280,
"mimetype": "video/mp4",
"size": 2495675,
"thumbnail_file": {
url: "",
key: { alg: "", key_ops: [], kty: "", k: "", ext: true },
iv: "",
hashes: {},
v: "",
},
"thumbnail_info": { mimetype: "" },
"xyz.amorgan.blurhash": "TrGl6bofof~paxWC?bj[oL%2fPj]",
},
url: "http://example.com",
};
const event = new MatrixEvent({
content,
});
const defaultProps: IBodyProps = {
mxEvent: event,
highlights: [],
highlightLink: "",
onMessageAllowed: jest.fn(),
permalinkCreator: {} as RoomPermalinkCreator,
mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper,
};
const { asFragment } = render(
<MatrixClientContext.Provider value={cli}>
<MVideoBody {...defaultProps} />
</MatrixClientContext.Provider>,
withClientContextRenderOptions(cli),
);
const { asFragment } = makeMVideoBody(720, 1280);
expect(asFragment()).toMatchSnapshot();
// If we get here, we did not crash.
});
@@ -191,39 +153,50 @@ describe("MVideoBody", () => {
expect(fetchMock).toHaveFetched(thumbUrl);
});
it("should download video if we were the sender", async () => {
fetchMock.getOnce(thumbUrl, { status: 200 });
const ourEncryptedMediaEvent = new MatrixEvent({
room_id: "!room:server",
sender: ourUserId,
type: EventType.RoomMessage,
event_id: "$foo:bar",
content: {
body: "alt for a test video",
info: {
duration: 420,
w: 40,
h: 50,
thumbnail_file: {
url: "mxc://server/encrypted-poster",
},
},
file: {
url: "mxc://server/encrypted-image",
},
},
});
const { asFragment } = render(
<MVideoBody
mxEvent={ourEncryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(ourEncryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
expect(fetchMock).toHaveFetched(thumbUrl);
expect(asFragment()).toMatchSnapshot();
});
});
});
function makeMVideoBody(w: number, h: number): RenderResult {
const content: IContent = {
info: {
"w": w,
"h": h,
"mimetype": "video/mp4",
"size": 2495675,
"thumbnail_file": {
url: "",
key: { alg: "", key_ops: [], kty: "", k: "", ext: true },
iv: "",
hashes: {},
v: "",
},
"thumbnail_info": { mimetype: "" },
"xyz.amorgan.blurhash": "TrGl6bofof~paxWC?bj[oL%2fPj]",
},
url: "http://example.com",
};
const event = new MatrixEvent({
content,
});
const defaultProps: IBodyProps = {
mxEvent: event,
highlights: [],
highlightLink: "",
onMessageAllowed: jest.fn(),
permalinkCreator: {} as RoomPermalinkCreator,
mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper,
};
const mockClient = getMockClientWithEventEmitter({
mxcUrlToHttp: jest.fn(),
getRoom: jest.fn(),
});
return render(
<MatrixClientContext.Provider value={mockClient}>
<MVideoBody {...defaultProps} />
</MatrixClientContext.Provider>,
);
}

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MVideoBody does not crash when given portrait dimensions 1`] = `
exports[`MVideoBody does not crash when given a portrait image 1`] = `
<DocumentFragment>
<span
class="mx_MVideoBody"
@@ -48,28 +48,3 @@ exports[`MVideoBody should show poster for encrypted media before downloading it
</span>
</DocumentFragment>
`;
exports[`MVideoBody with video previews/thumbnails disabled should download video if we were the sender 1`] = `
<DocumentFragment>
<span
class="mx_MVideoBody"
>
<div
class="mx_MVideoBody_container"
style="max-width: 40px; max-height: 50px;"
>
<video
class="mx_MVideoBody"
controls=""
controlslist="nodownload"
poster="https://server/_matrix/media/v3/download/server/encrypted-poster"
preload="none"
title="alt for a test video"
/>
<div
style="width: 40px; height: 50px;"
/>
</div>
</span>
</DocumentFragment>
`;

View File

@@ -8,12 +8,9 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { render } from "jest-matrix-react";
import { type MatrixCall } from "matrix-js-sdk/src/matrix";
import { type CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
import LegacyCallView from "../../../../../src/components/views/voip/LegacyCallView";
import { stubClient } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
describe("LegacyCallView", () => {
it("should exit full screen on unmount", () => {
@@ -35,70 +32,9 @@ describe("LegacyCallView", () => {
isScreensharing: jest.fn().mockReturnValue(false),
} as unknown as MatrixCall;
const { unmount } = render(<LegacyCallView call={call} sidebarShown={false} />);
const { unmount } = render(<LegacyCallView call={call} />);
expect(document.exitFullscreen).not.toHaveBeenCalled();
unmount();
expect(document.exitFullscreen).toHaveBeenCalled();
});
it("should show/hide the sidebar based on the sidebarShown prop", async () => {
stubClient();
const call = {
roomId: "test-room",
on: jest.fn(),
removeListener: jest.fn(),
getFeeds: jest.fn().mockReturnValue(
[{ local: true }, { local: false }, { local: true, screenshare: true }].map(
(x, i) =>
({
stream: { id: "test-" + i },
addListener: jest.fn(),
removeListener: jest.fn(),
getMember: jest.fn(),
isAudioMuted: jest.fn().mockReturnValue(true),
isVideoMuted: jest.fn().mockReturnValue(true),
isLocal: jest.fn().mockReturnValue(x.local),
purpose: x.screenshare && SDPStreamMetadataPurpose.Screenshare,
}) as unknown as CallFeed,
),
),
isLocalOnHold: jest.fn().mockReturnValue(false),
isRemoteOnHold: jest.fn().mockReturnValue(false),
isMicrophoneMuted: jest.fn().mockReturnValue(true),
isLocalVideoMuted: jest.fn().mockReturnValue(true),
isScreensharing: jest.fn().mockReturnValue(true),
noIncomingFeeds: jest.fn().mockReturnValue(false),
opponentSupportsSDPStreamMetadata: jest.fn().mockReturnValue(true),
} as unknown as MatrixCall;
DMRoomMap.setShared({
getUserIdForRoomId: jest.fn().mockReturnValue("test-user"),
} as unknown as DMRoomMap);
const { container, rerender } = render(<LegacyCallView call={call} sidebarShown={true} />);
expect(container.querySelector(".mx_LegacyCallViewSidebar")).toBeTruthy();
rerender(<LegacyCallView call={call} sidebarShown={true} />);
expect(container.querySelector(".mx_LegacyCallViewSidebar")).toBeTruthy();
});
it("should not show the sidebar button in picture-in-picture mode", async () => {
stubClient();
const call = {
on: jest.fn(),
removeListener: jest.fn(),
getFeeds: jest.fn().mockReturnValue([]),
isLocalOnHold: jest.fn().mockReturnValue(false),
isRemoteOnHold: jest.fn().mockReturnValue(false),
isMicrophoneMuted: jest.fn().mockReturnValue(false),
isLocalVideoMuted: jest.fn().mockReturnValue(false),
isScreensharing: jest.fn().mockReturnValue(false),
} as unknown as MatrixCall;
DMRoomMap.setShared({
getUserIdForRoomId: jest.fn().mockReturnValue("test-user"),
} as unknown as DMRoomMap);
const { container } = render(<LegacyCallView call={call} sidebarShown={false} pipMode={true} />);
expect(container.querySelector(".mx_LegacyCallViewButtons_button_sidebar")).toBeFalsy();
});
});

View File

@@ -1,72 +0,0 @@
/*
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 } from "jest-matrix-react";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
import LegacyCallView from "../../../../../src/components/views/voip/LegacyCallView";
import LegacyCallViewForRoom from "../../../../../src/components/views/voip/LegacyCallViewForRoom";
import { mkStubRoom, stubClient } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
import LegacyCallHandler from "../../../../../src/LegacyCallHandler";
jest.mock("../../../../../src/components/views/voip/LegacyCallView", () => jest.fn(() => "LegacyCallView"));
describe("LegacyCallViewForRoom", () => {
const LegacyCallViewMock = LegacyCallView as unknown as jest.Mock;
beforeEach(() => {
LegacyCallViewMock.mockClear();
});
it("should remember sidebar state, defaulting to shown", async () => {
stubClient();
const callHandler = new LegacyCallHandler();
callHandler.start();
jest.spyOn(LegacyCallHandler, "instance", "get").mockImplementation(() => callHandler);
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId: "test-room",
});
DMRoomMap.setShared({
getUserIdForRoomId: jest.fn().mockReturnValue("test-user"),
} as unknown as DMRoomMap);
const room = mkStubRoom(call.roomId, "room", MatrixClientPeg.safeGet());
MatrixClientPeg.safeGet().getRoom = jest.fn().mockReturnValue(room);
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
const { rerender } = render(
<LegacyCallViewForRoom roomId={call.roomId} resizeNotifier={new ResizeNotifier()} />,
);
let props = LegacyCallViewMock.mock.lastCall![0];
expect(props.sidebarShown).toBeTruthy(); // Sidebar defaults to shown
props.setSidebarShown(false); // Hide the sidebar
rerender(<LegacyCallViewForRoom roomId={call.roomId} resizeNotifier={new ResizeNotifier()} />);
console.log(LegacyCallViewMock.mock);
props = LegacyCallViewMock.mock.lastCall![0];
expect(props.sidebarShown).toBeFalsy();
rerender(<div> </div>); // Destroy the LegacyCallViewForRoom and LegacyCallView
LegacyCallViewMock.mockClear(); // Drop stored LegacyCallView props
rerender(<LegacyCallViewForRoom roomId={call.roomId} resizeNotifier={new ResizeNotifier()} />);
props = LegacyCallViewMock.mock.lastCall![0];
expect(props.sidebarShown).toBeFalsy(); // Value was remembered
});
});

View File

@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { act, renderHook, waitFor } from "jest-matrix-react";
import { JoinRule, MatrixEvent, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { JoinRule, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { useMediaVisible } from "../../../src/hooks/useMediaVisible";
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../test-utils";
@@ -22,18 +22,8 @@ describe("useMediaVisible", () => {
let room: Room;
const mediaPreviewConfig: MediaPreviewConfig = MediaPreviewConfigController.default;
function render({ sender }: { sender?: string } = {}) {
return renderHook(
() =>
useMediaVisible(
new MatrixEvent({
event_id: EVENT_ID,
room_id: ROOM_ID,
sender,
}),
),
withClientContextRenderOptions(matrixClient),
);
function render() {
return renderHook(() => useMediaVisible(EVENT_ID, ROOM_ID), withClientContextRenderOptions(matrixClient));
}
beforeEach(() => {
matrixClient = createTestClient();
@@ -52,62 +42,53 @@ describe("useMediaVisible", () => {
jest.restoreAllMocks();
});
it("should display media by default", () => {
const [visible] = render().result.current;
expect(visible).toEqual(true);
it("should display media by default", async () => {
const { result } = render();
expect(result.current[0]).toEqual(true);
});
it("should hide media when media previews are Off", () => {
it("should hide media when media previews are Off", async () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const [visible] = render().result.current;
expect(visible).toEqual(false);
});
it("should always show media sent by us", () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const [visible] = render({ sender: matrixClient.getUserId()! }).result.current;
expect(visible).toEqual(true);
const { result } = render();
expect(result.current[0]).toEqual(false);
});
it.each([[JoinRule.Invite], [JoinRule.Knock], [JoinRule.Restricted]])(
"should display media when media previews are Private and the join rule is %s",
(rule) => {
async (rule) => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Private;
room.currentState.getJoinRule = jest.fn().mockReturnValue(rule);
const [visible] = render().result.current;
expect(visible).toEqual(true);
const { result } = render();
expect(result.current[0]).toEqual(true);
},
);
it.each([[JoinRule.Public], ["anything_else"]])(
"should hide media when media previews are Private and the join rule is %s",
(rule) => {
async (rule) => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Private;
room.currentState.getJoinRule = jest.fn().mockReturnValue(rule);
const [visible] = render().result.current;
expect(visible).toEqual(false);
const { result } = render();
expect(result.current[0]).toEqual(false);
},
);
it("should hide media after function is called", async () => {
const { result } = render();
expect(result.current[0]).toEqual(true);
expect(result.current[1]).toBeDefined();
act(() => {
result.current[1]!(false);
result.current[1](false);
});
await waitFor(() => {
expect(result.current[0]).toEqual(false);
});
});
it("should show media after function is called", async () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const { result } = render();
expect(result.current[0]).toEqual(false);
expect(result.current[1]).toBeDefined();
act(() => {
result.current[1]!(true);
result.current[1](true);
});
await waitFor(() => {
expect(result.current[0]).toEqual(true);

View File

@@ -5,35 +5,37 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { mocked } from "jest-mock";
import * as navigator from "../../../src/utils/permalinks/navigator";
import { NavigationApi } from "../../../src/modules/Navigation.ts";
import { stubClient } from "../../test-utils";
import defaultDispatcher from "../../../src/dispatcher/dispatcher.ts";
describe("NavigationApi", () => {
const api = new NavigationApi();
describe("toMatrixToLink", () => {
it.each([
["roomId", "https://matrix.to/#/!roomId:server.com"],
["roomAlias", "https://matrix.to/#/#alias:server.com"],
["user", "https://matrix.to/#/@user:server.com"],
])("should call navigateToPermalink with the correct parameters for %s", async (_type, link) => {
it("should call navigateToPermalink with the correct parameters", async () => {
const link = "https://matrix.to/#/!roomId:server.com";
const spy = jest.spyOn(navigator, "navigateToPermalink");
await api.toMatrixToLink(link);
expect(spy).toHaveBeenCalledWith(link);
});
it("should set auto_join to true when join=true", async () => {
const link = "https://matrix.to/#/#alias:server.com?via=server.com";
it("should resolve the room alias to a room id when join=true", async () => {
const cli = stubClient();
mocked(cli.getRoomIdForAlias).mockResolvedValue({ room_id: "!roomId:server.com", servers: [] });
const link = "https://matrix.to/#/#alias:server.com";
const spy = jest.spyOn(defaultDispatcher, "dispatch");
await api.toMatrixToLink(link, true);
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
room_alias: "#alias:server.com",
auto_join: true,
action: "join_room",
roomId: "!roomId:server.com",
}),
);
});

View File

@@ -24,7 +24,6 @@ import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
import * as navigator from "../../../src/utils/permalinks/navigator.ts";
describe("ProxiedApiModule", () => {
afterEach(() => {
@@ -362,30 +361,4 @@ describe("ProxiedApiModule", () => {
expect(WidgetLayoutStore.instance.moveToContainer).toHaveBeenCalledWith(room, app, Container.Top);
});
});
describe("navigatePermalink", () => {
const api = new ProxiedModuleApi();
it("should call navigateToPermalink with the correct parameters", async () => {
const link = "https://matrix.to/#/!roomId:server.com";
const spy = jest.spyOn(navigator, "navigateToPermalink");
await api.navigatePermalink(link);
expect(spy).toHaveBeenCalledWith(link);
});
it("should set auto_join to true when join=true", async () => {
const link = "https://matrix.to/#/#alias:server.com";
const spy = jest.spyOn(defaultDispatcher, "dispatch");
await api.navigatePermalink(link, true);
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
room_alias: "#alias:server.com",
auto_join: true,
}),
);
});
});
});

View File

@@ -112,7 +112,7 @@ describe("BreadcrumbsStore", () => {
await dispatchJoinRoom(room.roomId);
// We pass the value of the dynamic predecessor setting through
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false);
});
});
@@ -134,7 +134,7 @@ describe("BreadcrumbsStore", () => {
await dispatchJoinRoom(room.roomId);
// We pass the value of the dynamic predecessor setting through
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true);
});
});

View File

@@ -43,7 +43,6 @@ import { type IApp } from "../../../src/utils/WidgetUtils-types";
import { CallStore } from "../../../src/stores/CallStore";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts";
jest.mock("../../../src/Modal");
@@ -212,22 +211,6 @@ describe("RoomViewStore", function () {
expect(roomViewStore.isJoining()).toBe(true);
});
it("can be used to view a room by alias with auto_join", async () => {
const alias = "#alias12345:server";
storeRoomAliasInCache(alias, roomId, ["server1"]);
dis.dispatch({ action: Action.ViewRoom, room_alias: alias, auto_join: true }, true);
await expect(untilDispatch(Action.ViewRoom, dis)).resolves.toEqual(
expect.objectContaining({
action: Action.ViewRoom,
room_id: roomId,
auto_join: true,
}),
);
await untilDispatch(Action.JoinRoomReady, dis);
expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: ["server1"] });
expect(roomViewStore.isJoining()).toBe(true);
});
it("can auto-join a room", async () => {
dis.dispatch({ action: Action.ViewRoom, room_id: roomId, auto_join: true });
await untilDispatch(Action.JoinRoomReady, dis);
@@ -439,8 +422,8 @@ describe("RoomViewStore", function () {
});
describe("Action.JoinRoom", () => {
it("dispatches Action.JoinRoomError and Action.AskToJoin when the join fails with 403", async () => {
const err = new MatrixError({}, 403);
it("dispatches Action.JoinRoomError and Action.AskToJoin when the join fails", async () => {
const err = new MatrixError();
jest.spyOn(dis, "dispatch");
jest.spyOn(mockClient, "joinRoom").mockRejectedValueOnce(err);

View File

@@ -41,7 +41,6 @@ import RoomListStore from "../../../src/stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../src/stores/room-list/models";
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
import { NotificationLevel } from "../../../src/stores/notifications/NotificationLevel";
import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts";
jest.useFakeTimers();
@@ -1218,15 +1217,6 @@ describe("SpaceStore", () => {
viewRoom(room1);
expect(store.activeSpace).toBe(MetaSpace.Home);
});
it("should check alias cache if switching to a room by alias", async () => {
viewRoom(room2);
storeRoomAliasInCache("#alias:server", room3, []);
store.setActiveSpace(space2, false);
defaultDispatcher.dispatch({ action: Action.ViewRoom, room_alias: "#alias:server" }, true);
expect(store.activeSpace).toBe(space1);
});
});
describe("traverseSpace", () => {

View File

@@ -28,7 +28,6 @@ import SettingsStore from "../../../../src/settings/SettingsStore";
import * as utils from "../../../../src/utils/notifications";
import * as roomMute from "../../../../src/stores/room-list/utils/roomMute";
import { Action } from "../../../../src/dispatcher/actions";
import { mocked } from "jest-mock";
describe("RoomListStoreV3", () => {
async function getRoomListStore() {
@@ -198,9 +197,6 @@ describe("RoomListStoreV3", () => {
const oldRoom = rooms[32];
// Create a new room with a predecessor event that points to oldRoom
const newRoom = new Room("!foonew:matrix.org", client, client.getSafeUserId(), {});
mocked(client.getRoomUpgradeHistory).mockImplementation((roomId) =>
roomId === newRoom.roomId ? [oldRoom, newRoom] : [],
);
const createWithPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: "@foo:foo.org",
@@ -231,41 +227,6 @@ describe("RoomListStoreV3", () => {
expect(roomIds).toContain(newRoom.roomId);
});
it("should not remove predecessor room based on non-reciprocated relationship", async () => {
const { store, rooms, client, dispatcher } = await getRoomListStore();
const oldRoom = rooms[32];
// Create a new room with a predecessor event that points to oldRoom, but oldRoom does not point back
const newRoom = new Room("!nefarious:matrix.org", client, client.getSafeUserId(), {});
const createWithPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: "@foo:foo.org",
room_id: newRoom.roomId,
content: {
predecessor: { room_id: oldRoom.roomId, event_id: "tombstone_event_id" },
},
event_id: "$create",
state_key: "",
});
upsertRoomStateEvents(newRoom, [createWithPredecessor]);
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Room.myMembership",
oldMembership: KnownMembership.Invite,
membership: KnownMembership.Join,
room: newRoom,
},
true,
);
expect(fn).toHaveBeenCalled();
const roomIds = store.getSortedRooms().map((r) => r.roomId);
expect(roomIds).toContain(oldRoom.roomId);
expect(roomIds).toContain(newRoom.roomId);
});
it("Rooms are re-inserted on m.direct event", async () => {
const { store, dispatcher, client } = await getRoomListStore();

View File

@@ -115,10 +115,6 @@ describe("RoomListStore", () => {
// Given a store we can spy on
const { store, handleRoomUpdate } = createStore();
mocked(client.getRoomUpgradeHistory).mockImplementation((roomId) =>
roomId === roomWithCreatePredecessor.roomId ? [oldRoom, roomWithCreatePredecessor] : [],
);
// When we tell it we joined a new room that has an old room as
// predecessor in the create event
const payload = {

View File

@@ -129,7 +129,7 @@ describe("leaveRoomBehaviour", () => {
it("Passes through the dynamic predecessor setting", async () => {
await leaveRoomBehaviour(client, room.roomId);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false);
});
});
@@ -143,7 +143,7 @@ describe("leaveRoomBehaviour", () => {
it("Passes through the dynamic predecessor setting", async () => {
await leaveRoomBehaviour(client, room.roomId);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true);
});
});
});

View File

@@ -21,7 +21,6 @@ import DesktopCapturerSourcePicker from "../../../../src/components/views/elemen
import ElectronPlatform from "../../../../src/vector/platform/ElectronPlatform";
import { setupLanguageMock } from "../../../setup/setupLanguage";
import { stubClient } from "../../../test-utils";
import ToastStore from "../../../../src/stores/ToastStore.ts";
jest.mock("../../../../src/rageshake/rageshake", () => ({
flush: jest.fn(),
@@ -128,30 +127,6 @@ describe("ElectronPlatform", () => {
expect(plat.ipc.call).toHaveBeenCalledWith("callDisplayMediaCallback", "source");
});
it("should show a toast when showToast is fired", async () => {
new ElectronPlatform();
dispatcher.dispatch(
{
action: "client_started",
},
true,
);
const spy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast");
const [event, handler] = getElectronEventHandlerCall("showToast")!;
handler({} as any, { title: "title", description: "description" });
expect(event).toBeTruthy();
await waitFor(() =>
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
title: "title",
props: expect.objectContaining({ description: "description" }),
}),
),
);
});
describe("updates", () => {
it("dispatches on check updates action", () => {
new ElectronPlatform();

View File

@@ -4697,6 +4697,7 @@
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-6.0.0.tgz#a07975ee46307fc31c2ec64a216b6be2b3b27fb3"
integrity sha512-Jk0NsLPCvdcuZi6an1cfyf4MDcIuoPlvja5ZWgJcORyGQZV1eLMHPYKShq9gj+EYk/BXZoPvQ1d6/T+/LSCNPA==
"@vector-im/compound-web@^8.1.2":
version "8.2.1"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-8.2.1.tgz#e16ce566c836e6c0fa3318128e57fda0a4542514"
@@ -4712,9 +4713,8 @@
classnames "^2.5.1"
vaul "^1.0.0"
"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.39.0-a6238e517f23a2f3025d9c65445914771c63b163-integrity/node_modules/bindings/wysiwyg-wasm":
"@vector-im/matrix-wysiwyg-wasm@link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.39.0-a6238e517f23a2f3025d9c65445914771c63b163-integrity/node_modules/bindings/wysiwyg-wasm":
version "0.0.0"
uid ""
"@vector-im/matrix-wysiwyg@2.39.0":
version "2.39.0"
@@ -11008,10 +11008,9 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
matrix-js-sdk@38.2.0:
version "38.2.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-38.2.0.tgz#bcda87ef767897e52afa31465a84b5bdacc80163"
integrity sha512-R3jzK8rDGi/3OXOax8jFFyxblCG9KTT5yuXAbvnZCGcpTm8lZ4mHQAn5UydVD8qiyUMNMpaaMd6/k7N+5I/yaQ==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "38.0.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ed607c48b0743ddf0fe4e0301d6155e823d2caf6"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^15.1.0"