mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-05 20:26:19 +02:00
Merge branch 'develop' of github.com:vector-im/element-web into langley/use_list_view_with_room_list
This commit is contained in:
commit
36499ecdf1
13
.github/workflows/docker.yaml
vendored
13
.github/workflows/docker.yaml
vendored
@ -139,3 +139,16 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: vectorim/element-web
|
||||
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
repository: element-hq/element-web-pro
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
event-type: image-built
|
||||
# Stable way to determine the :version
|
||||
client-payload: |-
|
||||
{
|
||||
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
|
||||
}
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
|
||||
====================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Fix downloaded attachments not being decrypted ([#30434](https://github.com/element-hq/element-web/pull/30434)). Contributed by @RiotRobot.
|
||||
|
||||
|
||||
Changes in [1.11.107](https://github.com/element-hq/element-web/releases/tag/v1.11.107) (2025-07-29)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.107",
|
||||
"version": "1.11.108",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@ -128,9 +128,9 @@
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-react": "4.3.1",
|
||||
"linkify-string": "4.3.1",
|
||||
"linkifyjs": "4.3.1",
|
||||
"linkify-react": "4.3.2",
|
||||
"linkify-string": "4.3.2",
|
||||
"linkifyjs": "4.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
@ -187,7 +187,7 @@
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.13.1",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.3",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.4",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
|
||||
@ -19,6 +19,7 @@ const clickButtonReply = async (tile: Locator) => {
|
||||
await tile.hover();
|
||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
}).toPass();
|
||||
await expect(tile.page().getByText("Replying", { exact: true })).toBeVisible();
|
||||
};
|
||||
|
||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
@ -35,8 +35,8 @@ test.describe("Header section of the room list", () => {
|
||||
|
||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
||||
|
||||
// New message should open the direct messages dialog
|
||||
await page.getByRole("menuitem", { name: "New message" }).click();
|
||||
// Start chat should open the direct messages dialog
|
||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
|
||||
@ -11,6 +11,32 @@ import { Bot } from "../../pages/bot";
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
|
||||
async function setupRoomWithMembers(
|
||||
app: any,
|
||||
page: any,
|
||||
homeserver: any,
|
||||
roomName: string,
|
||||
memberNames: string[],
|
||||
): Promise<string> {
|
||||
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
||||
const id = await app.client.createRoom({ name: roomName, visibility });
|
||||
const bots: Bot[] = [];
|
||||
|
||||
for (let i = 0; i < memberNames.length; i++) {
|
||||
const displayName = memberNames[i];
|
||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
||||
if (displayName === "Susan") {
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(id, bot.credentials?.userId);
|
||||
} else {
|
||||
await bot.joinRoom(id);
|
||||
}
|
||||
bots.push(bot);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
presence: {
|
||||
@ -25,17 +51,8 @@ test.use({
|
||||
test.describe("Memberlist", () => {
|
||||
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
|
||||
testInfo.setTimeout(testInfo.timeout + 30_000);
|
||||
const id = await app.client.createRoom({ name: ROOM_NAME });
|
||||
const newBots: Bot[] = [];
|
||||
const names = ["Bob", "Bob", "Susan"];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const displayName = names[i];
|
||||
const autoAcceptInvites = displayName !== "Susan";
|
||||
const bot = new Bot(page, homeserver, { displayName, startClient: true, autoAcceptInvites });
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(id, bot.credentials?.userId);
|
||||
newBots.push(bot);
|
||||
}
|
||||
await setupRoomWithMembers(app, page, homeserver, ROOM_NAME, names);
|
||||
});
|
||||
|
||||
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
@ -45,4 +62,37 @@ test.describe("Memberlist", () => {
|
||||
await expect(memberlist.getByText("Invited")).toHaveCount(1);
|
||||
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
||||
});
|
||||
|
||||
test("should handle scroll and click to view member profile", async ({ page, app, homeserver }) => {
|
||||
// Create a room with many members to enable scrolling
|
||||
const memberNames = Array.from({ length: 15 }, (_, i) => `Member${i.toString()}`);
|
||||
await setupRoomWithMembers(app, page, homeserver, "Large Room", memberNames);
|
||||
|
||||
// Navigate to the room and open member list
|
||||
await app.viewRoomByName("Large Room");
|
||||
|
||||
const memberlist = await app.toggleMemberlistPanel();
|
||||
|
||||
// Get the scrollable container
|
||||
const memberListContainer = memberlist.locator(".mx_AutoHideScrollbar");
|
||||
|
||||
// Scroll down to the bottom of the member list
|
||||
await app.scrollListToBottom(memberListContainer);
|
||||
|
||||
// Wait for the target member to be visible after scrolling
|
||||
const targetName = "Member14";
|
||||
const targetMember = memberlist.locator(".mx_MemberTileView_name").filter({ hasText: targetName });
|
||||
await targetMember.waitFor({ state: "visible" });
|
||||
|
||||
// Verify Alice is not visible at this point
|
||||
await expect(memberlist.locator(".mx_MemberTileView_name").filter({ hasText: "Alice" })).toHaveCount(0);
|
||||
|
||||
// Click on a member near the bottom of the list
|
||||
await expect(targetMember).toBeVisible();
|
||||
await targetMember.click();
|
||||
|
||||
// Verify that the user info screen is shown and hasn't scrolled back to top
|
||||
await expect(page.locator(".mx_UserInfo")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_profile").getByText(targetName)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.3 KiB |
@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "develop@sha256:8c2d9a93dd209a79d3e5e50cd18addfe52d80bea0ffe48a5d3e15836032eeb9d";
|
||||
const TAG = "develop@sha256:2f6fff14ff23f356705abdbf2ed62c3dd6ca2103cef4ae813714ddc199bbd76a";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@ -141,6 +141,7 @@ import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenFor
|
||||
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
|
||||
import Markdown from "../../Markdown";
|
||||
import { sanitizeHtmlParams } from "../../Linkify";
|
||||
import { isOnlyAdmin } from "../../utils/membership";
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
@ -1255,29 +1256,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client && roomToLeave) {
|
||||
const plEvent = roomToLeave.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
const plContent = plEvent ? plEvent.getContent() : {};
|
||||
const userLevels = plContent.users || {};
|
||||
const currentUserLevel = userLevels[client.getUserId()!];
|
||||
const userLevelValues = Object.values(userLevels);
|
||||
if (userLevelValues.every((x) => typeof x === "number")) {
|
||||
// If the user is the only user with highest power level
|
||||
if (isOnlyAdmin(roomToLeave)) {
|
||||
const userLevelValues = roomToLeave.getJoinedMembers().map((m) => m.powerLevel);
|
||||
|
||||
const maxUserLevel = Math.max(...(userLevelValues as number[]));
|
||||
// If the user is the only user with highest power level
|
||||
if (
|
||||
maxUserLevel === currentUserLevel &&
|
||||
userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel)
|
||||
) {
|
||||
const warning =
|
||||
maxUserLevel >= 100
|
||||
? _t("leave_room_dialog|room_leave_admin_warning")
|
||||
: _t("leave_room_dialog|room_leave_mod_warning");
|
||||
warnings.push(
|
||||
<strong className="warning" key="last_admin_warning">
|
||||
{" " /* Whitespace, otherwise the sentences get smashed together */}
|
||||
{warning}
|
||||
</strong>,
|
||||
);
|
||||
}
|
||||
|
||||
const warning =
|
||||
maxUserLevel >= 100
|
||||
? _t("leave_room_dialog|room_leave_admin_warning")
|
||||
: _t("leave_room_dialog|room_leave_mod_warning");
|
||||
warnings.push(
|
||||
<strong className="warning" key="last_admin_warning">
|
||||
{" " /* Whitespace, otherwise the sentences get smashed together */}
|
||||
{warning}
|
||||
</strong>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,12 @@ export interface IListViewProps<Item, Context>
|
||||
* @param context - The context object containing the focused key and any additional data
|
||||
* @returns JSX element representing the rendered item
|
||||
*/
|
||||
getItemComponent: (index: number, item: Item, context: ListContext<Context>) => JSX.Element;
|
||||
getItemComponent: (
|
||||
index: number,
|
||||
item: Item,
|
||||
context: ListContext<Context>,
|
||||
onFocus: (e: React.FocusEvent) => void,
|
||||
) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Optional additional context data to pass to each rendered item.
|
||||
@ -207,6 +212,20 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
virtuosoDomRef.current = element;
|
||||
}, []);
|
||||
|
||||
const getItemComponentInternal = useCallback(
|
||||
(index: number, item: Item, context: ListContext<Context>): JSX.Element => {
|
||||
const onFocus = (e: React.FocusEvent): void => {
|
||||
// If one of the item components has been focused directly, set the focused and tabIndex state
|
||||
// and stop propagation so the ListViews onFocus doesn't also handle it.
|
||||
const key = getItemKey(item);
|
||||
setIsFocused(true);
|
||||
setTabIndexKey(key);
|
||||
e.stopPropagation();
|
||||
};
|
||||
return getItemComponent(index, item, context, onFocus);
|
||||
},
|
||||
[getItemComponent, getItemKey],
|
||||
);
|
||||
/**
|
||||
* Handles focus events on the list.
|
||||
* Sets the focused state and scrolls to the focused item if it is not currently visible.
|
||||
@ -259,7 +278,7 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
data={props.items}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
itemContent={props.getItemComponent}
|
||||
itemContent={getItemComponentInternal}
|
||||
{...virtuosoProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -14,6 +14,7 @@ import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/we
|
||||
import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8";
|
||||
import BusyIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8";
|
||||
import classNames from "classnames";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import RoomAvatar from "./RoomAvatar";
|
||||
import { AvatarBadgeDecoration, useRoomAvatarViewModel } from "../../viewmodels/avatars/RoomAvatarViewModel";
|
||||
@ -37,6 +38,7 @@ export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element {
|
||||
if (!vm.badgeDecoration) return <RoomAvatar size="32px" room={room} />;
|
||||
|
||||
const icon = getAvatarDecoration(vm.badgeDecoration, vm.presence);
|
||||
const label = getDecorationLabel(vm.badgeDecoration, vm.presence);
|
||||
|
||||
// Presence indicator and video/public icons don't have the same size
|
||||
// We use different masks
|
||||
@ -48,22 +50,15 @@ export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element {
|
||||
return (
|
||||
<div className="mx_RoomAvatarView">
|
||||
<RoomAvatar className={classNames("mx_RoomAvatarView_RoomAvatar", maskClass)} size="32px" room={room} />
|
||||
{icon}
|
||||
{label ? <Tooltip label={label}>{icon}</Tooltip> : icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type PresenceDecorationProps = {
|
||||
/**
|
||||
* The presence of the user in the DM room.
|
||||
*/
|
||||
presence: NonNullable<Presence>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display the presence of a user in a DM room.
|
||||
* Get the decoration for the avatar based on the presence.
|
||||
*/
|
||||
function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element {
|
||||
function getPresenceDecoration(presence: Presence): JSX.Element {
|
||||
switch (presence) {
|
||||
case Presence.Online:
|
||||
return (
|
||||
@ -72,7 +67,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
||||
height="8px"
|
||||
className="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-accent-primary)"
|
||||
aria-label={_t("presence|online")}
|
||||
aria-label={getPresenceLabel(presence)}
|
||||
/>
|
||||
);
|
||||
case Presence.Away:
|
||||
@ -82,7 +77,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
||||
height="8px"
|
||||
className="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-quaternary)"
|
||||
aria-label={_t("presence|away")}
|
||||
aria-label={getPresenceLabel(presence)}
|
||||
/>
|
||||
);
|
||||
case Presence.Offline:
|
||||
@ -92,7 +87,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
||||
height="8px"
|
||||
className="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
aria-label={_t("presence|offline")}
|
||||
aria-label={getPresenceLabel(presence)}
|
||||
/>
|
||||
);
|
||||
case Presence.Busy:
|
||||
@ -102,7 +97,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
||||
height="8px"
|
||||
className="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
aria-label={_t("presence|busy")}
|
||||
aria-label={getPresenceLabel(presence)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -116,7 +111,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
|
||||
height="16px"
|
||||
className="mx_RoomAvatarView_icon"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
aria-label={_t("room|room_is_low_priority")}
|
||||
aria-label={getDecorationLabel(decoration, presence)}
|
||||
/>
|
||||
);
|
||||
} else if (decoration === AvatarBadgeDecoration.VideoRoom) {
|
||||
@ -126,7 +121,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
|
||||
height="16px"
|
||||
className="mx_RoomAvatarView_icon"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
aria-label={_t("room|video_room")}
|
||||
aria-label={getDecorationLabel(decoration, presence)}
|
||||
/>
|
||||
);
|
||||
} else if (decoration === AvatarBadgeDecoration.PublicRoom) {
|
||||
@ -136,10 +131,44 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
|
||||
height="16px"
|
||||
className="mx_RoomAvatarView_icon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
aria-label={_t("room|header|room_is_public")}
|
||||
aria-label={getDecorationLabel(decoration, presence)}
|
||||
/>
|
||||
);
|
||||
} else if (decoration === AvatarBadgeDecoration.Presence) {
|
||||
return <PresenceDecoration presence={presence!} />;
|
||||
return getPresenceDecoration(presence!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the label for the avatar decoration.
|
||||
* This is used for the tooltip and a11y label.
|
||||
*/
|
||||
function getDecorationLabel(decoration: AvatarBadgeDecoration, presence: Presence | null): string | undefined {
|
||||
switch (decoration) {
|
||||
case AvatarBadgeDecoration.LowPriority:
|
||||
return _t("room|room_is_low_priority");
|
||||
case AvatarBadgeDecoration.VideoRoom:
|
||||
return _t("room|video_room");
|
||||
case AvatarBadgeDecoration.PublicRoom:
|
||||
return _t("room|header|room_is_public");
|
||||
case AvatarBadgeDecoration.Presence:
|
||||
return getPresenceLabel(presence!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the label for the presence.
|
||||
* This is used for the tooltip and a11y label.
|
||||
*/
|
||||
function getPresenceLabel(presence: Presence): string {
|
||||
switch (presence) {
|
||||
case Presence.Online:
|
||||
return _t("presence|online");
|
||||
case Presence.Away:
|
||||
return _t("presence|away");
|
||||
case Presence.Offline:
|
||||
return _t("presence|offline");
|
||||
case Presence.Busy:
|
||||
return _t("presence|busy");
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,23 +15,13 @@ import BaseDialog from "../dialogs/BaseDialog";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
||||
import { filterBoolean } from "../../../utils/arrays";
|
||||
import { isOnlyAdmin } from "../../../utils/membership";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onFinished(leave: boolean, rooms?: Room[]): void;
|
||||
}
|
||||
|
||||
const isOnlyAdmin = (room: Room): boolean => {
|
||||
const userId = room.client.getSafeUserId();
|
||||
if (room.getMember(userId)?.powerLevelNorm !== 100) {
|
||||
return false; // user is not an admin
|
||||
}
|
||||
return room.getJoinedMembers().every((member) => {
|
||||
// return true if every other member has a lower power level (we are highest)
|
||||
return member.userId === userId || member.powerLevelNorm < 100;
|
||||
});
|
||||
};
|
||||
|
||||
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
||||
const spaceChildren = useMemo(() => {
|
||||
const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId));
|
||||
|
||||
@ -26,6 +26,12 @@ interface IProps {
|
||||
mediaEventHelperGet: () => MediaEventHelper | undefined;
|
||||
}
|
||||
|
||||
function useButtonTitle(loading: boolean, isEncrypted: boolean): string {
|
||||
if (!loading) return _t("action|download");
|
||||
|
||||
return isEncrypted ? _t("timeline|download_action_decrypting") : _t("timeline|download_action_downloading");
|
||||
}
|
||||
|
||||
export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: IProps): ReactElement | null {
|
||||
const mediaEventHelper = useMemo(() => mediaEventHelperGet(), [mediaEventHelperGet]);
|
||||
const downloadUrl = mediaEventHelper?.media.srcHttp ?? "";
|
||||
@ -33,6 +39,8 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I
|
||||
|
||||
const { download, loading, canDownload } = useDownloadMedia(downloadUrl, fileName, mxEvent);
|
||||
|
||||
const buttonTitle = useButtonTitle(loading, mediaEventHelper?.media.isEncrypted ?? false);
|
||||
|
||||
if (!canDownload) return null;
|
||||
|
||||
const spinner = loading ? <Spinner w={18} h={18} /> : undefined;
|
||||
@ -45,7 +53,7 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I
|
||||
return (
|
||||
<RovingAccessibleButton
|
||||
className={classes}
|
||||
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
|
||||
title={buttonTitle}
|
||||
onClick={download}
|
||||
disabled={loading}
|
||||
placement="left"
|
||||
|
||||
@ -41,7 +41,12 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
}, []);
|
||||
|
||||
const getItemComponent = useCallback(
|
||||
(index: number, item: MemberWithSeparator, context: ListContext<any>): JSX.Element => {
|
||||
(
|
||||
index: number,
|
||||
item: MemberWithSeparator,
|
||||
context: ListContext<any>,
|
||||
onFocus: (e: React.FocusEvent) => void,
|
||||
): JSX.Element => {
|
||||
const itemKey = getItemKey(item);
|
||||
const isRovingItem = itemKey === context.tabIndexKey;
|
||||
const focused = isRovingItem && context.focused;
|
||||
@ -56,6 +61,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
tabIndex={isRovingItem ? 0 : -1}
|
||||
index={index}
|
||||
memberCount={memberCount}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@ -66,6 +72,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
tabIndex={isRovingItem ? 0 : -1}
|
||||
memberIndex={index - 1} // Adjust as invites are below the separator
|
||||
memberCount={memberCount}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -73,19 +80,6 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
[isPresenceEnabled, getItemKey, memberCount],
|
||||
);
|
||||
|
||||
const handleSelectItem = useCallback(
|
||||
(item: MemberWithSeparator): void => {
|
||||
if (item !== SEPARATOR) {
|
||||
if (item.member) {
|
||||
onClickMember(item.member);
|
||||
} else {
|
||||
onClickMember(item.threePidInvite);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onClickMember],
|
||||
);
|
||||
|
||||
const isItemFocusable = useCallback((item: MemberWithSeparator): boolean => {
|
||||
return item !== SEPARATOR;
|
||||
}, []);
|
||||
|
||||
@ -24,6 +24,7 @@ interface IProps {
|
||||
showPresence?: boolean;
|
||||
focused?: boolean;
|
||||
tabIndex?: number;
|
||||
onFocus: (e: React.FocusEvent) => void;
|
||||
}
|
||||
|
||||
export function RoomMemberTileView(props: IProps): JSX.Element {
|
||||
@ -59,6 +60,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
|
||||
return (
|
||||
<MemberTileView
|
||||
onClick={vm.onClick}
|
||||
onFocus={props.onFocus}
|
||||
avatarJsx={av}
|
||||
presenceJsx={presenceJSX}
|
||||
nameJsx={nameJSX}
|
||||
|
||||
@ -19,6 +19,7 @@ interface Props {
|
||||
memberCount: number;
|
||||
focused?: boolean;
|
||||
tabIndex?: number;
|
||||
onFocus: (e: React.FocusEvent) => void;
|
||||
}
|
||||
|
||||
export function ThreePidInviteTileView(props: Props): JSX.Element {
|
||||
@ -39,6 +40,7 @@ export function ThreePidInviteTileView(props: Props): JSX.Element {
|
||||
iconJsx={iconJsx}
|
||||
focused={props.focused}
|
||||
tabIndex={props.tabIndex}
|
||||
onFocus={props.onFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ interface Props {
|
||||
avatarJsx: JSX.Element;
|
||||
nameJsx: JSX.Element | string;
|
||||
onClick: () => void;
|
||||
onFocus: (e: React.FocusEvent) => void;
|
||||
memberIndex: number;
|
||||
memberCount: number;
|
||||
ariaLabel?: string;
|
||||
@ -41,6 +42,7 @@ export function MemberTileView(props: Props): JSX.Element {
|
||||
ref={ref}
|
||||
className="mx_MemberTileView"
|
||||
onClick={props.onClick}
|
||||
onFocus={props.onFocus}
|
||||
aria-label={props?.ariaLabel}
|
||||
tabIndex={props.tabIndex}
|
||||
role="option"
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import React, { type JSX, type PropsWithChildren } from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
|
||||
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
|
||||
import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
|
||||
|
||||
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
@ -148,8 +148,8 @@ function DefaultPlaceholder({ vm }: DefaultPlaceholderProps): JSX.Element {
|
||||
direction="column"
|
||||
gap="var(--cpd-space-4x)"
|
||||
>
|
||||
<Button size="sm" kind="secondary" Icon={UserAddIcon} onClick={vm.createChatRoom}>
|
||||
{_t("action|new_message")}
|
||||
<Button size="sm" kind="secondary" Icon={ChatIcon} onClick={vm.createChatRoom}>
|
||||
{_t("action|start_chat")}
|
||||
</Button>
|
||||
{vm.canCreateRoom && (
|
||||
<Button size="sm" kind="secondary" Icon={RoomIcon} onClick={vm.createRoom}>
|
||||
|
||||
@ -14,6 +14,7 @@ import HomeIcon from "@vector-im/compound-design-tokens/assets/web/icons/home";
|
||||
import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences";
|
||||
import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings";
|
||||
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call";
|
||||
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
@ -49,7 +50,7 @@ export function RoomListHeaderView(): JSX.Element {
|
||||
{vm.displayComposeMenu ? (
|
||||
<ComposeMenu vm={vm} />
|
||||
) : (
|
||||
<IconButton aria-label={_t("action|new_message")} onClick={(e) => vm.createChatRoom(e.nativeEvent)}>
|
||||
<IconButton aria-label={_t("action|start_chat")} onClick={(e) => vm.createChatRoom(e.nativeEvent)}>
|
||||
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
|
||||
</IconButton>
|
||||
)}
|
||||
@ -143,12 +144,7 @@ function ComposeMenu({ vm }: ComposeMenuProps): JSX.Element {
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<MenuItem
|
||||
Icon={UserAddIcon}
|
||||
label={_t("action|new_message")}
|
||||
onSelect={vm.createChatRoom}
|
||||
hideChevron={true}
|
||||
/>
|
||||
<MenuItem Icon={ChatIcon} label={_t("action|start_chat")} onSelect={vm.createChatRoom} hideChevron={true} />
|
||||
{vm.canCreateRoom && (
|
||||
<MenuItem Icon={RoomIcon} label={_t("action|new_room")} onSelect={vm.createRoom} hideChevron={true} />
|
||||
)}
|
||||
|
||||
@ -93,7 +93,6 @@
|
||||
"maximise": "Maximise",
|
||||
"mention": "Mention",
|
||||
"minimise": "Minimise",
|
||||
"new_message": "New message",
|
||||
"new_room": "New room",
|
||||
"new_video_room": "New video room",
|
||||
"next": "Next",
|
||||
@ -1534,6 +1533,9 @@
|
||||
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
||||
"report_to_moderators": "Report to moderators",
|
||||
"report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
|
||||
"share_history_on_invite": "Share encrypted history with new members",
|
||||
"share_history_on_invite_description": "When inviting a user to an encrypted room that has history visibility set to \"shared\", share encrypted history with that user, and accept encrypted history when you are invited to such a room.",
|
||||
"share_history_on_invite_warning": "This feature is EXPERIMENTAL and not all security precautions are implemented. Do not enable on production accounts.",
|
||||
"sliding_sync": "Sliding Sync mode",
|
||||
"sliding_sync_description": "Under active development, cannot be disabled.",
|
||||
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
||||
@ -3373,6 +3375,7 @@
|
||||
"unable_to_decrypt": "Unable to decrypt message"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Decrypting",
|
||||
"download_action_downloading": "Downloading",
|
||||
"download_failed": "Download failed",
|
||||
"download_failed_description": "An error occurred while downloading this file",
|
||||
|
||||
@ -669,12 +669,12 @@ export class ElementCall extends Call {
|
||||
|
||||
// Splice together the Element Call URL for this call
|
||||
const params = new URLSearchParams({
|
||||
embed: "true", // We're embedding EC within another application
|
||||
confineToRoom: "true", // Only show the call interface for the configured room
|
||||
// Template variables are used, so that this can be configured using the widget data.
|
||||
skipLobby: "$skipLobby", // Skip the lobby in case we show a lobby component of our own.
|
||||
returnToLobby: "$returnToLobby", // Returns to the lobby (instead of blank screen) when the call ends. (For video rooms)
|
||||
perParticipantE2EE: "$perParticipantE2EE",
|
||||
hideHeader: "true", // Hide the header since our room header is enough
|
||||
header: "none", // Hide the header since our room header is enough
|
||||
userId: client.getUserId()!,
|
||||
deviceId: client.getDeviceId()!,
|
||||
roomId: roomId,
|
||||
|
||||
@ -205,6 +205,7 @@ export interface Settings {
|
||||
"feature_mjolnir": IFeature;
|
||||
"feature_custom_themes": IFeature;
|
||||
"feature_exclude_insecure_devices": IFeature;
|
||||
"feature_share_history_on_invite": IFeature;
|
||||
"feature_html_topic": IFeature;
|
||||
"feature_bridge_state": IFeature;
|
||||
"feature_jump_to_date": IFeature;
|
||||
@ -503,6 +504,29 @@ export const SETTINGS: Settings = {
|
||||
supportedLevelsAreOrdered: true,
|
||||
default: false,
|
||||
},
|
||||
"feature_share_history_on_invite": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Encryption,
|
||||
displayName: _td("labs|share_history_on_invite"),
|
||||
description: () => (
|
||||
<>
|
||||
{_t("labs|share_history_on_invite_description")}
|
||||
<div className="mx_SettingsFlag_microcopy">
|
||||
{_t(
|
||||
"settings|warning",
|
||||
{},
|
||||
{
|
||||
w: (sub) => <span className="mx_SettingsTab_microcopy_warning">{sub}</span>,
|
||||
description: _t("labs|share_history_on_invite_warning"),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
|
||||
supportedLevelsAreOrdered: true,
|
||||
default: false,
|
||||
},
|
||||
"useOnlyCurrentProfiles": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("settings|disable_historical_profile"),
|
||||
|
||||
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React, { type ReactNode } from "react";
|
||||
import * as utils from "matrix-js-sdk/src/utils";
|
||||
import { MatrixError, JoinRule, type Room, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixError, JoinRule, type Room, type MatrixEvent, type IJoinRoomOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type ViewRoom as ViewRoomEvent } from "@matrix-org/analytics-events/types/typescript/ViewRoom";
|
||||
@ -512,15 +512,19 @@ export class RoomViewStore extends EventEmitter {
|
||||
// take a copy of roomAlias & roomId as they may change by the time the join is complete
|
||||
const { roomAlias, roomId = payload.roomId } = this.state;
|
||||
const address = roomAlias || roomId!;
|
||||
const viaServers = this.state.viaServers || [];
|
||||
|
||||
const joinOpts: IJoinRoomOpts = {
|
||||
viaServers: this.state.viaServers || [],
|
||||
...(payload.opts ?? {}),
|
||||
};
|
||||
if (SettingsStore.getValue("feature_share_history_on_invite")) {
|
||||
joinOpts.acceptSharedHistory = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
await retry<Room, MatrixError>(
|
||||
() =>
|
||||
cli.joinRoom(address, {
|
||||
viaServers,
|
||||
...(payload.opts || {}),
|
||||
}),
|
||||
() => cli.joinRoom(address, joinOpts),
|
||||
NUM_JOIN_RETRY,
|
||||
(err) => {
|
||||
// if we received a Gateway timeout or Cloudflare timeout then retry
|
||||
|
||||
@ -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 { MatrixError, type MatrixClient, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixError, type MatrixClient, EventType, type EmptyObject, type InviteOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@ -183,7 +183,11 @@ export default class MultiInviter {
|
||||
}
|
||||
}
|
||||
|
||||
return this.matrixClient.invite(roomId, addr, this.reason);
|
||||
const opts: InviteOpts = {};
|
||||
if (this.reason !== undefined) opts.reason = this.reason;
|
||||
if (SettingsStore.getValue("feature_share_history_on_invite")) opts.shareEncryptedHistory = true;
|
||||
|
||||
return this.matrixClient.invite(roomId, addr, opts);
|
||||
} else {
|
||||
throw new Error("Unsupported address");
|
||||
}
|
||||
|
||||
@ -131,3 +131,23 @@ export async function waitForMember(
|
||||
client.removeListener(RoomStateEvent.NewMember, handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is the only joined admin in the room
|
||||
* This function will *not* cause lazy loading of room members, so if these should be included then
|
||||
* the caller needs to make sure members have been loaded.
|
||||
* @param room The room to check if the user is the only admin.
|
||||
* @returns True if the user is the only user with the highest power level, false otherwise
|
||||
*/
|
||||
export function isOnlyAdmin(room: Room): boolean {
|
||||
const currentUserLevel = room.getMember(room.client.getSafeUserId())?.powerLevel;
|
||||
|
||||
const userLevelValues = room.getJoinedMembers().map((m) => m.powerLevel);
|
||||
|
||||
const maxUserLevel = Math.max(...userLevelValues.filter((x) => typeof x === "number"));
|
||||
// If the user is the only user with highest power level
|
||||
return (
|
||||
maxUserLevel === currentUserLevel &&
|
||||
userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel)
|
||||
);
|
||||
}
|
||||
|
||||
@ -270,11 +270,17 @@ describe("<MatrixChat />", () => {
|
||||
// (must be sync otherwise the next test will start before it happens)
|
||||
act(() => defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true));
|
||||
|
||||
// that will cause the Login to kick off an update in the background, which we need to allow to finish within
|
||||
// an `act` to avoid warnings
|
||||
await flushPromises();
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
// This is a massive hack, but ...
|
||||
//
|
||||
// A lot of these tests end up completing while the login flow is still proceeding. So then, we start the next
|
||||
// test while stuff is still ongoing from the previous test, which messes up the current test (by changing
|
||||
// localStorage or opening modals, or whatever).
|
||||
//
|
||||
// There is no obvious event we could wait for which indicates that everything has completed, since each test
|
||||
// does something different. Instead...
|
||||
await act(() => sleep(200));
|
||||
});
|
||||
|
||||
resetJsDomAfterEach();
|
||||
@ -685,6 +691,8 @@ describe("<MatrixChat />", () => {
|
||||
jest.spyOn(spaceRoom, "isSpaceRoom").mockReturnValue(true);
|
||||
|
||||
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null);
|
||||
(room as any).client = mockClient;
|
||||
(spaceRoom as any).client = mockClient;
|
||||
});
|
||||
|
||||
describe("forget_room", () => {
|
||||
@ -769,6 +777,22 @@ describe("<MatrixChat />", () => {
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it("should warn when user is the last admin", async () => {
|
||||
jest.spyOn(room, "getJoinedMembers").mockReturnValue([
|
||||
{ powerLevel: 100 } as unknown as MatrixJs.RoomMember,
|
||||
{ powerLevel: 0 } as unknown as MatrixJs.RoomMember,
|
||||
]);
|
||||
jest.spyOn(room, "getMember").mockReturnValue({
|
||||
powerLevel: 100,
|
||||
} as unknown as MatrixJs.RoomMember);
|
||||
dispatchAction();
|
||||
await screen.findByRole("dialog");
|
||||
expect(
|
||||
screen.getByText(
|
||||
"You're the only administrator in this room. If you leave, nobody will be able to change room settings or take other important actions.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it("should do nothing on cancel", async () => {
|
||||
dispatchAction();
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
@ -50,6 +50,7 @@ exports[`<RoomAvatarView /> should render a low priority room decoration 1`] = `
|
||||
</span>
|
||||
<svg
|
||||
aria-label="This is a low priority room"
|
||||
aria-labelledby="«r0»"
|
||||
class="mx_RoomAvatarView_icon"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
fill="currentColor"
|
||||
@ -92,6 +93,7 @@ exports[`<RoomAvatarView /> should render a public room decoration 1`] = `
|
||||
</span>
|
||||
<svg
|
||||
aria-label="This room is public"
|
||||
aria-labelledby="«rc»"
|
||||
class="mx_RoomAvatarView_icon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@ -134,6 +136,7 @@ exports[`<RoomAvatarView /> should render a video room decoration 1`] = `
|
||||
</span>
|
||||
<svg
|
||||
aria-label="This room is a video room"
|
||||
aria-labelledby="«r6»"
|
||||
class="mx_RoomAvatarView_icon"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
fill="currentColor"
|
||||
@ -176,6 +179,7 @@ exports[`<RoomAvatarView /> should render the AWAY presence 1`] = `
|
||||
</span>
|
||||
<svg
|
||||
aria-label="Away"
|
||||
aria-labelledby="«r14»"
|
||||
class="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-quaternary)"
|
||||
fill="currentColor"
|
||||
@ -231,6 +235,7 @@ exports[`<RoomAvatarView /> should render the BUSY presence 1`] = `
|
||||
</span>
|
||||
<svg
|
||||
aria-label="Busy"
|
||||
aria-labelledby="«ru»"
|
||||
class="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
fill="currentColor"
|
||||
@ -288,6 +293,7 @@ exports[`<RoomAvatarView /> should render the OFFLINE presence 1`] = `
|
||||
</span>
|
||||
<svg
|
||||
aria-label="Offline"
|
||||
aria-labelledby="«ro»"
|
||||
class="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-tertiary)"
|
||||
fill="currentColor"
|
||||
@ -345,6 +351,7 @@ exports[`<RoomAvatarView /> should render the ONLINE presence 1`] = `
|
||||
</span>
|
||||
<svg
|
||||
aria-label="Online"
|
||||
aria-labelledby="«ri»"
|
||||
class="mx_RoomAvatarView_PresenceDecoration"
|
||||
color="var(--cpd-color-icon-accent-primary)"
|
||||
fill="currentColor"
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { MatrixEvent, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import LeaveSpaceDialog from "../../../../../src/components/views/dialogs/LeaveSpaceDialog";
|
||||
import { createTestClient, mkStubRoom } from "../../../../test-utils";
|
||||
|
||||
describe("LeaveSpaceDialog", () => {
|
||||
it("should warn about not being able to rejoin non-public space", () => {
|
||||
const mockClient = createTestClient();
|
||||
const mockSpace = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any;
|
||||
jest.spyOn(mockSpace.currentState, "getStateEvents").mockReturnValue(
|
||||
new MatrixEvent({
|
||||
type: "m.room.join_rules",
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
render(<LeaveSpaceDialog space={mockSpace} onFinished={jest.fn()} />);
|
||||
|
||||
expect(screen.getByText(/You won't be able to rejoin unless you are re-invited/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should warn if user is the only admin", () => {
|
||||
const mockClient = createTestClient();
|
||||
const mockSpace = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any;
|
||||
jest.spyOn(mockSpace, "getJoinedMembers").mockReturnValue([
|
||||
{ powerLevel: 100 } as unknown as RoomMember,
|
||||
{ powerLevel: 0 } as unknown as RoomMember,
|
||||
]);
|
||||
jest.spyOn(mockSpace, "getMember").mockReturnValue({
|
||||
powerLevel: 100,
|
||||
} as unknown as RoomMember);
|
||||
|
||||
render(<LeaveSpaceDialog space={mockSpace} onFinished={jest.fn()} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/You're the only admin of this space. Leaving it will mean no one has control over it./),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -11,14 +11,39 @@ import { mocked } from "jest-mock";
|
||||
import fetchMockJest from "fetch-mock-jest";
|
||||
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { clearAllModals, stubClient } from "../../../../test-utils";
|
||||
import DownloadActionButton from "../../../../../src/components/views/messages/DownloadActionButton";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
jest.mock("matrix-encrypt-attachment", () => ({
|
||||
decryptAttachment: jest.fn().mockResolvedValue(new Blob(["TESTFILE"], { type: "application/octet-stream" })),
|
||||
}));
|
||||
|
||||
describe("DownloadActionButton", () => {
|
||||
const plainEvent = new MatrixEvent({
|
||||
room_id: "!room:id",
|
||||
sender: "@user:id",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "test",
|
||||
msgtype: "m.image",
|
||||
url: "mxc://matrix.org/1234",
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
fetchMockJest.restore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearAllModals();
|
||||
});
|
||||
|
||||
it("should show error if media API returns one", async () => {
|
||||
const cli = stubClient();
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
@ -26,24 +51,14 @@ describe("DownloadActionButton", () => {
|
||||
(mxc) => `https://matrix.org/_matrix/media/r0/download/${mxc.slice(6)}`,
|
||||
);
|
||||
|
||||
fetchMockJest.get("https://matrix.org/_matrix/media/r0/download/matrix.org/1234", {
|
||||
fetchMockJest.getOnce("https://matrix.org/_matrix/media/r0/download/matrix.org/1234", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Not found" },
|
||||
});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:id",
|
||||
sender: "@user:id",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "test",
|
||||
msgtype: "m.image",
|
||||
url: "mxc://matrix.org/1234",
|
||||
},
|
||||
});
|
||||
const mediaEventHelper = new MediaEventHelper(event);
|
||||
const mediaEventHelper = new MediaEventHelper(plainEvent);
|
||||
|
||||
render(<DownloadActionButton mxEvent={event} mediaEventHelperGet={() => mediaEventHelper} />);
|
||||
render(<DownloadActionButton mxEvent={plainEvent} mediaEventHelperGet={() => mediaEventHelper} />);
|
||||
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
@ -57,4 +72,85 @@ describe("DownloadActionButton", () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show download tooltip on hover", async () => {
|
||||
stubClient();
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
fetchMockJest.getOnce("https://matrix.org/_matrix/media/r0/download/matrix.org/1234", "TESTFILE");
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:id",
|
||||
sender: "@user:id",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "test",
|
||||
msgtype: "m.image",
|
||||
url: "mxc://matrix.org/1234",
|
||||
},
|
||||
});
|
||||
|
||||
render(<DownloadActionButton mxEvent={event} mediaEventHelperGet={() => undefined} />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
await user.hover(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent("Download");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show downloading tooltip while unencrypted files are downloading", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
stubClient();
|
||||
|
||||
fetchMockJest.getOnce("http://this.is.a.url/matrix.org/1234", "TESTFILE");
|
||||
|
||||
const mediaEventHelper = new MediaEventHelper(plainEvent);
|
||||
|
||||
render(<DownloadActionButton mxEvent={plainEvent} mediaEventHelperGet={() => mediaEventHelper} />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
await user.hover(button);
|
||||
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent("Downloading");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show decrypting tooltip while encrypted files are downloading", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
stubClient();
|
||||
|
||||
fetchMockJest.getOnce("http://this.is.a.url/matrix.org/1234", "UFTUGJMF");
|
||||
|
||||
const e2eEvent = new MatrixEvent({
|
||||
room_id: "!room:id",
|
||||
sender: "@user:id",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "test",
|
||||
msgtype: "m.image",
|
||||
file: { url: "mxc://matrix.org/1234" },
|
||||
},
|
||||
});
|
||||
|
||||
const mediaEventHelper = new MediaEventHelper(e2eEvent);
|
||||
|
||||
render(<DownloadActionButton mxEvent={e2eEvent} mediaEventHelperGet={() => mediaEventHelper} />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
await user.hover(button);
|
||||
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent("Decrypting");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -35,7 +35,7 @@ describe("<EmptyRoomList />", () => {
|
||||
expect(screen.getByText("No chats yet")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "New message" }));
|
||||
await user.click(screen.getByRole("button", { name: "Start chat" }));
|
||||
expect(vm.createChatRoom).toHaveBeenCalled();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "New room" }));
|
||||
|
||||
@ -69,7 +69,7 @@ describe("<RoomListHeaderView />", () => {
|
||||
expect(screen.queryByRole("button", { name: "Add" })).toBeNull();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "New message" }));
|
||||
await user.click(screen.getByRole("button", { name: "Start chat" }));
|
||||
expect(defaultValue.createChatRoom).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -80,7 +80,7 @@ describe("<RoomListHeaderView />", () => {
|
||||
const openMenu = screen.getByRole("button", { name: "Add" });
|
||||
await user.click(openMenu);
|
||||
|
||||
await user.click(screen.getByRole("menuitem", { name: "New message" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Start chat" }));
|
||||
expect(defaultValue.createChatRoom).toHaveBeenCalled();
|
||||
|
||||
await user.click(openMenu);
|
||||
|
||||
@ -200,10 +200,10 @@ exports[`<EmptyRoomList /> should not render the new room button if the user doe
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
|
||||
d="m1.5 21.25 1.45-4.95a10.2 10.2 0 0 1-.712-2.1A10.2 10.2 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22q-1.125 0-2.2-.238a10.2 10.2 0 0 1-2.1-.712L2.75 22.5a.94.94 0 0 1-1-.25.94.94 0 0 1-.25-1m2.45-1.2 3.2-.95a1 1 0 0 1 .275-.062q.15-.013.275-.013.225 0 .438.038.212.036.412.137a7.4 7.4 0 0 0 1.675.6Q11.1 20 12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12q0 .9.2 1.775t.6 1.675q.176.325.188.688t-.088.712z"
|
||||
/>
|
||||
</svg>
|
||||
New message
|
||||
Start chat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -247,10 +247,10 @@ exports[`<EmptyRoomList /> should render the default placeholder when there is n
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
|
||||
d="m1.5 21.25 1.45-4.95a10.2 10.2 0 0 1-.712-2.1A10.2 10.2 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22q-1.125 0-2.2-.238a10.2 10.2 0 0 1-2.1-.712L2.75 22.5a.94.94 0 0 1-1-.25.94.94 0 0 1-.25-1m2.45-1.2 3.2-.95a1 1 0 0 1 .275-.062q.15-.013.275-.013.225 0 .438.038.212.036.412.137a7.4 7.4 0 0 0 1.675.6Q11.1 20 12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12q0 .9.2 1.775t.6 1.675q.176.325.188.688t-.088.712z"
|
||||
/>
|
||||
</svg>
|
||||
New message
|
||||
Start chat
|
||||
</button>
|
||||
<button
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57"
|
||||
|
||||
@ -217,7 +217,7 @@ exports[`<RoomListHeaderView /> compose menu should not display the compose menu
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="New message"
|
||||
aria-label="Start chat"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
|
||||
@ -35,7 +35,9 @@ describe("MemberTileView", () => {
|
||||
});
|
||||
|
||||
it("should not display an E2EIcon when the e2E status = normal", () => {
|
||||
const { container } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
|
||||
const { container } = render(
|
||||
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
|
||||
);
|
||||
const e2eIcon = container.querySelector(".mx_E2EIconView");
|
||||
expect(e2eIcon).toBeNull();
|
||||
expect(container).toMatchSnapshot();
|
||||
@ -47,7 +49,9 @@ describe("MemberTileView", () => {
|
||||
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||
} as unknown as UserVerificationStatus);
|
||||
|
||||
const { container } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
|
||||
const { container } = render(
|
||||
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
|
||||
);
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
|
||||
expect(screen.getByText("This user has not verified all of their sessions.")).toBeInTheDocument();
|
||||
@ -68,7 +72,9 @@ describe("MemberTileView", () => {
|
||||
crossSigningVerified: true,
|
||||
} as DeviceVerificationStatus);
|
||||
|
||||
const { container } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
|
||||
const { container } = render(
|
||||
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
|
||||
@ -81,15 +87,21 @@ describe("MemberTileView", () => {
|
||||
|
||||
it("renders user labels correctly", async () => {
|
||||
member.powerLevel = 50;
|
||||
const { container: container1 } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
|
||||
const { container: container1 } = render(
|
||||
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
|
||||
);
|
||||
expect(container1).toHaveTextContent("Moderator");
|
||||
|
||||
member.powerLevel = 100;
|
||||
const { container: container2 } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
|
||||
const { container: container2 } = render(
|
||||
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
|
||||
);
|
||||
expect(container2).toHaveTextContent("Admin");
|
||||
|
||||
member.isInvite = true;
|
||||
const { container: container3 } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
|
||||
const { container: container3 } = render(
|
||||
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
|
||||
);
|
||||
expect(container3).toHaveTextContent("Invited");
|
||||
});
|
||||
});
|
||||
@ -110,7 +122,12 @@ describe("MemberTileView", () => {
|
||||
it("renders ThreePidInvite correctly", async () => {
|
||||
const [{ threePidInvite }] = getPending3PidInvites(room);
|
||||
const { container } = render(
|
||||
<ThreePidInviteTileView threePidInvite={threePidInvite!} memberIndex={0} memberCount={1} />,
|
||||
<ThreePidInviteTileView
|
||||
threePidInvite={threePidInvite!}
|
||||
memberIndex={0}
|
||||
memberCount={1}
|
||||
onFocus={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@ -305,6 +305,82 @@ describe("ListView", () => {
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not scroll to top when clicking an item after manual scroll", () => {
|
||||
// Create a larger list to enable meaningful scrolling
|
||||
const largerItems = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `item-${i}`,
|
||||
name: `Item ${i}`,
|
||||
}));
|
||||
|
||||
const mockOnClick = jest.fn();
|
||||
|
||||
mockGetItemComponent.mockImplementation(
|
||||
(index: number, item: TestItemWithSeparator, context: any, onFocus: (e: React.FocusEvent) => void) => {
|
||||
const itemKey = typeof item === "string" ? item : item.id;
|
||||
const isFocused = context.tabIndexKey === itemKey;
|
||||
return (
|
||||
<div
|
||||
className="mx_item"
|
||||
data-testid={`row-${index}`}
|
||||
tabIndex={isFocused ? 0 : -1}
|
||||
onClick={() => mockOnClick(item)}
|
||||
onFocus={onFocus}
|
||||
>
|
||||
{item === SEPARATOR_ITEM ? "---" : (item as TestItem).name}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const { container } = renderListViewWithHeight({ items: largerItems });
|
||||
const listContainer = screen.getByRole("grid");
|
||||
|
||||
// Step 1: Focus the list initially (this sets tabIndexKey to first item: "item-0")
|
||||
fireEvent.focus(listContainer);
|
||||
|
||||
// Verify first item is focused initially and tabIndexKey is set to first item
|
||||
let items = container.querySelectorAll(".mx_item");
|
||||
expect(items[0]).toHaveAttribute("tabindex", "0");
|
||||
expect(items[0]).toHaveAttribute("data-testid", "row-0");
|
||||
|
||||
// Step 2: Simulate manual scrolling (mouse wheel, scroll bar drag, etc.)
|
||||
// This changes which items are visible but DOES NOT change tabIndexKey
|
||||
// tabIndexKey should still point to "item-0" but "item-0" is no longer visible
|
||||
fireEvent.scroll(listContainer, { target: { scrollTop: 300 } });
|
||||
|
||||
// Step 3: After scrolling, different items should now be visible
|
||||
// but tabIndexKey should still point to "item-0" (which is no longer visible)
|
||||
items = container.querySelectorAll(".mx_item");
|
||||
|
||||
// Verify that item-0 is no longer in the DOM (because it's scrolled out of view)
|
||||
const item0 = container.querySelector("[data-testid='row-0']");
|
||||
expect(item0).toBeNull();
|
||||
|
||||
// Find a visible item to click on (should be items from further down the list)
|
||||
const visibleItems = container.querySelectorAll(".mx_item");
|
||||
expect(visibleItems.length).toBeGreaterThan(0);
|
||||
const clickTargetItem = visibleItems[0]; // Click on the first visible item
|
||||
|
||||
// Click on the visible item
|
||||
fireEvent.click(clickTargetItem);
|
||||
|
||||
// The click should trigger the onFocus callback, which updates the tabIndexKey
|
||||
// This simulates the real user interaction where clicking an item focuses it
|
||||
fireEvent.focus(clickTargetItem);
|
||||
|
||||
// Verify the click was handled
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
|
||||
// With the fix applied: the clicked item should become focused (tabindex="0")
|
||||
// This validates that the fix prevents unwanted scrolling back to the top
|
||||
expect(clickTargetItem).toHaveAttribute("tabindex", "0");
|
||||
|
||||
// The key validation: ensure we haven't scrolled back to the top
|
||||
// item-0 should still not be visible (if the fix is working)
|
||||
const item0AfterClick = container.querySelector("[data-testid='row-0']");
|
||||
expect(item0AfterClick).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
|
||||
@ -440,6 +440,17 @@ describe("RoomViewStore", function () {
|
||||
});
|
||||
expect(mocked(dis.dispatch).mock.calls[2][0]).toEqual({ action: "prompt_ask_to_join" });
|
||||
});
|
||||
|
||||
it("sets 'acceptSharedHistory' if that option is enabled", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => {
|
||||
return settingName === "feature_share_history_on_invite"; // this is enabled, everything else is disabled.
|
||||
});
|
||||
|
||||
dis.dispatch({ action: Action.ViewRoom, room_id: roomId });
|
||||
dis.dispatch({ action: Action.JoinRoom });
|
||||
await untilDispatch(Action.JoinRoomReady, dis);
|
||||
expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { acceptSharedHistory: true, viaServers: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Action.JoinRoomError", () => {
|
||||
|
||||
@ -96,9 +96,9 @@ describe("MultiInviter", () => {
|
||||
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
||||
|
||||
expect(client.invite).toHaveBeenCalledTimes(3);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
|
||||
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {});
|
||||
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {});
|
||||
|
||||
expectAllInvitedResult(result);
|
||||
});
|
||||
@ -114,9 +114,9 @@ describe("MultiInviter", () => {
|
||||
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
||||
|
||||
expect(client.invite).toHaveBeenCalledTimes(3);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
|
||||
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {});
|
||||
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {});
|
||||
|
||||
expectAllInvitedResult(result);
|
||||
});
|
||||
@ -129,7 +129,7 @@ describe("MultiInviter", () => {
|
||||
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
||||
|
||||
expect(client.invite).toHaveBeenCalledTimes(1);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
|
||||
|
||||
// The resolved state is 'invited' for all users.
|
||||
// With the above client expectations, the test ensures that only the first user is invited.
|
||||
@ -231,5 +231,15 @@ describe("MultiInviter", () => {
|
||||
`"This space is unfederated. You cannot invite people from external servers."`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should set shareEncryptedHistory if that setting is enabled", async () => {
|
||||
mocked(SettingsStore.getValue).mockImplementation((settingName, roomId, value) => {
|
||||
return settingName === "feature_share_history_on_invite"; // this is enabled, everything else is disabled.
|
||||
});
|
||||
await inviter.invite([MXID1]);
|
||||
|
||||
expect(client.invite).toHaveBeenCalledTimes(1);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, { shareEncryptedHistory: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
34
yarn.lock
34
yarn.lock
@ -1687,10 +1687,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.3.0.tgz#6067fa654174d1dd0953447bb036e38f9dfa51a5"
|
||||
integrity sha512-rEV0xnT/tNYPIdqHWWiz2KZo96UeZR0YChfoVLiPT46ZlEYyxqkjxT5bOm1eL2/CiYRe8t1yka3UDkIjq481/g==
|
||||
|
||||
"@element-hq/element-web-playwright-common@^1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-1.4.3.tgz#c33217032e805a0668fbf3fa09929aac9acedb09"
|
||||
integrity sha512-WrvScEsXTBreYmOMK2AiAA/ifAbgOrctolex2LRO0Z0TUkDF5Bh2sg6MBTK8i11EO+ifsy2eCLJtAQ//Yzj1GA==
|
||||
"@element-hq/element-web-playwright-common@^1.4.4":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-1.4.4.tgz#d58dba7b5b4198f2fc137e1bdd1ad82c2cee46fb"
|
||||
integrity sha512-QnWz8dlRuQHZYZT9ewrcN++l7gQ0Kf+oZwMCi0k1TBf8Za40r5ibNrgZqZYyCoItBc8LGTVL3yOrUfzN4Dm2Qw==
|
||||
dependencies:
|
||||
"@axe-core/playwright" "^4.10.1"
|
||||
"@testcontainers/postgresql" "^11.0.0"
|
||||
@ -4552,7 +4552,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.39.0.tgz#a6238e517f23a2f3025d9c65445914771c63b163"
|
||||
integrity sha512-OROXnzPcQWrCMoUpIrCKEC4FYU+9SsRomUgu+VbJwWtBDkCbfvLD4z6w/mgiADw3iTUpBPgmcWJoGxesFuB20Q==
|
||||
dependencies:
|
||||
"@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"
|
||||
|
||||
"@vitest/expect@3.2.4":
|
||||
version "3.2.4"
|
||||
@ -10510,20 +10510,20 @@ linkify-it@^4.0.1:
|
||||
dependencies:
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
linkify-react@4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/linkify-react/-/linkify-react-4.3.1.tgz#0655632d654a881e54d955ec12b1ab817d879f50"
|
||||
integrity sha512-w8ahBdCwF9C/doS4V3nE93QF1oyORmosvi8UEUbpHYws077eGzhkxUzJQcE2/SU5Q2K7SD80M4ybwwZGHErx5Q==
|
||||
linkify-react@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/linkify-react/-/linkify-react-4.3.2.tgz#8d47fb0ad96ab5b38c07bfbebdcbc57794430693"
|
||||
integrity sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==
|
||||
|
||||
linkify-string@4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.3.1.tgz#d6f8b7166d588a64943e3bb23302ce44047f61a2"
|
||||
integrity sha512-1AnH52wZwuJi+skG/9dUphhQEUblVGSf0ntkM8z21RS9bF7xR0qPpqnNTyCo2Obqs5MR5wi8y5wOLPoBbzxm2w==
|
||||
linkify-string@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.3.2.tgz#535f7a3c25a8c83b862aa3263d6cb09fd4e4b3f4"
|
||||
integrity sha512-JqBuQpSa+CSj2tskIII70SKOjPfjXwDFyjRRNFTrlg76gp2nap36xeRj/cWaXxukqBNrxM+L07XyKRsUtH/DpQ==
|
||||
|
||||
linkifyjs@4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.1.tgz#1f246ebf4be040002accd1f4535b6af7c7e37898"
|
||||
integrity sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==
|
||||
linkifyjs@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
|
||||
integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==
|
||||
|
||||
lint-staged@^16.0.0:
|
||||
version "16.1.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user