Compare commits
9 Commits
hs/a11y-up
...
robin/call
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d7a9ee847 | ||
|
|
5df083f009 | ||
|
|
991ce70209 | ||
|
|
60de81b824 | ||
|
|
20d8abf7c2 | ||
|
|
fda658182a | ||
|
|
9bfea92b66 | ||
|
|
962136d453 | ||
|
|
917d53a56f |
2
.github/workflows/docker.yaml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
--rm \
|
||||
-e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \
|
||||
-dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \
|
||||
-v $(pwd)/modules:/tmp/element-web-modules \
|
||||
-v $(pwd)/modules:/modules \
|
||||
"$IMAGEID" \
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field
|
||||
# Loads modules from `/modules` into config.json's `modules` field
|
||||
|
||||
set -e
|
||||
|
||||
@@ -15,15 +15,15 @@ mkdir -p /tmp/element-web-config
|
||||
cp /app/config*.json /tmp/element-web-config/
|
||||
|
||||
# If there are modules to be loaded
|
||||
if [ -d "/tmp/element-web-modules" ]; then
|
||||
cd /tmp/element-web-modules
|
||||
if [ -d "/modules" ]; then
|
||||
cd /modules
|
||||
|
||||
for MODULE in *
|
||||
do
|
||||
# If the module has a package.json, use its main field as the entrypoint
|
||||
ENTRYPOINT="index.js"
|
||||
if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then
|
||||
ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json")
|
||||
if [ -f "/modules/$MODULE/package.json" ]; then
|
||||
ENTRYPOINT=$(jq -r '.main' "/modules/$MODULE/package.json")
|
||||
fi
|
||||
|
||||
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
|
||||
|
||||
@@ -22,7 +22,7 @@ server {
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
location /modules {
|
||||
alias /tmp/element-web-modules;
|
||||
alias /modules;
|
||||
}
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
|
||||
@@ -67,7 +67,7 @@ image as root (`docker run --user 0`) or, better, change the port that nginx
|
||||
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
||||
|
||||
[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
|
||||
by being made available (e.g. via bind mount) in a directory within `/tmp/element-web-modules/`.
|
||||
by being made available (e.g. via bind mount) in a directory within `/modules/`.
|
||||
The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
|
||||
These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
|
||||
|
||||
@@ -75,7 +75,7 @@ If you wish to use docker in read-only mode,
|
||||
you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
|
||||
but additionally include the following directories:
|
||||
|
||||
- /tmp/element-web-config/
|
||||
- /tmp/
|
||||
- /etc/nginx/conf.d/
|
||||
|
||||
The behaviour of the docker image can be customised via the following
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 { expect, test } from "../../../element-web-test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
test.describe("Room list filters and sort", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: true,
|
||||
},
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
function getPrimaryFilters(page: Page) {
|
||||
return page.getByRole("listbox", { name: "Room list filters" });
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
|
||||
const unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
|
||||
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, favouriteId);
|
||||
});
|
||||
|
||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
const allFilters = await primaryFilters.locator("option").all();
|
||||
for (const filter of allFilters) {
|
||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
||||
}
|
||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019-2023 The Matrix.org Foundation C.I.C
|
||||
Copyright 2017-2019 New Vector Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
@@ -217,7 +217,7 @@ textarea {
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
:not(.mx_ChangePasswordForm input) > input[type="password"],
|
||||
input[type="password"]:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
@@ -592,7 +592,6 @@ legend {
|
||||
*/
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_EncryptionUserSettingsTab button,
|
||||
.mx_UserProfileSettings button,
|
||||
.mx_ShareDialog button,
|
||||
@@ -621,7 +620,6 @@ legend {
|
||||
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
[class|="maplibregl"],
|
||||
.mx_AccessibleButton,
|
||||
@@ -636,7 +634,6 @@ legend {
|
||||
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
[class|="maplibregl"],
|
||||
.mx_AccessibleButton,
|
||||
@@ -656,7 +653,6 @@ legend {
|
||||
.mx_Dialog input[type="submit"].mx_Dialog_primary,
|
||||
.mx_Dialog_buttons
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
.mx_AccessibleButton,
|
||||
.mx_UserProfileSettings button,
|
||||
@@ -676,7 +672,6 @@ legend {
|
||||
.mx_Dialog input[type="submit"].danger,
|
||||
.mx_Dialog_buttons
|
||||
button.danger:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
.mx_AccessibleButton,
|
||||
.mx_UserProfileSettings button,
|
||||
@@ -699,7 +694,6 @@ legend {
|
||||
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
[class|="maplibregl"],
|
||||
.mx_AccessibleButton,
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
@import "./views/rooms/RoomListPanel/_RoomListCell.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListPanel.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListSearch.pcss";
|
||||
@import "./views/rooms/_AppsDrawer.pcss";
|
||||
@import "./views/rooms/_Autocomplete.pcss";
|
||||
|
||||
@@ -12,4 +12,5 @@ Please see LICENSE files in the repository root for full details.
|
||||
align-items: var(--mx-flex-align, unset);
|
||||
justify-content: var(--mx-flex-justify, unset);
|
||||
gap: var(--mx-flex-gap, unset);
|
||||
flex-wrap: var(--mx-flex-wrap, unset);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.mx_RoomListPrimaryFilters {
|
||||
margin: unset;
|
||||
list-style-type: none;
|
||||
padding: var(--cpd-space-2x) var(--cpd-space-3x);
|
||||
}
|
||||
@@ -39,6 +39,11 @@ type FlexProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any
|
||||
* @default start
|
||||
*/
|
||||
justify?: "start" | "center" | "end" | "space-between";
|
||||
/**
|
||||
* The wrapping of the flex children
|
||||
* @default nowrap
|
||||
*/
|
||||
wrap?: "wrap" | "nowrap" | "wrap-reverse";
|
||||
/**
|
||||
* The spacing between the flex children, expressed with the CSS unit
|
||||
* @default 0
|
||||
@@ -60,6 +65,7 @@ export function Flex<T extends keyof JSX.IntrinsicElements | JSXElementConstruct
|
||||
align = "start",
|
||||
justify = "start",
|
||||
gap = "0",
|
||||
wrap = "nowrap",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
@@ -71,8 +77,9 @@ export function Flex<T extends keyof JSX.IntrinsicElements | JSXElementConstruct
|
||||
"--mx-flex-align": align,
|
||||
"--mx-flex-justify": justify,
|
||||
"--mx-flex-gap": gap,
|
||||
"--mx-flex-wrap": wrap,
|
||||
}),
|
||||
[align, direction, display, gap, justify],
|
||||
[align, direction, display, gap, justify, wrap],
|
||||
);
|
||||
|
||||
return React.createElement(as, { ...props, className: classNames("mx_Flex", className), style }, children);
|
||||
|
||||
@@ -6,24 +6,15 @@ 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 React, { type RefCallback, type RefObject, useCallback, useMemo, useState } from "react";
|
||||
import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import type { Score, ZxcvbnResult } from "@zxcvbn-ts/core";
|
||||
import type { ZxcvbnResult } from "@zxcvbn-ts/core";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import withValidation, { type IValidationResult } from "../elements/Validation";
|
||||
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
|
||||
import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||
import { type IInputProps } from "../elements/Field";
|
||||
import Field, { type IInputProps } from "../elements/Field";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Field, Label, PasswordInput, Progress } from "@vector-im/compound-web";
|
||||
|
||||
const SCORE_TINT: Record<Score, "red" | "orange" | "lime" | "green"> ={
|
||||
"0": "red",
|
||||
"1": "red",
|
||||
"2": "orange",
|
||||
"3": "lime",
|
||||
"4": "green"
|
||||
};
|
||||
|
||||
interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
|
||||
autoFocus?: boolean;
|
||||
@@ -31,44 +22,43 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
|
||||
className?: string;
|
||||
minScore: 0 | 1 | 2 | 3 | 4;
|
||||
value: string;
|
||||
fieldRef?: RefCallback<HTMLInputElement> | RefObject<HTMLInputElement>;
|
||||
fieldRef?: RefCallback<Field> | RefObject<Field>;
|
||||
// Additional strings such as a username used to catch bad passwords
|
||||
userInputs?: string[];
|
||||
|
||||
label: TranslationKey;
|
||||
labelEnterPassword?: TranslationKey;
|
||||
labelStrongPassword?: TranslationKey;
|
||||
labelAllowedButUnsafe?: TranslationKey;
|
||||
// tooltipAlignment?: ComponentProps<typeof Field>["tooltipAlignment"];
|
||||
labelEnterPassword: TranslationKey;
|
||||
labelStrongPassword: TranslationKey;
|
||||
labelAllowedButUnsafe: TranslationKey;
|
||||
tooltipAlignment?: ComponentProps<typeof Field>["tooltipAlignment"];
|
||||
|
||||
onChange(ev: React.FormEvent<HTMLElement>): void;
|
||||
onValidate?(result: IValidationResult): void;
|
||||
}
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
label: _td("common|password"),
|
||||
labelEnterPassword: _td("auth|password_field_label"),
|
||||
labelStrongPassword: _td("auth|password_field_strong_label"),
|
||||
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
|
||||
};
|
||||
class PassphraseField extends PureComponent<IProps> {
|
||||
public static defaultProps = {
|
||||
label: _td("common|password"),
|
||||
labelEnterPassword: _td("auth|password_field_label"),
|
||||
labelStrongPassword: _td("auth|password_field_strong_label"),
|
||||
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
|
||||
};
|
||||
|
||||
const PassphraseField: React.FC<IProps> = (props) => {
|
||||
const { labelEnterPassword, userInputs, minScore, label, labelStrongPassword, labelAllowedButUnsafe, className, id, fieldRef, autoFocus, onChange, onValidate} = {...DEFAULT_PROPS, ...props};
|
||||
const validateFn = useMemo(() => withValidation<{}, ZxcvbnResult | null>({
|
||||
public readonly validate = withValidation<this, ZxcvbnResult | null>({
|
||||
description: function (complexity) {
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <Progress tint={SCORE_TINT[score]} size="sm" value={score} max={4} />
|
||||
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
||||
},
|
||||
deriveData: async ({ value }): Promise<ZxcvbnResult | null> => {
|
||||
if (!value) return null;
|
||||
const { scorePassword } = await import("../../../utils/PasswordScorer");
|
||||
return scorePassword(MatrixClientPeg.get(), value, userInputs);
|
||||
return scorePassword(MatrixClientPeg.get(), value, this.props.userInputs);
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t(labelEnterPassword),
|
||||
invalid: () => _t(this.props.labelEnterPassword),
|
||||
},
|
||||
{
|
||||
key: "complexity",
|
||||
@@ -76,7 +66,7 @@ const PassphraseField: React.FC<IProps> = (props) => {
|
||||
if (!value || !complexity) {
|
||||
return false;
|
||||
}
|
||||
const safe = complexity.score >= minScore;
|
||||
const safe = complexity.score >= this.props.minScore;
|
||||
const allowUnsafe = SdkConfig.get("dangerously_allow_unsafe_and_insecure_passwords");
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
@@ -84,10 +74,10 @@ const PassphraseField: React.FC<IProps> = (props) => {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (complexity && complexity.score >= minScore) {
|
||||
return _t(labelStrongPassword);
|
||||
if (complexity && complexity.score >= this.props.minScore) {
|
||||
return _t(this.props.labelStrongPassword);
|
||||
}
|
||||
return _t(labelAllowedButUnsafe);
|
||||
return _t(this.props.labelAllowedButUnsafe);
|
||||
},
|
||||
invalid: function (complexity) {
|
||||
if (!complexity) {
|
||||
@@ -99,26 +89,33 @@ const PassphraseField: React.FC<IProps> = (props) => {
|
||||
},
|
||||
],
|
||||
memoize: true,
|
||||
}), [labelEnterPassword, userInputs, minScore, labelStrongPassword, labelAllowedButUnsafe]);
|
||||
const [feedback, setFeedback]= useState<string|JSX.Element>();
|
||||
});
|
||||
|
||||
const onInputChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((ev) => {
|
||||
onChange(ev);
|
||||
validateFn({
|
||||
value: ev.target.value,
|
||||
focused: true,
|
||||
}).then((v) => {
|
||||
setFeedback(v.feedback);
|
||||
onValidate?.(v);
|
||||
});
|
||||
}, [validateFn]);
|
||||
public onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validate(fieldState);
|
||||
if (this.props.onValidate) {
|
||||
this.props.onValidate(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
return <Field id={id} name="password" className={classNames("mx_PassphraseField", className)}>
|
||||
<Label>{_t(label)}</Label>
|
||||
<PasswordInput ref={fieldRef} autoFocus={autoFocus} onChange={onInputChange} />
|
||||
{feedback}
|
||||
</Field>
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<Field
|
||||
id={this.props.id}
|
||||
autoFocus={this.props.autoFocus}
|
||||
className={classNames("mx_PassphraseField", this.props.className)}
|
||||
ref={this.props.fieldRef}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={_t(this.props.label)}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
onValidate={this.onValidate}
|
||||
tooltipAlignment={this.props.tooltipAlignment}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PassphraseField;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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, { type JSX } from "react";
|
||||
import { ChatFilter } from "@vector-im/compound-web";
|
||||
|
||||
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
interface RoomListPrimaryFiltersProps {
|
||||
/**
|
||||
* The view model for the room list
|
||||
*/
|
||||
vm: RoomListViewState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The primary filters for the room list
|
||||
*/
|
||||
export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX.Element {
|
||||
return (
|
||||
<Flex
|
||||
as="ul"
|
||||
role="listbox"
|
||||
aria-label={_t("room_list|primary_filters")}
|
||||
className="mx_RoomListPrimaryFilters"
|
||||
align="center"
|
||||
gap="var(--cpd-space-2x)"
|
||||
wrap="wrap"
|
||||
>
|
||||
{vm.primaryFilters.map((filter) => (
|
||||
<li role="option" aria-selected={filter.active} key={filter.name}>
|
||||
<ChatFilter selected={filter.active} onClick={filter.toggle}>
|
||||
{filter.name}
|
||||
</ChatFilter>
|
||||
</li>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -9,12 +9,17 @@ import React, { type JSX } from "react";
|
||||
|
||||
import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { RoomList } from "./RoomList";
|
||||
import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
|
||||
|
||||
/**
|
||||
* Host the room list and the (future) room filters
|
||||
*/
|
||||
export function RoomListView(): JSX.Element {
|
||||
const vm = useRoomListViewModel();
|
||||
// Room filters will be added soon
|
||||
return <RoomList vm={vm} />;
|
||||
return (
|
||||
<>
|
||||
<RoomListPrimaryFilters vm={vm} />
|
||||
<RoomList vm={vm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,16 +95,9 @@ export function attachMentions(
|
||||
const userMentions = new Set<string>();
|
||||
let roomMention = false;
|
||||
|
||||
// If there's a reply, initialize the mentioned users as the sender of that
|
||||
// event + any mentioned users in that event.
|
||||
// If there's a reply, initialize the mentioned users as the sender of that event.
|
||||
if (replyToEvent) {
|
||||
userMentions.add(replyToEvent.sender!.userId);
|
||||
// TODO What do we do if the reply event *doeesn't* have this property?
|
||||
// Try to fish out replies from the contents?
|
||||
const userIds = replyToEvent.getContent()["m.mentions"]?.user_ids;
|
||||
if (Array.isArray(userIds)) {
|
||||
userIds.forEach((userId) => userMentions.add(userId));
|
||||
}
|
||||
}
|
||||
|
||||
// If user provided content is available, check to see if any users are mentioned.
|
||||
|
||||
@@ -9,12 +9,16 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import Field from "../elements/Field";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton, { type AccessibleButtonKind } from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
|
||||
import { UserFriendlyError, _t, _td } from "../../../languageHandler";
|
||||
import { PASSWORD_MIN_SCORE } from "../auth/RegistrationForm";
|
||||
import { Root, Field as CpdField, PasswordInput, Label, InlineSpinner, HelpMessage, Button } from "@vector-im/compound-web";
|
||||
import Modal from "../../../Modal";
|
||||
import PassphraseField from "../auth/PassphraseField";
|
||||
import { PASSWORD_MIN_SCORE } from "../auth/RegistrationForm";
|
||||
import SetEmailDialog from "../dialogs/SetEmailDialog";
|
||||
|
||||
const FIELD_OLD_PASSWORD = "field_old_password";
|
||||
const FIELD_NEW_PASSWORD = "field_new_password";
|
||||
@@ -30,11 +34,19 @@ enum Phase {
|
||||
interface IProps {
|
||||
onFinished: (outcome: { didSetEmail?: boolean }) => void;
|
||||
onError: (error: Error) => void;
|
||||
rowClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonKind?: AccessibleButtonKind;
|
||||
buttonLabel?: string;
|
||||
confirm?: boolean;
|
||||
// Whether to autoFocus the new password input
|
||||
autoFocusNewPasswordInput?: boolean;
|
||||
className?: string;
|
||||
shouldAskForEmail?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
fieldValid: Partial<Record<FieldType, IValidationResult>>;
|
||||
fieldValid: Partial<Record<FieldType, boolean>>;
|
||||
phase: Phase;
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
@@ -42,13 +54,15 @@ interface IState {
|
||||
}
|
||||
|
||||
export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
private [FIELD_OLD_PASSWORD]: HTMLInputElement | null = null;
|
||||
private [FIELD_NEW_PASSWORD]: HTMLInputElement | null = null;
|
||||
private [FIELD_NEW_PASSWORD_CONFIRM]: HTMLInputElement | null = null;
|
||||
private [FIELD_OLD_PASSWORD]: Field | null = null;
|
||||
private [FIELD_NEW_PASSWORD]: Field | null = null;
|
||||
private [FIELD_NEW_PASSWORD_CONFIRM]: Field | null = null;
|
||||
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
onFinished() {},
|
||||
onError() {},
|
||||
|
||||
confirm: true,
|
||||
};
|
||||
|
||||
public constructor(props: IProps) {
|
||||
@@ -86,7 +100,15 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
cli.setPassword(authDict, newPassword, false)
|
||||
.then(
|
||||
() => {
|
||||
this.props.onFinished({});
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this.optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
didSetEmail: confirmed,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.props.onFinished({});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (err instanceof Error) {
|
||||
@@ -127,9 +149,17 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
private markFieldValid(fieldID: FieldType, result: IValidationResult): void {
|
||||
private optionallySetEmail(): Promise<boolean> {
|
||||
// Ask for an email otherwise the user has no way to reset their password
|
||||
const modal = Modal.createDialog(SetEmailDialog, {
|
||||
title: _t("auth|set_email_prompt"),
|
||||
});
|
||||
return modal.finished.then(([confirmed]) => !!confirmed);
|
||||
}
|
||||
|
||||
private markFieldValid(fieldID: FieldType, valid?: boolean): void {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = result;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
fieldValid,
|
||||
});
|
||||
@@ -139,16 +169,11 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
this.setState({
|
||||
oldPassword: ev.target.value,
|
||||
});
|
||||
this.onOldPasswordValidate({
|
||||
value: ev.target.value,
|
||||
focused: true,
|
||||
allowEmpty: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validateOldPasswordRules(fieldState);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -169,24 +194,18 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onNewPasswordValidate = (result: IValidationResult): void => {
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||
};
|
||||
|
||||
private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPasswordConfirm: ev.target.value,
|
||||
});
|
||||
|
||||
this.onNewPasswordConfirmValidate({
|
||||
value: ev.target.value,
|
||||
focused: true,
|
||||
allowEmpty: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -247,25 +266,26 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
await this.onOldPasswordValidate({
|
||||
value: this[FIELD_OLD_PASSWORD]?.value ?? null,
|
||||
allowEmpty: false,
|
||||
focused: true,
|
||||
});
|
||||
await this.onNewPasswordConfirmValidate({
|
||||
value: this[FIELD_NEW_PASSWORD_CONFIRM]?.value ?? null,
|
||||
allowEmpty: false,
|
||||
focused: true,
|
||||
});
|
||||
|
||||
const fieldIDsInDisplayOrder: FieldType[] = [
|
||||
FIELD_OLD_PASSWORD,
|
||||
FIELD_NEW_PASSWORD,
|
||||
FIELD_NEW_PASSWORD_CONFIRM,
|
||||
];
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||
const field = this[fieldID];
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
// We must wait for these validations to finish before queueing
|
||||
// up the setState below so our setState goes in the queue after
|
||||
// all the setStates from these validate calls (that's how we
|
||||
// know they've finished).
|
||||
await field.validate({ allowEmpty: false });
|
||||
}
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise<void>((resolve) => this.setState({}, resolve));
|
||||
@@ -283,16 +303,15 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
// Focus the first invalid field and show feedback in the stricter mode
|
||||
// that no longer allows empty values for required fields.
|
||||
invalidField.focus();
|
||||
// TODO: HMM
|
||||
// invalidField.validate({ allowEmpty: false, focused: true });
|
||||
invalidField.validate({ allowEmpty: false, focused: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
private allFieldsValid(): boolean {
|
||||
return Object.values(this.state.fieldValid).map(v => v.valid).every(Boolean);
|
||||
return Object.values(this.state.fieldValid).every(Boolean);
|
||||
}
|
||||
|
||||
private findFirstInvalidField(fieldIDs: FieldType[]): HTMLInputElement | null {
|
||||
private findFirstInvalidField(fieldIDs: FieldType[]): Field | null {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
@@ -302,56 +321,60 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const { fieldValid, phase } = this.state;
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
|
||||
switch (phase) {
|
||||
switch (this.state.phase) {
|
||||
case Phase.Edit:
|
||||
return (
|
||||
<Root className={"mx_ChangePasswordForm"} onSubmit={this.onClickChange}>
|
||||
<CpdField name={FIELD_OLD_PASSWORD}>
|
||||
<Label>
|
||||
{_t("auth|change_password_current_label")}
|
||||
</Label>
|
||||
<PasswordInput ref={(field) => (this[FIELD_OLD_PASSWORD] = field)} data-invalid={fieldValid[FIELD_OLD_PASSWORD]?.valid === false ? true : undefined} value={this.state.oldPassword} onChange={this.onChangeOldPassword} />
|
||||
{fieldValid[FIELD_OLD_PASSWORD]?.feedback && <HelpMessage>
|
||||
{fieldValid[FIELD_OLD_PASSWORD]?.feedback}
|
||||
</HelpMessage>}
|
||||
</CpdField>
|
||||
{ /* This is a compound field. */}
|
||||
<PassphraseField
|
||||
fieldRef={(field) => (this[FIELD_NEW_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_td("auth|change_password_new_label")}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
onChange={this.onChangeNewPassword}
|
||||
onValidate={this.onNewPasswordValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<CpdField name={FIELD_NEW_PASSWORD_CONFIRM}>
|
||||
<Label>
|
||||
{_t("auth|change_password_confirm_label")}
|
||||
</Label>
|
||||
<PasswordInput autoComplete="new-password" ref={(field) => (this[FIELD_NEW_PASSWORD_CONFIRM] = field)} data-invalid={fieldValid[FIELD_NEW_PASSWORD_CONFIRM]?.valid === false ? true : undefined} value={this.state.newPasswordConfirm} onChange={this.onChangeNewPasswordConfirm} />
|
||||
{fieldValid[FIELD_NEW_PASSWORD_CONFIRM]?.feedback && <HelpMessage>
|
||||
{fieldValid[FIELD_NEW_PASSWORD_CONFIRM]?.feedback}
|
||||
</HelpMessage>}
|
||||
</CpdField>
|
||||
<Button
|
||||
disabled={!this.allFieldsValid()}
|
||||
style={{width: "fit-content"}}
|
||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={(field) => (this[FIELD_OLD_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_t("auth|change_password_current_label")}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
onValidate={this.onOldPasswordValidate}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<PassphraseField
|
||||
fieldRef={(field) => (this[FIELD_NEW_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_td("auth|change_password_new_label")}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
onChange={this.onChangeNewPassword}
|
||||
onValidate={this.onNewPasswordValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={(field) => (this[FIELD_NEW_PASSWORD_CONFIRM] = field)}
|
||||
type="password"
|
||||
label={_t("auth|change_password_confirm_label")}
|
||||
value={this.state.newPasswordConfirm}
|
||||
onChange={this.onChangeNewPasswordConfirm}
|
||||
onValidate={this.onNewPasswordConfirmValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className={buttonClassName}
|
||||
kind={this.props.buttonKind}
|
||||
onClick={this.onClickChange}
|
||||
kind="primary"
|
||||
size="sm"
|
||||
>
|
||||
{this.props.buttonLabel || _t("auth|change_password_action")}
|
||||
</Button>
|
||||
</Root>
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
case Phase.Uploading:
|
||||
return (
|
||||
<div className="mx_Dialog_content">
|
||||
<InlineSpinner />
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2114,6 +2114,7 @@
|
||||
"list_title": "Room list",
|
||||
"notification_options": "Notification options",
|
||||
"open_space_menu": "Open space menu",
|
||||
"primary_filters": "Room list filters",
|
||||
"redacting_messages_status": {
|
||||
"one": "Currently removing messages in %(count)s room",
|
||||
"other": "Currently removing messages in %(count)s rooms"
|
||||
|
||||
@@ -23,18 +23,15 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||
import { type IWidgetApiRequest, type ClientWidgetApi, type IWidgetData } from "matrix-widget-api";
|
||||
import {
|
||||
MatrixRTCSession,
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
type CallMembership,
|
||||
MatrixRTCSessionManagerEvents,
|
||||
type ICallNotifyContent,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import type EventEmitter from "events";
|
||||
import type { IApp } from "../stores/WidgetStore";
|
||||
import SdkConfig, { DEFAULTS } from "../SdkConfig";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
||||
import { timeout } from "../utils/promise";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
@@ -45,7 +42,6 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidge
|
||||
import { getCurrentLanguage } from "../languageHandler";
|
||||
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||
import { UPDATE_EVENT } from "../stores/AsyncStore";
|
||||
import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers";
|
||||
import { isVideoRoom } from "../utils/video-rooms";
|
||||
import { FontWatcher } from "../settings/watchers/FontWatcher";
|
||||
import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types";
|
||||
@@ -85,15 +81,9 @@ export enum ConnectionState {
|
||||
export const isConnected = (state: ConnectionState): boolean =>
|
||||
state === ConnectionState.Connected || state === ConnectionState.Disconnecting;
|
||||
|
||||
export enum Layout {
|
||||
Tile = "tile",
|
||||
Spotlight = "spotlight",
|
||||
}
|
||||
|
||||
export enum CallEvent {
|
||||
ConnectionState = "connection_state",
|
||||
Participants = "participants",
|
||||
Layout = "layout",
|
||||
Close = "close",
|
||||
Destroy = "destroy",
|
||||
}
|
||||
@@ -104,7 +94,6 @@ interface CallEventHandlerMap {
|
||||
participants: Map<RoomMember, Set<string>>,
|
||||
prevParticipants: Map<RoomMember, Set<string>>,
|
||||
) => void;
|
||||
[CallEvent.Layout]: (layout: Layout) => void;
|
||||
[CallEvent.Close]: () => void;
|
||||
[CallEvent.Destroy]: () => void;
|
||||
}
|
||||
@@ -202,18 +191,6 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
*/
|
||||
public abstract clean(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Contacts the widget to connect to the call or prompt the user to connect to the call.
|
||||
* @param {MediaDeviceInfo | null} audioInput The audio input to use, or
|
||||
* null to start muted.
|
||||
* @param {MediaDeviceInfo | null} audioInput The video input to use, or
|
||||
* null to start muted.
|
||||
*/
|
||||
protected abstract performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Contacts the widget to disconnect from the call.
|
||||
*/
|
||||
@@ -221,28 +198,10 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
|
||||
/**
|
||||
* Starts the communication between the widget and the call.
|
||||
* The call then waits for the necessary requirements to actually perform the connection
|
||||
* or connects right away depending on the call type. (Jitsi, Legacy, ElementCall...)
|
||||
* It uses the media devices set in MediaDeviceHandler.
|
||||
* The widget associated with the call must be active
|
||||
* for this to succeed.
|
||||
* The widget associated with the call must be active for this to succeed.
|
||||
* Only call this if the call state is: ConnectionState.Disconnected.
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
|
||||
(await MediaDeviceHandler.getDevices())!;
|
||||
|
||||
let audioInput: MediaDeviceInfo | null = null;
|
||||
if (!MediaDeviceHandler.startWithAudioMuted) {
|
||||
const deviceId = MediaDeviceHandler.getAudioInput();
|
||||
audioInput = audioInputs.find((d) => d.deviceId === deviceId) ?? audioInputs[0] ?? null;
|
||||
}
|
||||
let videoInput: MediaDeviceInfo | null = null;
|
||||
if (!MediaDeviceHandler.startWithVideoMuted) {
|
||||
const deviceId = MediaDeviceHandler.getVideoInput();
|
||||
videoInput = videoInputs.find((d) => d.deviceId === deviceId) ?? videoInputs[0] ?? null;
|
||||
}
|
||||
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
this.messaging = messagingStore.getMessagingForUid(this.widgetUid) ?? null;
|
||||
if (!this.messaging) {
|
||||
@@ -263,13 +222,23 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
await this.performConnection(audioInput, videoInput);
|
||||
}
|
||||
|
||||
protected setConnected(): void {
|
||||
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.addEventListener("beforeunload", this.beforeUnload);
|
||||
this.connectionState = ConnectionState.Connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually marks the call as disconnected.
|
||||
*/
|
||||
protected setDisconnected(): void {
|
||||
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the user from the call.
|
||||
*/
|
||||
@@ -282,15 +251,6 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually marks the call as disconnected.
|
||||
*/
|
||||
public setDisconnected(): void {
|
||||
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops further communication with the widget and tells the UI to close.
|
||||
*/
|
||||
@@ -476,66 +436,10 @@ export class JitsiCall extends Call {
|
||||
});
|
||||
}
|
||||
|
||||
protected async performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {
|
||||
// Ensure that the messaging doesn't get stopped while we're waiting for responses
|
||||
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
|
||||
const listener = (uid: string): void => {
|
||||
if (uid === this.widgetUid) {
|
||||
cleanup();
|
||||
reject(new Error("Messaging stopped"));
|
||||
}
|
||||
};
|
||||
const done = (): void => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = (): void => {
|
||||
messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.off(CallEvent.ConnectionState, done);
|
||||
};
|
||||
|
||||
messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.on(CallEvent.ConnectionState, done);
|
||||
});
|
||||
|
||||
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
|
||||
// sending a hangup event that races with the rest of this method, so we need
|
||||
// to add the hangup listener now rather than later
|
||||
public async start(): Promise<void> {
|
||||
await super.start();
|
||||
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
// Actually perform the join
|
||||
const response = waitForEvent(
|
||||
this.messaging!,
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const request = this.messaging!.transport.send(ElementWidgetActions.JoinCall, {
|
||||
audioInput: audioInput?.label ?? null,
|
||||
videoInput: videoInput?.label ?? null,
|
||||
});
|
||||
try {
|
||||
await Promise.race([Promise.all([request, response]), dontStopMessaging]);
|
||||
} catch (e) {
|
||||
// If it timed out, clean up our advance preparations
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
if (this.messaging!.transport.ready) {
|
||||
// The messaging still exists, which means Jitsi might still be going in the background
|
||||
this.messaging!.transport.send(ElementWidgetActions.HangupCall, { force: true });
|
||||
}
|
||||
|
||||
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
}
|
||||
@@ -558,18 +462,17 @@ export class JitsiCall extends Call {
|
||||
}
|
||||
}
|
||||
|
||||
public setDisconnected(): void {
|
||||
// During tests this.messaging can be undefined
|
||||
this.messaging?.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
public close(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
|
||||
super.setDisconnected();
|
||||
super.close();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.room.off(RoomStateEvent.Update, this.onRoomState);
|
||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
||||
this.off(CallEvent.ConnectionState, this.onConnectionState);
|
||||
if (this.participantsExpirationTimer !== null) {
|
||||
clearTimeout(this.participantsExpirationTimer);
|
||||
this.participantsExpirationTimer = null;
|
||||
@@ -621,27 +524,21 @@ export class JitsiCall extends Call {
|
||||
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
|
||||
};
|
||||
|
||||
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setConnected();
|
||||
};
|
||||
|
||||
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
// If we're already in the middle of a client-initiated disconnection,
|
||||
// ignore the event
|
||||
if (this.connectionState === ConnectionState.Disconnecting) return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
||||
// wait for the connection event in order to avoid racing
|
||||
if (this.connectionState === ConnectionState.Disconnected) {
|
||||
await waitForEvent(this, CallEvent.ConnectionState);
|
||||
}
|
||||
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
this.close();
|
||||
// In video rooms we immediately want to restart the call after hangup
|
||||
// The lobby will be shown again and it connects to all signals from Jitsi.
|
||||
if (isVideoRoom(this.room)) {
|
||||
this.start();
|
||||
}
|
||||
if (!isVideoRoom(this.room)) this.close();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -658,14 +555,6 @@ export class ElementCall extends Call {
|
||||
|
||||
private settingsStoreCallEncryptionWatcher?: string;
|
||||
private terminationTimer?: number;
|
||||
private _layout = Layout.Tile;
|
||||
public get layout(): Layout {
|
||||
return this._layout;
|
||||
}
|
||||
protected set layout(value: Layout) {
|
||||
this._layout = value;
|
||||
this.emit(CallEvent.Layout, value);
|
||||
}
|
||||
|
||||
public get presented(): boolean {
|
||||
return super.presented;
|
||||
@@ -688,7 +577,6 @@ export class ElementCall extends Call {
|
||||
const params = new URLSearchParams({
|
||||
embed: "true", // We're embedding EC within another application
|
||||
// Template variables are used, so that this can be configured using the widget data.
|
||||
preload: "$preload", // We want it to load in the background.
|
||||
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",
|
||||
@@ -728,17 +616,13 @@ export class ElementCall extends Call {
|
||||
}
|
||||
|
||||
// Creates a new widget if there isn't any widget of typ Call in this room.
|
||||
// Defaults for creating a new widget are: skipLobby = false, preload = false
|
||||
// Defaults for creating a new widget are: skipLobby = false
|
||||
// When there is already a widget the current widget configuration will be used or can be overwritten
|
||||
// by passing the according parameters (skipLobby, preload).
|
||||
//
|
||||
// `preload` is deprecated. We used it for optimizing EC by using a custom EW call lobby and preloading the iframe.
|
||||
// now it should always be false.
|
||||
// by passing the according parameters (skipLobby).
|
||||
private static createOrGetCallWidget(
|
||||
roomId: string,
|
||||
client: MatrixClient,
|
||||
skipLobby: boolean | undefined,
|
||||
preload: boolean | undefined,
|
||||
returnToLobby: boolean | undefined,
|
||||
): IApp {
|
||||
const ecWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.CALL.matches(app.type));
|
||||
@@ -749,9 +633,6 @@ export class ElementCall extends Call {
|
||||
if (skipLobby !== undefined) {
|
||||
overwrites.skipLobby = skipLobby;
|
||||
}
|
||||
if (preload !== undefined) {
|
||||
overwrites.preload = preload;
|
||||
}
|
||||
if (returnToLobby !== undefined) {
|
||||
overwrites.returnToLobby = returnToLobby;
|
||||
}
|
||||
@@ -776,7 +657,6 @@ export class ElementCall extends Call {
|
||||
{},
|
||||
{
|
||||
skipLobby: skipLobby ?? false,
|
||||
preload: preload ?? false,
|
||||
returnToLobby: returnToLobby ?? false,
|
||||
},
|
||||
),
|
||||
@@ -842,7 +722,6 @@ export class ElementCall extends Call {
|
||||
room.roomId,
|
||||
room.client,
|
||||
undefined,
|
||||
undefined,
|
||||
isVideoRoom(room),
|
||||
);
|
||||
return new ElementCall(session, availableOrCreatedWidget, room.client);
|
||||
@@ -852,99 +731,41 @@ export class ElementCall extends Call {
|
||||
}
|
||||
|
||||
public static create(room: Room, skipLobby = false): void {
|
||||
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, false, isVideoRoom(room));
|
||||
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, isVideoRoom(room));
|
||||
}
|
||||
|
||||
protected async sendCallNotify(): Promise<void> {
|
||||
const room = this.room;
|
||||
const existingOtherRoomCallMembers = MatrixRTCSession.callMembershipsForRoom(room).filter(
|
||||
// filter all memberships where the application is m.call and the call_id is ""
|
||||
(m) => {
|
||||
const isRoomCallMember = m.application === "m.call" && m.callId === "";
|
||||
const isThisDevice = m.deviceId === this.client.deviceId;
|
||||
return isRoomCallMember && !isThisDevice;
|
||||
},
|
||||
);
|
||||
|
||||
const memberCount = getJoinedNonFunctionalMembers(room).length;
|
||||
if (!isVideoRoom(room) && existingOtherRoomCallMembers.length === 0) {
|
||||
// send ringing event
|
||||
const content: ICallNotifyContent = {
|
||||
"application": "m.call",
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notify_type": memberCount == 2 ? "ring" : "notify",
|
||||
"call_id": "",
|
||||
};
|
||||
|
||||
await room.client.sendEvent(room.roomId, EventType.CallNotify, content);
|
||||
}
|
||||
}
|
||||
|
||||
protected async performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {
|
||||
// The JoinCall action is only send if the widget is waiting for it.
|
||||
if (this.widget.data?.preload) {
|
||||
try {
|
||||
await this.messaging!.transport.send(ElementWidgetActions.JoinCall, {
|
||||
audioInput: audioInput?.label ?? null,
|
||||
videoInput: videoInput?.label ?? null,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
public async start(): Promise<void> {
|
||||
await super.start();
|
||||
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
|
||||
|
||||
// TODO: if the widget informs us when the join button is clicked (widget action), so we can
|
||||
// - set state to connecting
|
||||
// - send call notify
|
||||
const session = this.client.matrixRTC.getActiveRoomSession(this.room);
|
||||
if (session) {
|
||||
await waitForEvent(
|
||||
session,
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
(_, newMemberships: CallMembership[]) =>
|
||||
newMemberships.some((m) => m.sender === this.client.getUserId()),
|
||||
false, // allow user to wait as long as they want (no timeout)
|
||||
);
|
||||
} else {
|
||||
await waitForEvent(
|
||||
this.client.matrixRTC,
|
||||
MatrixRTCSessionManagerEvents.SessionStarted,
|
||||
(roomId: string, session: MatrixRTCSession) =>
|
||||
this.session.callId === session.callId && roomId === this.roomId,
|
||||
false, // allow user to wait as long as they want (no timeout)
|
||||
);
|
||||
}
|
||||
this.sendCallNotify();
|
||||
}
|
||||
|
||||
protected async performDisconnection(): Promise<void> {
|
||||
const response = waitForEvent(
|
||||
this.messaging!,
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const request = this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
try {
|
||||
await this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
await waitForEvent(
|
||||
this.session,
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
(_, newMemberships: CallMembership[]) =>
|
||||
!newMemberships.some((m) => m.sender === this.client.getUserId()),
|
||||
);
|
||||
await Promise.all([request, response]);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
public setDisconnected(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
public close(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
|
||||
super.setDisconnected();
|
||||
super.close();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
@@ -966,15 +787,6 @@ export class ElementCall extends Call {
|
||||
if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the call's layout.
|
||||
* @param layout The layout to switch to.
|
||||
*/
|
||||
public async setLayout(layout: Layout): Promise<void> {
|
||||
const action = layout === Layout.Tile ? ElementWidgetActions.TileLayout : ElementWidgetActions.SpotlightLayout;
|
||||
await this.messaging!.transport.send(action, {});
|
||||
}
|
||||
|
||||
private readonly onMembershipChanged = (): void => this.updateParticipants();
|
||||
|
||||
private updateParticipants(): void {
|
||||
@@ -1000,15 +812,20 @@ export class ElementCall extends Call {
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setConnected();
|
||||
};
|
||||
|
||||
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
// If we're already in the middle of a client-initiated disconnection,
|
||||
// ignore the event
|
||||
if (this.connectionState === ConnectionState.Disconnecting) return;
|
||||
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
// In video rooms we immediately want to reconnect after hangup
|
||||
// This starts the lobby again and connects to all signals from EC.
|
||||
if (isVideoRoom(this.room)) {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly onClose = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
@@ -1018,18 +835,6 @@ export class ElementCall extends Call {
|
||||
this.close();
|
||||
};
|
||||
|
||||
private readonly onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Tile;
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
private readonly onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Spotlight;
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
public clean(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ export class MockedCall extends Call {
|
||||
// No action needed for any of the following methods since this is just a mock
|
||||
public async clean(): Promise<void> {}
|
||||
// Public to allow spying
|
||||
public async performConnection(): Promise<void> {}
|
||||
public async performDisconnection(): Promise<void> {}
|
||||
|
||||
public destroy() {
|
||||
|
||||
@@ -99,7 +99,7 @@ exports[`<UnsupportedBrowserView /> should match snapshot 1`] = `
|
||||
</p>
|
||||
<div
|
||||
class="mx_Flex mx_ErrorView_buttons"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57"
|
||||
@@ -140,7 +140,7 @@ exports[`<UnsupportedBrowserView /> should match snapshot 1`] = `
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57"
|
||||
@@ -210,7 +210,7 @@ exports[`<UnsupportedBrowserView /> should match snapshot 1`] = `
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
href="https://apps.apple.com/app/vector/id1083446067"
|
||||
|
||||
@@ -52,7 +52,7 @@ exports[`FilePanel renders empty state 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_EmptyState"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
|
||||
@@ -7,7 +7,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
@@ -222,7 +222,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
@@ -522,7 +522,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
@@ -899,7 +899,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
@@ -1284,7 +1284,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
@@ -1492,7 +1492,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
@@ -1873,7 +1873,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
|
||||
@@ -43,7 +43,7 @@ exports[`<MasUnlockCrossSigningAuthEntry/> should render 1`] = `
|
||||
</p>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_Dialog_nonDialogButton _has-icon_vczzf_57"
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("CallEvent", () => {
|
||||
}),
|
||||
);
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
await act(() => call.start());
|
||||
act(() => call.setConnectionState(ConnectionState.Connected));
|
||||
|
||||
// Test that the leave button works
|
||||
fireEvent.click(screen.getByRole("button", { name: "Leave" }));
|
||||
|
||||
@@ -71,7 +71,7 @@ exports[`<ExtensionsCard /> should render empty state 1`] = `
|
||||
</button>
|
||||
<div
|
||||
class="mx_Flex mx_EmptyState"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
|
||||
@@ -50,7 +50,7 @@ exports[`<PinnedMessagesCard /> should show the empty state when there are no pi
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_EmptyState"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
|
||||
@@ -69,7 +69,7 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
|
||||
/>
|
||||
<section
|
||||
class="mx_Flex mx_RoomSummaryCard_badges"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 _badge_1t12g_8"
|
||||
@@ -91,7 +91,7 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
|
||||
</section>
|
||||
<section
|
||||
class="mx_Flex mx_RoomSummaryCard_topic"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Box mx_RoomSummaryCard_topic_container mx_Box--flex"
|
||||
@@ -724,7 +724,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||
/>
|
||||
<section
|
||||
class="mx_Flex mx_RoomSummaryCard_badges"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 _badge_1t12g_8"
|
||||
@@ -746,7 +746,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||
</section>
|
||||
<section
|
||||
class="mx_Flex mx_RoomSummaryCard_topic"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Box mx_Box--flex"
|
||||
@@ -1342,7 +1342,7 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||
/>
|
||||
<section
|
||||
class="mx_Flex mx_RoomSummaryCard_badges"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 _badge_1t12g_8"
|
||||
@@ -1364,7 +1364,7 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||
</section>
|
||||
<section
|
||||
class="mx_Flex mx_RoomSummaryCard_topic"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Box mx_RoomSummaryCard_topic_container mx_Box--flex"
|
||||
|
||||
@@ -77,7 +77,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -85,7 +85,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 mx_UserInfo_verification_unavailable"
|
||||
@@ -363,7 +363,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -371,7 +371,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
@@ -399,7 +399,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="_icon_11k6c_18"
|
||||
@@ -653,7 +653,7 @@ exports[`<UserInfoHeader /> renders verification unavailable message 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -661,7 +661,7 @@ exports[`<UserInfoHeader /> renders verification unavailable message 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
@@ -689,7 +689,7 @@ exports[`<UserInfoHeader /> renders verification unavailable message 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 mx_UserInfo_verification_unavailable"
|
||||
@@ -735,7 +735,7 @@ exports[`<UserInfoHeader /> renders verified badge when user is verified 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -743,7 +743,7 @@ exports[`<UserInfoHeader /> renders verified badge when user is verified 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
@@ -771,7 +771,7 @@ exports[`<UserInfoHeader /> renders verified badge when user is verified 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 _badge_1t12g_8 mx_UserInfo_verified_badge"
|
||||
@@ -832,7 +832,7 @@ exports[`<UserInfoHeader /> renders verify button 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -840,7 +840,7 @@ exports[`<UserInfoHeader /> renders verify button 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
@@ -868,7 +868,7 @@ exports[`<UserInfoHeader /> renders verify button 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_container_verifyButton"
|
||||
|
||||
@@ -432,8 +432,6 @@ describe("<EditMessageComposer/>", () => {
|
||||
user_ids: [
|
||||
// sender of event we replied to
|
||||
originalEvent.getSender()!,
|
||||
// mentions from this event
|
||||
"@bob:server.org",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
<DocumentFragment>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 userEvent from "@testing-library/user-event";
|
||||
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||
import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters";
|
||||
|
||||
describe("<RoomListPrimaryFilters />", () => {
|
||||
let vm: RoomListViewState;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = {
|
||||
rooms: [],
|
||||
openRoom: jest.fn(),
|
||||
primaryFilters: [
|
||||
{ name: "People", active: false, toggle: jest.fn() },
|
||||
{ name: "Rooms", active: true, toggle: jest.fn() },
|
||||
],
|
||||
activateSecondaryFilter: () => {},
|
||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||
};
|
||||
});
|
||||
|
||||
it("should render primary filters", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { asFragment } = render(<RoomListPrimaryFilters vm={vm} />);
|
||||
expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "People" }));
|
||||
expect(vm.primaryFilters[0].toggle).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -58,7 +58,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room0"
|
||||
@@ -77,7 +77,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -104,7 +104,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room1"
|
||||
@@ -123,7 +123,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -150,7 +150,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room2"
|
||||
@@ -169,7 +169,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -196,7 +196,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room3"
|
||||
@@ -215,7 +215,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -242,7 +242,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room4"
|
||||
@@ -261,7 +261,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -288,7 +288,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room5"
|
||||
@@ -307,7 +307,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -334,7 +334,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room6"
|
||||
@@ -353,7 +353,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -380,7 +380,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room7"
|
||||
@@ -399,7 +399,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -426,7 +426,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room8"
|
||||
@@ -445,7 +445,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -472,7 +472,7 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room9"
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`<RoomListCell /> should render a room cell 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
@@ -36,7 +36,7 @@ exports[`<RoomListCell /> should render a room cell 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListCell_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
title="room1"
|
||||
|
||||
@@ -6,11 +6,11 @@ exports[`<RoomListHeaderView /> compose menu should display the compose menu 1`]
|
||||
aria-label="Room options"
|
||||
class="mx_Flex mx_RoomListHeaderView"
|
||||
data-testid="room-list-header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListHeaderView_title"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
title="title"
|
||||
@@ -93,11 +93,11 @@ exports[`<RoomListHeaderView /> compose menu should not display the compose menu
|
||||
aria-label="Room options"
|
||||
class="mx_Flex mx_RoomListHeaderView"
|
||||
data-testid="room-list-header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListHeaderView_title"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
title="title"
|
||||
@@ -145,11 +145,11 @@ exports[`<RoomListHeaderView /> space menu should display the space menu 1`] = `
|
||||
aria-label="Room options"
|
||||
class="mx_Flex mx_RoomListHeaderView"
|
||||
data-testid="room-list-header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListHeaderView_title"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
title="title"
|
||||
@@ -232,11 +232,11 @@ exports[`<RoomListHeaderView /> space menu should not display the space menu 1`]
|
||||
aria-label="Room options"
|
||||
class="mx_Flex mx_RoomListHeaderView"
|
||||
data-testid="room-list-header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListHeaderView_title"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
title="title"
|
||||
|
||||
@@ -5,17 +5,17 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
|
||||
<section
|
||||
class="mx_Flex mx_RoomListPanel"
|
||||
data-testid="room-list-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<header
|
||||
aria-label="Room options"
|
||||
class="mx_Flex mx_RoomListHeaderView"
|
||||
data-testid="room-list-header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListHeaderView_title"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
title="Home"
|
||||
@@ -24,6 +24,65 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<ul
|
||||
aria-label="Room list filters"
|
||||
class="mx_Flex mx_RoomListPrimaryFilters"
|
||||
role="listbox"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: wrap;"
|
||||
>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Unread
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Favourites
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
People
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rooms
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="mx_RoomList"
|
||||
data-testid="room-list"
|
||||
@@ -64,12 +123,12 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
|
||||
<section
|
||||
class="mx_Flex mx_RoomListPanel"
|
||||
data-testid="room-list-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListSearch"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_RoomListSearch_search _has-icon_vczzf_57"
|
||||
@@ -92,7 +151,7 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
|
||||
</svg>
|
||||
<span
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
Search
|
||||
<kbd>
|
||||
@@ -126,11 +185,11 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
|
||||
aria-label="Room options"
|
||||
class="mx_Flex mx_RoomListHeaderView"
|
||||
data-testid="room-list-header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListHeaderView_title"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
title="Home"
|
||||
@@ -174,6 +233,65 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
|
||||
</div>
|
||||
</button>
|
||||
</header>
|
||||
<ul
|
||||
aria-label="Room list filters"
|
||||
class="mx_Flex mx_RoomListPrimaryFilters"
|
||||
role="listbox"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: wrap;"
|
||||
>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Unread
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Favourites
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
People
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rooms
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="mx_RoomList"
|
||||
data-testid="room-list"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomListPrimaryFilters /> should render primary filters 1`] = `
|
||||
<DocumentFragment>
|
||||
<ul
|
||||
aria-label="Room list filters"
|
||||
class="mx_Flex mx_RoomListPrimaryFilters"
|
||||
role="listbox"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: wrap;"
|
||||
>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
People
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="true"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="true"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rooms
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ exports[`<RoomListSearch /> should display search and explore buttons 1`] = `
|
||||
<div
|
||||
class="mx_Flex mx_RoomListSearch"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_RoomListSearch_search _has-icon_vczzf_57"
|
||||
@@ -28,7 +28,7 @@ exports[`<RoomListSearch /> should display search and explore buttons 1`] = `
|
||||
</svg>
|
||||
<span
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
Search
|
||||
<kbd>
|
||||
@@ -66,7 +66,7 @@ exports[`<RoomListSearch /> should display the dial button when the PTSN protoco
|
||||
<div
|
||||
class="mx_Flex mx_RoomListSearch"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_RoomListSearch_search _has-icon_vczzf_57"
|
||||
@@ -89,7 +89,7 @@ exports[`<RoomListSearch /> should display the dial button when the PTSN protoco
|
||||
</svg>
|
||||
<span
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
Search
|
||||
<kbd>
|
||||
@@ -148,7 +148,7 @@ exports[`<RoomListSearch /> should hide the explore button when UIComponent.Expl
|
||||
<div
|
||||
class="mx_Flex mx_RoomListSearch"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_RoomListSearch_search _has-icon_vczzf_57"
|
||||
@@ -171,7 +171,7 @@ exports[`<RoomListSearch /> should hide the explore button when UIComponent.Expl
|
||||
</svg>
|
||||
<span
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
Search
|
||||
<kbd>
|
||||
@@ -188,7 +188,7 @@ exports[`<RoomListSearch /> should hide the explore button when the active space
|
||||
<div
|
||||
class="mx_Flex mx_RoomListSearch"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_RoomListSearch_search _has-icon_vczzf_57"
|
||||
@@ -211,7 +211,7 @@ exports[`<RoomListSearch /> should hide the explore button when the active space
|
||||
</svg>
|
||||
<span
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
Search
|
||||
<kbd>
|
||||
|
||||
@@ -46,6 +46,7 @@ import { UIComponent } from "../../../../../src/settings/UIFeature";
|
||||
import { MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { ConnectionState } from "../../../../../src/models/Call";
|
||||
|
||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
@@ -215,7 +216,7 @@ describe("RoomTile", () => {
|
||||
it("tracks connection state", async () => {
|
||||
renderRoomTile();
|
||||
screen.getByText("Video");
|
||||
await act(() => call.start());
|
||||
act(() => call.setConnectionState(ConnectionState.Connected));
|
||||
screen.getByText("Joined");
|
||||
await act(() => call.disconnect());
|
||||
screen.getByText("Video");
|
||||
|
||||
@@ -194,7 +194,7 @@ describe("<SendMessageComposer/>", () => {
|
||||
"m.mentions": { user_ids: ["@bob:test"] },
|
||||
});
|
||||
|
||||
// It also adds any other mentioned users, but removes yourself.
|
||||
// It no longer adds any other mentioned users
|
||||
replyToEvent = mkEvent({
|
||||
type: "m.room.message",
|
||||
user: "@bob:test",
|
||||
@@ -205,7 +205,7 @@ describe("<SendMessageComposer/>", () => {
|
||||
content = {};
|
||||
attachMentions("@alice:test", content, model, replyToEvent);
|
||||
expect(content).toEqual({
|
||||
"m.mentions": { user_ids: ["@bob:test", "@charlie:test"] },
|
||||
"m.mentions": { user_ids: ["@bob:test"] },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ exports[`<ThirdPartyMemberInfo /> should render invite 1`] = `
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_ThirdPartyMemberInfo"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<section
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74"
|
||||
@@ -124,11 +124,11 @@ exports[`<ThirdPartyMemberInfo /> should render invite when room in not availabl
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_ThirdPartyMemberInfo"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<section
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74"
|
||||
|
||||
@@ -232,7 +232,7 @@ exports[`MemberTileView ThreePidInviteTileView renders ThreePidInvite correctly
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_InvitedIconView"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
|
||||
@@ -84,7 +84,7 @@ exports[`<ResetIdentityPanel /> should display the 'forgot recovery key' variant
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_15wzx_8"
|
||||
@@ -266,7 +266,7 @@ exports[`<ResetIdentityPanel /> should reset the encryption when the continue bu
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_15wzx_8"
|
||||
@@ -451,7 +451,7 @@ exports[`<ResetIdentityPanel /> should reset the encryption when the continue bu
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_15wzx_8"
|
||||
@@ -555,7 +555,7 @@ exports[`<ResetIdentityPanel /> should reset the encryption when the continue bu
|
||||
</button>
|
||||
<div
|
||||
class="mx_Flex mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_ResetIdentityPanel_warning"
|
||||
|
||||
@@ -255,7 +255,7 @@ exports[`<EncryptionUserSettingsTab /> should display the reset identity panel w
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_15wzx_8"
|
||||
|
||||
@@ -34,7 +34,6 @@ import type { Mocked } from "jest-mock";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import {
|
||||
type JitsiCallMemberContent,
|
||||
Layout,
|
||||
Call,
|
||||
CallEvent,
|
||||
ConnectionState,
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
ElementCall,
|
||||
} from "../../../src/models/Call";
|
||||
import { stubClient, mkEvent, mkRoomMember, setupAsyncStoreWithClient, mockPlatformPeg } from "../../test-utils";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
|
||||
@@ -52,18 +50,6 @@ import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { PosthogAnalytics } from "../../../src/PosthogAnalytics";
|
||||
import { type SettingKey } from "../../../src/settings/Settings.tsx";
|
||||
|
||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||
[MediaDeviceKindEnum.AudioInput]: [
|
||||
{ deviceId: "1", groupId: "1", kind: "audioinput", label: "Headphones", toJSON: () => {} },
|
||||
],
|
||||
[MediaDeviceKindEnum.VideoInput]: [
|
||||
{ deviceId: "2", groupId: "2", kind: "videoinput", label: "Built-in webcam", toJSON: () => {} },
|
||||
],
|
||||
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||
});
|
||||
jest.spyOn(MediaDeviceHandler, "getAudioInput").mockReturnValue("1");
|
||||
jest.spyOn(MediaDeviceHandler, "getVideoInput").mockReturnValue("2");
|
||||
|
||||
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName): any => enabledSettings.has(settingName) || undefined,
|
||||
@@ -136,14 +122,7 @@ const cleanUpClientRoomAndStores = (client: MatrixClient, room: Room) => {
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
};
|
||||
|
||||
const setUpWidget = (
|
||||
call: Call,
|
||||
): {
|
||||
widget: Widget;
|
||||
messaging: Mocked<ClientWidgetApi>;
|
||||
audioMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
videoMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
} => {
|
||||
const setUpWidget = (call: Call): { widget: Widget; messaging: Mocked<ClientWidgetApi> } => {
|
||||
call.widget.data = { ...call.widget, skipLobby: true };
|
||||
const widget = new Widget(call.widget);
|
||||
|
||||
@@ -161,23 +140,45 @@ const setUpWidget = (
|
||||
} as unknown as Mocked<ClientWidgetApi>;
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, call.roomId, messaging);
|
||||
|
||||
const audioMutedSpy = jest.spyOn(MediaDeviceHandler, "startWithAudioMuted", "get");
|
||||
const videoMutedSpy = jest.spyOn(MediaDeviceHandler, "startWithVideoMuted", "get");
|
||||
|
||||
return { widget, messaging, audioMutedSpy, videoMutedSpy };
|
||||
return { widget, messaging };
|
||||
};
|
||||
|
||||
const cleanUpCallAndWidget = (
|
||||
call: Call,
|
||||
widget: Widget,
|
||||
audioMutedSpy: jest.SpyInstance<boolean, []>,
|
||||
videoMutedSpy: jest.SpyInstance<boolean, []>,
|
||||
) => {
|
||||
async function connect(call: Call, messaging: Mocked<ClientWidgetApi>, startWidget = true): Promise<void> {
|
||||
async function sessionConnect() {
|
||||
await new Promise<void>((r) => {
|
||||
setTimeout(() => r(), 400);
|
||||
});
|
||||
messaging.emit(`action:${ElementWidgetActions.JoinCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
}
|
||||
async function runTimers() {
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(500);
|
||||
}
|
||||
sessionConnect();
|
||||
await Promise.all([...(startWidget ? [call.start()] : []), runTimers()]);
|
||||
}
|
||||
|
||||
async function disconnect(call: Call, messaging: Mocked<ClientWidgetApi>): Promise<void> {
|
||||
async function sessionDisconnect() {
|
||||
await new Promise<void>((r) => {
|
||||
setTimeout(() => r(), 400);
|
||||
});
|
||||
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
}
|
||||
async function runTimers() {
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(500);
|
||||
}
|
||||
sessionDisconnect();
|
||||
const promise = call.disconnect();
|
||||
runTimers();
|
||||
await promise;
|
||||
}
|
||||
|
||||
const cleanUpCallAndWidget = (call: Call, widget: Widget) => {
|
||||
call.destroy();
|
||||
jest.clearAllMocks();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, call.roomId);
|
||||
audioMutedSpy.mockRestore();
|
||||
videoMutedSpy.mockRestore();
|
||||
};
|
||||
|
||||
describe("JitsiCall", () => {
|
||||
@@ -221,8 +222,6 @@ describe("JitsiCall", () => {
|
||||
let call: JitsiCall;
|
||||
let widget: Widget;
|
||||
let messaging: Mocked<ClientWidgetApi>;
|
||||
let audioMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
let videoMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
@@ -233,7 +232,7 @@ describe("JitsiCall", () => {
|
||||
if (maybeCall === null) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
({ widget, messaging, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
|
||||
({ widget, messaging } = setUpWidget(call));
|
||||
|
||||
mocked(messaging.transport).send.mockImplementation(async (action, data): Promise<any> => {
|
||||
if (action === ElementWidgetActions.JoinCall) {
|
||||
@@ -251,102 +250,37 @@ describe("JitsiCall", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
|
||||
afterEach(() => cleanUpCallAndWidget(call, widget));
|
||||
|
||||
it("connects muted", async () => {
|
||||
it("connects", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
audioMutedSpy.mockReturnValue(true);
|
||||
videoMutedSpy.mockReturnValue(true);
|
||||
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
|
||||
audioInput: null,
|
||||
videoInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("connects unmuted", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
audioMutedSpy.mockReturnValue(false);
|
||||
videoMutedSpy.mockReturnValue(false);
|
||||
|
||||
await call.start();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
|
||||
audioInput: "Headphones",
|
||||
videoInput: "Built-in webcam",
|
||||
});
|
||||
});
|
||||
|
||||
it("waits for messaging when connecting", async () => {
|
||||
it("waits for messaging when starting", async () => {
|
||||
// Temporarily remove the messaging to simulate connecting while the
|
||||
// widget is still initializing
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = call.start();
|
||||
const startup = call.start();
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
await connect;
|
||||
await startup;
|
||||
await connect(call, messaging, false);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
});
|
||||
|
||||
it("doesn't stop messaging when connecting", async () => {
|
||||
// Temporarily remove the messaging to simulate connecting while the
|
||||
// widget is still initializing
|
||||
jest.useFakeTimers();
|
||||
const oldSendMock = messaging.transport.send;
|
||||
mocked(messaging.transport).send.mockImplementation(async (action: string): Promise<any> => {
|
||||
if (action === ElementWidgetActions.JoinCall) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
}
|
||||
});
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = call.start();
|
||||
async function runTimers() {
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(1000);
|
||||
}
|
||||
async function runStopMessaging() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
}
|
||||
runStopMessaging();
|
||||
runTimers();
|
||||
let connectError;
|
||||
try {
|
||||
await connect;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
connectError = e;
|
||||
}
|
||||
expect(connectError).toBeDefined();
|
||||
// const connect2 = await connect;
|
||||
// expect(connect2).toThrow();
|
||||
messaging.transport.send = oldSendMock;
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("fails to connect if the widget returns an error", async () => {
|
||||
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
|
||||
await expect(call.start()).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("fails to disconnect if the widget returns an error", async () => {
|
||||
await call.start();
|
||||
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
|
||||
await connect(call, messaging);
|
||||
mocked(messaging.transport).send.mockRejectedValue(new Error("never!"));
|
||||
await expect(call.disconnect()).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("handles remote disconnection", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
|
||||
const callback = jest.fn();
|
||||
@@ -354,7 +288,6 @@ describe("JitsiCall", () => {
|
||||
call.on(CallEvent.ConnectionState, callback);
|
||||
|
||||
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||
await waitFor(() => {
|
||||
expect(callback).toHaveBeenNthCalledWith(1, ConnectionState.Disconnected, ConnectionState.Connected);
|
||||
});
|
||||
@@ -364,14 +297,14 @@ describe("JitsiCall", () => {
|
||||
|
||||
it("disconnects", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
await call.disconnect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
|
||||
it("disconnects when we leave the room", async () => {
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
room.emit(RoomEvent.MyMembership, room, KnownMembership.Leave);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
@@ -379,14 +312,14 @@ describe("JitsiCall", () => {
|
||||
|
||||
it("reconnects after disconnect in video rooms", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
await call.disconnect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
|
||||
it("remains connected if we stay in the room", async () => {
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
room.emit(RoomEvent.MyMembership, room, KnownMembership.Join);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
@@ -412,7 +345,7 @@ describe("JitsiCall", () => {
|
||||
|
||||
// Now, stub out client.sendStateEvent so we can test our local echo
|
||||
client.sendStateEvent.mockReset();
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
expect(call.participants).toEqual(
|
||||
new Map([
|
||||
[alice, new Set(["alices_device"])],
|
||||
@@ -425,8 +358,8 @@ describe("JitsiCall", () => {
|
||||
});
|
||||
|
||||
it("updates room state when connecting and disconnecting", async () => {
|
||||
await connect(call, messaging);
|
||||
const now1 = Date.now();
|
||||
await call.start();
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
@@ -453,7 +386,7 @@ describe("JitsiCall", () => {
|
||||
});
|
||||
|
||||
it("repeatedly updates room state while connected", async () => {
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
@@ -483,7 +416,7 @@ describe("JitsiCall", () => {
|
||||
const onConnectionState = jest.fn();
|
||||
call.on(CallEvent.ConnectionState, onConnectionState);
|
||||
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
await call.disconnect();
|
||||
expect(onConnectionState.mock.calls).toEqual([
|
||||
[ConnectionState.Connected, ConnectionState.Disconnected],
|
||||
@@ -498,7 +431,7 @@ describe("JitsiCall", () => {
|
||||
const onParticipants = jest.fn();
|
||||
call.on(CallEvent.Participants, onParticipants);
|
||||
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
await call.disconnect();
|
||||
expect(onParticipants.mock.calls).toEqual([
|
||||
[new Map([[alice, new Set(["alices_device"])]]), new Map()],
|
||||
@@ -511,7 +444,7 @@ describe("JitsiCall", () => {
|
||||
});
|
||||
|
||||
it("switches to spotlight layout when the widget becomes a PiP", async () => {
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Undock);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Dock);
|
||||
@@ -555,7 +488,7 @@ describe("JitsiCall", () => {
|
||||
});
|
||||
|
||||
it("doesn't clean up valid devices", async () => {
|
||||
await call.start();
|
||||
await connect(call, messaging);
|
||||
await client.sendStateEvent(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
@@ -620,47 +553,6 @@ describe("ElementCall", () => {
|
||||
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
|
||||
}
|
||||
|
||||
const callConnectProcedure = async (call: ElementCall, startWidget = true): Promise<void> => {
|
||||
async function sessionConnect() {
|
||||
await new Promise<void>((r) => {
|
||||
setTimeout(() => r(), 400);
|
||||
});
|
||||
client.matrixRTC.emit(MatrixRTCSessionManagerEvents.SessionStarted, call.roomId, {
|
||||
sessionId: undefined,
|
||||
} as unknown as MatrixRTCSession);
|
||||
call.session?.emit(
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
[],
|
||||
[{ sender: client.getUserId() } as CallMembership],
|
||||
);
|
||||
}
|
||||
async function runTimers() {
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(500);
|
||||
}
|
||||
sessionConnect();
|
||||
await Promise.all([...(startWidget ? [call.start()] : []), runTimers()]);
|
||||
};
|
||||
const callDisconnectionProcedure: (call: ElementCall) => Promise<void> = async (call) => {
|
||||
async function sessionDisconnect() {
|
||||
await new Promise<void>((r) => {
|
||||
setTimeout(() => r(), 400);
|
||||
});
|
||||
client.matrixRTC.emit(MatrixRTCSessionManagerEvents.SessionStarted, call.roomId, {
|
||||
sessionId: undefined,
|
||||
} as unknown as MatrixRTCSession);
|
||||
call.session?.emit(MatrixRTCSessionEvent.MembershipsChanged, [], []);
|
||||
}
|
||||
async function runTimers() {
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(500);
|
||||
}
|
||||
sessionDisconnect();
|
||||
const promise = call.disconnect();
|
||||
runTimers();
|
||||
await promise;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
({ client, room, alice } = setUpClientRoomAndStores());
|
||||
@@ -835,8 +727,6 @@ describe("ElementCall", () => {
|
||||
let call: ElementCall;
|
||||
let widget: Widget;
|
||||
let messaging: Mocked<ClientWidgetApi>;
|
||||
let audioMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
let videoMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
@@ -847,34 +737,28 @@ describe("ElementCall", () => {
|
||||
if (maybeCall === null) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
({ widget, messaging, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
|
||||
({ widget, messaging } = setUpWidget(call));
|
||||
});
|
||||
|
||||
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
|
||||
afterEach(() => cleanUpCallAndWidget(call, widget));
|
||||
// TODO refactor initial device configuration to use the EW settings.
|
||||
// Add tests for passing EW device configuration to the widget.
|
||||
it("waits for messaging when connecting", async () => {
|
||||
it("waits for messaging when starting", async () => {
|
||||
// Temporarily remove the messaging to simulate connecting while the
|
||||
// widget is still initializing
|
||||
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = callConnectProcedure(call);
|
||||
const startup = call.start();
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
await connect;
|
||||
await startup;
|
||||
await connect(call, messaging, false);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
});
|
||||
|
||||
it("fails to connect if the widget returns an error", async () => {
|
||||
// we only send a JoinCall action if the widget is preloading
|
||||
call.widget.data = { ...call.widget, preload: true };
|
||||
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
|
||||
await expect(call.start()).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("fails to disconnect if the widget returns an error", async () => {
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
|
||||
await expect(call.disconnect()).rejects.toBeDefined();
|
||||
});
|
||||
@@ -882,7 +766,7 @@ describe("ElementCall", () => {
|
||||
it("handles remote disconnection", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
|
||||
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
@@ -892,62 +776,35 @@ describe("ElementCall", () => {
|
||||
|
||||
it("disconnects", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
await callDisconnectionProcedure(call);
|
||||
await disconnect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
|
||||
it("disconnects when we leave the room", async () => {
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
room.emit(RoomEvent.MyMembership, room, KnownMembership.Leave);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
|
||||
it("remains connected if we stay in the room", async () => {
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
room.emit(RoomEvent.MyMembership, room, KnownMembership.Join);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
});
|
||||
|
||||
it("disconnects if the widget dies", async () => {
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
|
||||
it("tracks layout", async () => {
|
||||
await callConnectProcedure(call);
|
||||
expect(call.layout).toBe(Layout.Tile);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.SpotlightLayout}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
expect(call.layout).toBe(Layout.Spotlight);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.TileLayout}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
expect(call.layout).toBe(Layout.Tile);
|
||||
});
|
||||
|
||||
it("sets layout", async () => {
|
||||
await callConnectProcedure(call);
|
||||
|
||||
await call.setLayout(Layout.Spotlight);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
|
||||
|
||||
await call.setLayout(Layout.Tile);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {});
|
||||
});
|
||||
|
||||
it("acknowledges mute_device widget action", async () => {
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
const preventDefault = jest.fn();
|
||||
const mockEv = {
|
||||
preventDefault,
|
||||
@@ -963,8 +820,8 @@ describe("ElementCall", () => {
|
||||
const onConnectionState = jest.fn();
|
||||
call.on(CallEvent.ConnectionState, onConnectionState);
|
||||
|
||||
await callConnectProcedure(call);
|
||||
await callDisconnectionProcedure(call);
|
||||
await connect(call, messaging);
|
||||
await disconnect(call, messaging);
|
||||
expect(onConnectionState.mock.calls).toEqual([
|
||||
[ConnectionState.Connected, ConnectionState.Disconnected],
|
||||
[ConnectionState.Disconnecting, ConnectionState.Connected],
|
||||
@@ -985,29 +842,11 @@ describe("ElementCall", () => {
|
||||
call.off(CallEvent.Participants, onParticipants);
|
||||
});
|
||||
|
||||
it("emits events when layout changes", async () => {
|
||||
await callConnectProcedure(call);
|
||||
const onLayout = jest.fn();
|
||||
call.on(CallEvent.Layout, onLayout);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.SpotlightLayout}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.TileLayout}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
expect(onLayout.mock.calls).toEqual([[Layout.Spotlight], [Layout.Tile]]);
|
||||
|
||||
call.off(CallEvent.Layout, onLayout);
|
||||
});
|
||||
|
||||
it("ends the call immediately if the session ended", async () => {
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
const onDestroy = jest.fn();
|
||||
call.on(CallEvent.Destroy, onDestroy);
|
||||
await callDisconnectionProcedure(call);
|
||||
await disconnect(call, messaging);
|
||||
// this will be called automatically
|
||||
// disconnect -> widget sends state event -> session manager notices no-one left
|
||||
client.matrixRTC.emit(
|
||||
@@ -1043,39 +882,12 @@ describe("ElementCall", () => {
|
||||
roomSpy.mockRestore();
|
||||
addWidgetSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("sends notify event on connect in a room with more than two members", async () => {
|
||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||
ElementCall.create(room);
|
||||
await callConnectProcedure(Call.get(room) as ElementCall);
|
||||
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"notify_type": "notify",
|
||||
});
|
||||
});
|
||||
it("sends ring on create in a DM (two participants) room", async () => {
|
||||
setRoomMembers(["@user:example.com", "@user2:example.com"]);
|
||||
|
||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||
ElementCall.create(room);
|
||||
await callConnectProcedure(Call.get(room) as ElementCall);
|
||||
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"notify_type": "ring",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance in a video room", () => {
|
||||
let call: ElementCall;
|
||||
let widget: Widget;
|
||||
let messaging: Mocked<ClientWidgetApi>;
|
||||
let audioMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
let videoMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
@@ -1088,64 +900,29 @@ describe("ElementCall", () => {
|
||||
if (maybeCall === null) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
({ widget, messaging, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
|
||||
({ widget, messaging } = setUpWidget(call));
|
||||
});
|
||||
|
||||
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
|
||||
afterEach(() => cleanUpCallAndWidget(call, widget));
|
||||
|
||||
it("doesn't end the call when the last participant leaves", async () => {
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
const onDestroy = jest.fn();
|
||||
call.on(CallEvent.Destroy, onDestroy);
|
||||
await callDisconnectionProcedure(call);
|
||||
await disconnect(call, messaging);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
call.off(CallEvent.Destroy, onDestroy);
|
||||
});
|
||||
|
||||
it("connect to call with ongoing session", async () => {
|
||||
// Mock membership getter used by `roomSessionForRoom`.
|
||||
// This makes sure the roomSession will not be empty.
|
||||
jest.spyOn(MatrixRTCSession, "callMembershipsForRoom").mockImplementation(() => [
|
||||
{ fakeVal: "fake membership", getMsUntilExpiry: () => 1000 } as unknown as CallMembership,
|
||||
]);
|
||||
// Create ongoing session
|
||||
const roomSession = MatrixRTCSession.roomSessionForRoom(client, room);
|
||||
const roomSessionEmitSpy = jest.spyOn(roomSession, "emit");
|
||||
|
||||
// Make sure the created session ends up in the call.
|
||||
// `getActiveRoomSession` will be used during `call.connect`
|
||||
// `getRoomSession` will be used during `Call.get`
|
||||
client.matrixRTC.getActiveRoomSession.mockImplementation(() => {
|
||||
return roomSession;
|
||||
});
|
||||
client.matrixRTC.getRoomSession.mockImplementation(() => {
|
||||
return roomSession;
|
||||
});
|
||||
|
||||
ElementCall.create(room);
|
||||
const call = Call.get(room);
|
||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||
expect(call.session).toBe(roomSession);
|
||||
await callConnectProcedure(call);
|
||||
expect(roomSessionEmitSpy).toHaveBeenCalledWith(
|
||||
"memberships_changed",
|
||||
[],
|
||||
[{ sender: "@alice:example.org" }],
|
||||
);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
call.destroy();
|
||||
});
|
||||
|
||||
it("handles remote disconnection and reconnect right after", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await callConnectProcedure(call);
|
||||
await connect(call, messaging);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
|
||||
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||
// We should now be able to reconnect without manually starting the widget
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await callConnectProcedure(call, false);
|
||||
await connect(call, messaging, false);
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected), { interval: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import "../../../../../src/stores/room-list/RoomListStore"; // must be imported
|
||||
import { Algorithm } from "../../../../../src/stores/room-list/algorithms/Algorithm";
|
||||
import { CallStore } from "../../../../../src/stores/CallStore";
|
||||
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { ConnectionState } from "../../../../../src/models/Call";
|
||||
|
||||
describe("Algorithm", () => {
|
||||
useMockedCalls();
|
||||
@@ -83,7 +84,7 @@ describe("Algorithm", () => {
|
||||
|
||||
MockedCall.create(roomWithCall, "1");
|
||||
const call = CallStore.instance.getCall(roomWithCall.roomId);
|
||||
if (call === null) throw new Error("Failed to create call");
|
||||
if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
|
||||
|
||||
const widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, roomWithCall.roomId, {
|
||||
@@ -93,7 +94,7 @@ describe("Algorithm", () => {
|
||||
// End of setup
|
||||
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
await call.start();
|
||||
call.setConnectionState(ConnectionState.Connected);
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([roomWithCall, room]);
|
||||
await call.disconnect();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
|
||||
@@ -103,7 +103,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
|
||||
</p>
|
||||
<div
|
||||
class="mx_Flex mx_ErrorView_buttons"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57"
|
||||
@@ -153,7 +153,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57"
|
||||
@@ -223,7 +223,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x);"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
href="https://apps.apple.com/app/vector/id1083446067"
|
||||
|
||||
73
yarn.lock
@@ -2744,11 +2744,11 @@
|
||||
"@svgr/plugin-svgo" "8.1.0"
|
||||
|
||||
"@testcontainers/postgresql@^10.16.0":
|
||||
version "10.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.18.0.tgz#87d3acb3a4bc2196bd8e9b7496fe6e3146b59f2b"
|
||||
integrity sha512-WxkE/tBlBpoKvqDEqL3i/mL6BOBWnXb8FXKtLhEeZ3lSt0zlldkTozMmewNsKJtFTBZdv7uFwMzWyXP12t0sxQ==
|
||||
version "10.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.19.0.tgz#e1ff9fbfee76c23bc899865524ee8e2ee297bdf2"
|
||||
integrity sha512-3+yQJHCWEtp4hylfZgRxCWN1P6dGqKhFM7Bypg22NpJqq1x/dcmamVCvD+4eTdm1uHV1Ta0BkHRWejxGOyTnrw==
|
||||
dependencies:
|
||||
testcontainers "^10.18.0"
|
||||
testcontainers "^10.19.0"
|
||||
|
||||
"@testing-library/dom@^10.4.0":
|
||||
version "10.4.0"
|
||||
@@ -2916,9 +2916,9 @@
|
||||
"@types/ssh2" "*"
|
||||
|
||||
"@types/dockerode@^3.3.29":
|
||||
version "3.3.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.34.tgz#1cef62f1b98f80bd4460961dd8aac99b95a0fb6e"
|
||||
integrity sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg==
|
||||
version "3.3.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.35.tgz#d78c844a246f8717e3bcf2cc134a976bfa630b10"
|
||||
integrity sha512-P+DCMASlsH+QaKkDpekKrP5pLls767PPs+/LrlVbKnEnY5tMpEUa2C6U4gRsdFZengOqxdCIqy16R22Q3pLB6Q==
|
||||
dependencies:
|
||||
"@types/docker-modem" "*"
|
||||
"@types/node" "*"
|
||||
@@ -3160,19 +3160,26 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "22.13.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca"
|
||||
integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==
|
||||
version "22.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.10.tgz#df9ea358c5ed991266becc3109dc2dc9125d77e4"
|
||||
integrity sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
"@types/node@18", "@types/node@^18.11.18":
|
||||
"@types/node@18":
|
||||
version "18.19.79"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.79.tgz#82fde7ac17809f4738a494b22273f0f7e6754f6e"
|
||||
integrity sha512-90K8Oayimbctc5zTPHPfZloc/lGVs7f3phUAAMcTgEPtg8kKquGZDERC8K4vkBYkQQh48msiYUslYtxTWvqcAg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.19.80"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.80.tgz#6d6008e8920dddcd23f9dd33da24684ef57d487c"
|
||||
integrity sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
|
||||
@@ -4334,9 +4341,9 @@ bare-fs@^4.0.1:
|
||||
bare-stream "^2.0.0"
|
||||
|
||||
bare-os@^3.0.1:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.4.0.tgz#97be31503f3095beb232a6871f0118859832eb0c"
|
||||
integrity sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.6.0.tgz#1465dd7e1bebe0dec230097a23ad00f7db51f957"
|
||||
integrity sha512-BUrFS5TqSBdA0LwHop4OjPJwisqxGy6JsWVqV6qaFoe965qqtaKfDzHY5T2YA1gUL0ZeeQeA+4BBc1FJTcHiPw==
|
||||
|
||||
bare-path@^3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -5071,19 +5078,19 @@ cronstrue@^2.41.0:
|
||||
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.56.0.tgz#70b406106e6059bf9c494c788255d671d8ecc361"
|
||||
integrity sha512-/YC3b4D/E/S8ToQ7f676A2fqoC3vVpXKjJ4SMsP0jYsvRYJdZ6h9+Fq/Y7FoFDEUFCqLTca+G2qTV227lyyFZg==
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.3:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
|
||||
cross-spawn@^7.0.2:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
|
||||
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cross-spawn@^7.0.2:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
|
||||
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
|
||||
cross-spawn@^7.0.3, cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
@@ -6640,11 +6647,11 @@ foreachasync@^3.0.0:
|
||||
integrity sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==
|
||||
|
||||
foreground-child@^3.1.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
|
||||
integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
|
||||
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.0"
|
||||
cross-spawn "^7.0.6"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
form-data@^4.0.0:
|
||||
@@ -9052,9 +9059,9 @@ murmurhash-js@^1.0.0:
|
||||
integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
|
||||
|
||||
nan@^2.19.0, nan@^2.20.0:
|
||||
version "2.22.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3"
|
||||
integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==
|
||||
version "2.22.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb"
|
||||
integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==
|
||||
|
||||
nanoid@^3.3.7, nanoid@^3.3.8:
|
||||
version "3.3.8"
|
||||
@@ -11944,10 +11951,10 @@ test-exclude@^6.0.0:
|
||||
glob "^7.1.4"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
testcontainers@^10.16.0, testcontainers@^10.18.0:
|
||||
version "10.18.0"
|
||||
resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.18.0.tgz#dbd74e1d6e5de431414a06fb2c2ed9d82971739e"
|
||||
integrity sha512-MnwWsPjsN5QVe+lSU1LwLZVOyjgwSwv1INzkw8FekdwgvOtvJ7FThQEkbmzRcguQootgwmA9FG54NoTChZDRvA==
|
||||
testcontainers@^10.16.0, testcontainers@^10.19.0:
|
||||
version "10.19.0"
|
||||
resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.19.0.tgz#007138559c6de68c80334232a259f4e94fa19955"
|
||||
integrity sha512-/mbcCOaj6jj2IPMMmt+YrBi71MZ4BqEzqicjAInsfEox4pVVMnYIW4CkWOdCLiuZ9nVUkoBtxFSJDTqggJNB5A==
|
||||
dependencies:
|
||||
"@balena/dockerignore" "^1.0.2"
|
||||
"@types/dockerode" "^3.3.29"
|
||||
|
||||