mirror of
https://github.com/element-hq/element-web.git
synced 2025-09-17 11:04:05 +02:00
Compare commits
2 Commits
master
...
travis/msc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
527b44bb7a | ||
|
|
b934d6b290 |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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.
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
3
src/@types/global.d.ts
vendored
3
src/@types/global.d.ts
vendored
@@ -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},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -245,7 +245,6 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
sidebarShown={false}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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][]>(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -48,7 +48,6 @@ describe("HideActionButton", () => {
|
||||
beforeEach(() => {
|
||||
cli = getMockClientWithEventEmitter({
|
||||
getRoom: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
11
yarn.lock
11
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user