mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Compare commits
57 Commits
header-sub
...
stable-cla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11fadebb87 | ||
|
|
943ce5b853 | ||
|
|
000ee66cfe | ||
|
|
58a8b7316e | ||
|
|
1e6d3d3357 | ||
|
|
dc21233767 | ||
|
|
374f56651c | ||
|
|
59999b9b37 | ||
|
|
ad76969ce2 | ||
|
|
c170aad364 | ||
|
|
9e8bebc3bf | ||
|
|
d5cdbfd4d1 | ||
|
|
9199dcb583 | ||
|
|
cbc4b77a4f | ||
|
|
ffb881436a | ||
|
|
9cf87efc6b | ||
|
|
b71454d63a | ||
|
|
b0feb33ec4 | ||
|
|
3837576a5f | ||
|
|
eb5a097999 | ||
|
|
db2d20ac56 | ||
|
|
f276c51772 | ||
|
|
ac2214ed77 | ||
|
|
8fe7bb5329 | ||
|
|
8028a6aa24 | ||
|
|
5b4e8865e5 | ||
|
|
356d526116 | ||
|
|
cca69b8108 | ||
|
|
75235a7853 | ||
|
|
0421783ebe | ||
|
|
a1be4ff0f9 | ||
|
|
8ab7f89e36 | ||
|
|
ddcb0f9981 | ||
|
|
6ca5ddda73 | ||
|
|
41d0933d4d | ||
|
|
5ad62c7097 | ||
|
|
76714ac3ad | ||
|
|
15b40835b8 | ||
|
|
dc5b541b21 | ||
|
|
f376ffb96b | ||
|
|
d5ce3a955a | ||
|
|
d2cb7e334e | ||
|
|
9d61ca13cb | ||
|
|
51bd17d335 | ||
|
|
1cd2764eb2 | ||
|
|
3fb81032e0 | ||
|
|
da942ea900 | ||
|
|
d534c617d2 | ||
|
|
ded3d0ab81 | ||
|
|
a4b4a6b655 | ||
|
|
be5d8140c9 | ||
|
|
7d54487354 | ||
|
|
93fae5dd67 | ||
|
|
504888b359 | ||
|
|
99e95658df | ||
|
|
5a68010022 | ||
|
|
c4fe0223ab |
120
.eslintrc.cjs
120
.eslintrc.cjs
@@ -1,120 +0,0 @@
|
||||
const COPYRIGHT_HEADER = `/*
|
||||
Copyright %%CURRENT_YEAR%% New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "rxjs", "jsdoc", "element-call"],
|
||||
extends: [
|
||||
"plugin:matrix-org/react",
|
||||
"plugin:matrix-org/a11y",
|
||||
"plugin:matrix-org/typescript",
|
||||
"prettier",
|
||||
"plugin:rxjs/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"element-call/no-observablescope-leak": "error",
|
||||
"jsdoc/no-types": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
"jsdoc/check-values": "error",
|
||||
"jsdoc/check-param-names": "warn",
|
||||
// "jsdoc/require-param": "warn",
|
||||
"jsdoc/require-param-description": "warn",
|
||||
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
"react/display-name": "error",
|
||||
// Encourage proper usage of Promises:
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
// To help ensure that we get proper vite/rollup lazy loading (e.g. for matrix-js-sdk):
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ fixStyle: "inline-type-imports" },
|
||||
],
|
||||
// To encourage good usage of RxJS:
|
||||
"rxjs/no-exposed-subjects": "error",
|
||||
"rxjs/finnish": ["error", { names: { "^this$": false } }],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: ["matrix-widget-api", "matrix-js-sdk"].flatMap((lib) =>
|
||||
["src", "src/", "src/index", "lib", "lib/", "lib/index"]
|
||||
.map((path) => `${lib}/${path}`)
|
||||
.map((name) => ({ name, message: `Please use ${lib} instead` })),
|
||||
),
|
||||
patterns: [
|
||||
...["matrix-widget-api"].map((lib) => ({
|
||||
group: ["src", "src/", "src/**", "lib", "lib/", "lib/**"].map(
|
||||
(path) => `${lib}/${path}`,
|
||||
),
|
||||
message: `Please use ${lib} instead`,
|
||||
})),
|
||||
// XXX: We use /lib in lots of places, so allow for now.
|
||||
...["matrix-js-sdk"].map((lib) => ({
|
||||
group: ["src", "src/", "src/**"].map((path) => `${lib}/${path}`),
|
||||
message: `Please use ${lib} instead`,
|
||||
})),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["src/*/**"],
|
||||
rules: {
|
||||
// In application code we should use the js-sdk logger, never console directly.
|
||||
"no-console": ["error"],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/test.ts",
|
||||
"**/test.tsx",
|
||||
"**/test-**",
|
||||
],
|
||||
rules: {
|
||||
// Tests often initialize an ObservableScope in an outer scope in
|
||||
// beforeEach, which is not actually a problem
|
||||
"element-call/no-observablescope-leak": "off",
|
||||
"jsdoc/no-types": "off",
|
||||
"jsdoc/empty-tags": "off",
|
||||
"jsdoc/check-property-names": "off",
|
||||
"jsdoc/check-values": "off",
|
||||
"jsdoc/check-param-names": "off",
|
||||
"jsdoc/require-param-description": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["playwright/**"],
|
||||
rules: {
|
||||
// Playwright as a `use` function that has nothing to do with React hooks.
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
};
|
||||
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@@ -24,8 +24,8 @@ jobs:
|
||||
run: "pnpm run format:check"
|
||||
- name: i18n
|
||||
run: "pnpm run i18n:check"
|
||||
- name: ESLint
|
||||
run: "pnpm run lint:eslint"
|
||||
- name: Lint
|
||||
run: "pnpm run lint:oxlint"
|
||||
- name: Type check
|
||||
run: "pnpm run lint:types"
|
||||
- name: Dead code analysis
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
container:
|
||||
# Make sure to grab the latest version of the Playwright image
|
||||
# https://playwright.dev/docs/docker#pull-the-image
|
||||
image: mcr.microsoft.com/playwright:v1.60.0-noble
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -15,9 +15,9 @@ jobs:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
|
||||
153
.oxlintrc.json
Normal file
153
.oxlintrc.json
Normal file
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"import",
|
||||
"jsdoc",
|
||||
"jsx-a11y",
|
||||
"promise",
|
||||
"react",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"vitest"
|
||||
],
|
||||
"jsPlugins": [
|
||||
"eslint-plugin-storybook",
|
||||
"eslint-plugin-element-call"
|
||||
// TODO: Re-enable once oxlint supports lint rules that rely on TypeScript type-awareness.
|
||||
// "eslint-plugin-rxjs"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "error",
|
||||
"perf": "error"
|
||||
},
|
||||
"options": {
|
||||
"denyWarnings": true,
|
||||
"typeAware": true
|
||||
},
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
"rules": {
|
||||
"element-call/copyright-header": [
|
||||
"error",
|
||||
"/*\nCopyright %%CURRENT_YEAR%% Element Creations Ltd.\n\nSPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial\nPlease see LICENSE in the repository root for full details.\n*/\n\n"
|
||||
],
|
||||
"element-call/no-observablescope-leak": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
"jsdoc/require-param-description": "warn",
|
||||
"react/display-name": "error",
|
||||
// TODO: Re-enable once oxlint supports lint rules that rely on TypeScript type-awareness.
|
||||
// "rxjs/no-exposed-subjects": "error",
|
||||
// "rxjs/finnish": [
|
||||
// "error",
|
||||
// {
|
||||
// "names": {
|
||||
// "^this$": false
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"matrix-widget-api/src",
|
||||
"matrix-widget-api/src/",
|
||||
"matrix-widget-api/src/**",
|
||||
"matrix-widget-api/lib",
|
||||
"matrix-widget-api/lib/",
|
||||
"matrix-widget-api/lib/**"
|
||||
],
|
||||
"message": "Please use matrix-widget-api instead"
|
||||
},
|
||||
{
|
||||
"group": [
|
||||
"matrix-js-sdk/src",
|
||||
"matrix-js-sdk/src/",
|
||||
"matrix-js-sdk/src/**",
|
||||
"matrix-js-sdk/lib",
|
||||
"matrix-js-sdk/lib/",
|
||||
"matrix-js-sdk/lib/index"
|
||||
],
|
||||
"message": "Please use matrix-js-sdk instead"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"typescript/no-floating-promises": "error",
|
||||
"typescript/no-misused-promises": "error",
|
||||
"typescript/promise-function-async": "error",
|
||||
"typescript/require-await": "error",
|
||||
"typescript/await-thenable": "error",
|
||||
// To help ensure that we get proper vite/rollup lazy loading (e.g. for matrix-js-sdk).
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
// TODO: These had to be disabled in the eslint -> oxlint migration. Would be nice to
|
||||
// enable them in future or at least document why we're disabling them.
|
||||
"eslint/no-await-in-loop": "off",
|
||||
"eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"import/default": "off",
|
||||
"jsdoc/check-tag-names": "off",
|
||||
"jsx-a11y/prefer-tag-over-role": "off",
|
||||
"promise/no-callback-in-promise": "off",
|
||||
"react/jsx-key": "off",
|
||||
"react/jsx-no-constructed-context-values": "off",
|
||||
"react/no-array-index-key": "off",
|
||||
"react/no-children-prop": "off",
|
||||
"react/no-object-type-as-default-prop": "off",
|
||||
"typescript/no-misused-spread": "off",
|
||||
"typescript/no-useless-default-assignment": "off",
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"vitest/expect-expect": "off",
|
||||
"vitest/no-conditional-expect": "off",
|
||||
"vitest/no-disabled-tests": "off",
|
||||
"vitest/require-mock-type-parameters": "off",
|
||||
"vitest/require-to-throw-message": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/*/**"],
|
||||
"rules": {
|
||||
// In application code we should use the js-sdk logger, never console directly.
|
||||
"no-console": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/test.ts",
|
||||
"**/test.tsx",
|
||||
"**/test-**"
|
||||
],
|
||||
"rules": {
|
||||
// Tests often initialize an ObservableScope in an outer scope in
|
||||
// beforeEach, which is not actually a problem
|
||||
"element-call/no-observablescope-leak": "off",
|
||||
"jsdoc/empty-tags": "off",
|
||||
"jsdoc/check-property-names": "off",
|
||||
"jsdoc/require-param-description": "off",
|
||||
"jsx-a11y/media-has-caption": "off"
|
||||
// TODO: Enable once oxlint supports them.
|
||||
// "jsdoc/check-values": "off",
|
||||
// "jsdoc/check-param-names": "off",
|
||||
// "jsdoc/no-types": "off",
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["playwright/**"],
|
||||
"rules": {
|
||||
// Playwright as a `use` function that has nothing to do with React hooks.
|
||||
"react-hooks/rules-of-hooks": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"feature_use_device_session_member_events": true
|
||||
},
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
"matrix_rtc_mode": "legacy",
|
||||
"matrix_rtc_mode": "compatibility",
|
||||
"matrix_rtc_session": {
|
||||
"wait_for_key_rotation_ms": 3000,
|
||||
"membership_event_expiry_ms": 180000000,
|
||||
|
||||
66
eslint/CopyrightHeader.js
Normal file
66
eslint/CopyrightHeader.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ESLintUtils } from "@typescript-eslint/utils";
|
||||
|
||||
const rule = ESLintUtils.RuleCreator(
|
||||
() => "https://github.com/element-hq/element-call",
|
||||
)({
|
||||
name: "copyright-header",
|
||||
meta: {
|
||||
type: "problem",
|
||||
fixable: "code",
|
||||
docs: {
|
||||
description: "Require a copyright header in files.",
|
||||
},
|
||||
messages: {
|
||||
noHeader: "Copyright header is required.",
|
||||
},
|
||||
schema: [{ type: "string" }],
|
||||
},
|
||||
create(context) {
|
||||
const code = context.getSourceCode();
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const firstToken = code.getFirstToken(node, { includeComments: false });
|
||||
|
||||
if (!firstToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headComments = code.getCommentsBefore(firstToken);
|
||||
const hasSomeCopyrightHeader = headComments?.some((comment) =>
|
||||
comment?.value?.includes("Copyright"),
|
||||
);
|
||||
if (hasSomeCopyrightHeader) {
|
||||
return;
|
||||
}
|
||||
const headerTemplate = context.options[0];
|
||||
const fix = headerTemplate
|
||||
? function (fixer) {
|
||||
return fixer.insertTextBefore(
|
||||
firstToken,
|
||||
headerTemplate.replace(
|
||||
/%%CURRENT_YEAR%%/g,
|
||||
new Date().getFullYear(),
|
||||
),
|
||||
);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
context.report({
|
||||
messageId: "noHeader",
|
||||
node,
|
||||
fix,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default rule;
|
||||
@@ -9,7 +9,21 @@ import { ESLintUtils } from "@typescript-eslint/utils";
|
||||
|
||||
// These ObservableScope methods will not generally cause resource leaks even if
|
||||
// called from a callback
|
||||
const safeScopeMethods = ["bind", "end"];
|
||||
const safeScopeMethods = new Set(["bind", "end"]);
|
||||
|
||||
/**
|
||||
* Determines whether the variable with the given name is local to
|
||||
* the enclosing function or class scope.
|
||||
*/
|
||||
function isLocal(name, scope) {
|
||||
// If it is nowhere to be found in the "through" scope, it is local.
|
||||
if (!scope.through.some(({ identifier }) => identifier.name === name))
|
||||
return true;
|
||||
if (scope.type === "function" || scope.type === "class") return false;
|
||||
// If this is something other than a function or class scope, check its outer
|
||||
// scope.
|
||||
return !scope.upper || isLocal(name, scope.upper);
|
||||
}
|
||||
|
||||
const rule = ESLintUtils.RuleCreator(
|
||||
() => "https://github.com/element-hq/element-call",
|
||||
@@ -32,27 +46,29 @@ const rule = ESLintUtils.RuleCreator(
|
||||
Identifier(node) {
|
||||
const scope = context.sourceCode.getScope(node);
|
||||
if (
|
||||
// Is this a reference to a variable defined in an outer ("through") scope?
|
||||
scope.through.some(
|
||||
({ identifier }) => identifier.name === node.name,
|
||||
) &&
|
||||
// Is this a reference to a variable defined in an outer scope?
|
||||
!isLocal(node.name, scope) &&
|
||||
// Exclude calls to "safe" ObservableScope methods
|
||||
node.parent?.type === "MemberExpression" &&
|
||||
node.parent.object === node &&
|
||||
node.parent.property.type === "Identifier" &&
|
||||
!safeScopeMethods.includes(node.parent.property.name)
|
||||
!safeScopeMethods.has(node.parent.property.name) &&
|
||||
/(^s|S)cope$/.test(node.name)
|
||||
) {
|
||||
// Verify that the variable is actually of type ObservableScope
|
||||
// (expensive, so we check this last)
|
||||
const services = ESLintUtils.getParserServices(context);
|
||||
const type = services.getTypeAtLocation(node);
|
||||
if (type.symbol?.name === "ObservableScope")
|
||||
// This ObservableScope method call may be causing resource leaks.
|
||||
context.report({
|
||||
messageId: "scopeLeak",
|
||||
loc: node.loc,
|
||||
node,
|
||||
});
|
||||
// TODO: Once oxlint supports lint rules that rely on TypeScript type-awareness,
|
||||
// Verify that the variable is actually of type ObservableScope rather than just
|
||||
// checking its name. This is expensive so we should do this last.
|
||||
//
|
||||
// const services = ESLintUtils.getParserServices(context);
|
||||
// const type = services.getTypeAtLocation(node);
|
||||
// if (type.symbol?.name === "ObservableScope") { ... }
|
||||
|
||||
// This ObservableScope method call may be causing resource leaks.
|
||||
context.report({
|
||||
messageId: "scopeLeak",
|
||||
loc: node.loc,
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
"copyright-header": require("./CopyrightHeader").default,
|
||||
"no-observablescope-leak": require("./NoObservableScopeLeak").default,
|
||||
},
|
||||
};
|
||||
|
||||
3
knip.ts
3
knip.ts
@@ -32,6 +32,9 @@ export default {
|
||||
// https://github.com/webpro-nl/knip/issues/766
|
||||
"@vector-im/compound-web",
|
||||
"matrix-widget-api",
|
||||
// Used by oxlint
|
||||
"eslint-plugin-element-call",
|
||||
"eslint-plugin-storybook",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
} satisfies KnipConfig;
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
"blur_not_supported_by_browser": "(Background blur is not supported by this device.)",
|
||||
"developer_tab_title": "Developer",
|
||||
"devices": {
|
||||
"activating": "Activating…",
|
||||
"camera": "Camera",
|
||||
"camera_numbered": "Camera {{n}}",
|
||||
"change_device_button": "Change audio device",
|
||||
|
||||
30
package.json
30
package.json
@@ -19,9 +19,9 @@
|
||||
"serve": "vite preview",
|
||||
"format": "oxfmt",
|
||||
"format:check": "oxfmt --check; rc=$?; [[ $rc -ne 0 ]] && printf '\\033[46;30m INFO \\033[0m To fix, run: pnpm format\\n' >&2; exit $rc",
|
||||
"lint": "pnpm lint:types && pnpm lint:eslint && pnpm lint:knip",
|
||||
"lint:eslint": "eslint --max-warnings 0 src playwright",
|
||||
"lint:eslint-fix": "eslint --max-warnings 0 src playwright --fix",
|
||||
"lint": "pnpm lint:types && pnpm lint:oxlint && pnpm lint:knip",
|
||||
"lint:oxlint": "oxlint src playwright",
|
||||
"lint:oxlint-fix": "oxlint --fix src playwright",
|
||||
"lint:knip": "knip",
|
||||
"lint:types": "tsc",
|
||||
"i18n": "npx i18next-cli extract",
|
||||
@@ -47,7 +47,6 @@
|
||||
"@formatjs/intl-segmenter": "^11.7.3",
|
||||
"@livekit/components-core": "^0.12.0",
|
||||
"@livekit/components-react": "^2.0.0",
|
||||
"@livekit/protocol": "^1.42.2",
|
||||
"@livekit/track-processors": "^0.7.1",
|
||||
"@mediapipe/tasks-vision": "^0.10.18",
|
||||
"@playwright/test": "^1.60.0",
|
||||
@@ -60,7 +59,6 @@
|
||||
"@storybook/addon-docs": "^10.3.6",
|
||||
"@storybook/addon-vitest": "^10.3.6",
|
||||
"@storybook/react-vite": "^10.3.6",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@testing-library/dom": "^10.1.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
@@ -75,8 +73,6 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"@typescript-eslint/utils": "^8.61.0",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vector-im/compound-design-tokens": "^10.0.0",
|
||||
@@ -84,25 +80,12 @@
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitest/browser-playwright": "^4.1.5",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@vitest/ui": "4.1.7",
|
||||
"@vitest/ui": "4.1.9",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-deprecate": "^0.9.0",
|
||||
"eslint-plugin-element-call": "link:eslint",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^61.5.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "2.1.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-rxjs": "^5.0.3",
|
||||
"eslint-plugin-storybook": "^10.3.6",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fetch-mock": "11.1.5",
|
||||
"global-jsdom": "^26.0.0",
|
||||
"i18next": "^25.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-cli": "^1.61.0",
|
||||
@@ -116,7 +99,9 @@
|
||||
"node-stdlib-browser": "^1.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
"oxfmt": "^0.55.0",
|
||||
"oxfmt": "^0.56.0",
|
||||
"oxlint": "^1.70.0",
|
||||
"oxlint-tsgolint": "^0.23.0",
|
||||
"pako": "^2.0.4",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
@@ -131,7 +116,6 @@
|
||||
"sass": "^1.42.1",
|
||||
"storybook": "^10.3.6",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"uuid": "^14.0.0",
|
||||
"vaul": "^1.0.0",
|
||||
|
||||
@@ -120,6 +120,11 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
.getByRole("heading", { name: "Welcome Room" }),
|
||||
).toBeVisible();
|
||||
} else if (callType === "dm") {
|
||||
await TestHelpers.closeReleaseAnnouncement(
|
||||
ewPage1,
|
||||
"Introducing Sections",
|
||||
);
|
||||
|
||||
await ewPage1
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
|
||||
@@ -88,15 +88,18 @@ test("When creator left, avoid reconnect to the same SFU", async ({
|
||||
await guestCPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||
|
||||
await guestCPage.waitForTimeout(1000);
|
||||
|
||||
if (wsConnectionCount === 2) {
|
||||
console.warn("wsConnectionCount is 2, expecting 1 after join");
|
||||
}
|
||||
const wsConnectionCountBeforeLeave = wsConnectionCount;
|
||||
// ========
|
||||
// the creator leaves the call
|
||||
await creatorPage.getByTestId("incall_leave").click();
|
||||
|
||||
// https://github.com/element-hq/element-call/issues/3344
|
||||
// The app used to request a new jwt token then to reconnect to the SFU
|
||||
expect(wsConnectionCount).toBe(1);
|
||||
expect(wsConnectionCount).toBe(wsConnectionCountBeforeLeave);
|
||||
// Wait a bit to be sure that if there was a reconnect, it would have happened by now
|
||||
await guestCPage.waitForTimeout(6000);
|
||||
expect(wsConnectionCount).toBe(1);
|
||||
expect(wsConnectionCount).toBe(wsConnectionCountBeforeLeave);
|
||||
});
|
||||
|
||||
@@ -160,6 +160,7 @@ export class TestHelpers {
|
||||
const expectedToasts = [
|
||||
{ title: "Failed to load service worker", button: "OK" },
|
||||
{ title: "Back up your chats", button: "Dismiss" },
|
||||
{ title: "Turn on key storage", button: "Dismiss" },
|
||||
{ title: "Element does not support this browser", button: "Dismiss" },
|
||||
];
|
||||
|
||||
@@ -189,11 +190,27 @@ export class TestHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
public static async closeReleaseAnnouncement(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await page
|
||||
.getByRole("dialog", { name })
|
||||
.getByRole("button", { name: "OK" })
|
||||
.click({ timeout: 2000 });
|
||||
} catch {
|
||||
// Announcement not shown; nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
public static async createRoom(
|
||||
name: string,
|
||||
page: Page,
|
||||
andInvite: string[] = [],
|
||||
): Promise<void> {
|
||||
await TestHelpers.closeReleaseAnnouncement(page, "Introducing Sections");
|
||||
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
|
||||
@@ -31,12 +31,10 @@ widgetTest(
|
||||
const brooksFrame = brooks.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
|
||||
// We should show a ringing tile, let's check for that
|
||||
await expect(
|
||||
brooksFrame
|
||||
.getByTestId("videoTile")
|
||||
.filter({ has: brooksFrame.getByText(whistler.displayName) })
|
||||
.filter({ has: brooksFrame.getByText("Calling…") }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -83,23 +81,22 @@ widgetTest(
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
|
||||
// So first we need to confirm that it is hidden when in the call.
|
||||
await expect(
|
||||
whistler.page.locator(".mx_BasicMessageComposer"),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
brooks.page.locator(".mx_BasicMessageComposer"),
|
||||
).not.toBeVisible();
|
||||
|
||||
// ASSERT hanging up on one side ends the call for both
|
||||
await brooksFrame.getByRole("button", { name: "End call" }).click();
|
||||
|
||||
// The widget should be closed on both sides and the timeline should be back on screen
|
||||
// We confirm that we started in Pip mode (voice call) and check that we still see the composer.
|
||||
await expect(
|
||||
whistler.page.locator(".mx_BasicMessageComposer"),
|
||||
).toBeVisible();
|
||||
await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible();
|
||||
|
||||
// ASSERT hanging up on one side ends the call for both
|
||||
await brooksFrame.getByRole("button", { name: "End call" }).click();
|
||||
|
||||
// The widget should be closed on both sides
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
5658
pnpm-lock.yaml
generated
5658
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,7 @@
|
||||
}
|
||||
],
|
||||
"semanticCommits": "disabled",
|
||||
"ignoreDeps": ["eslint-plugin-matrix-org"],
|
||||
"ignoreDeps": [],
|
||||
"vulnerabilityAlerts": {
|
||||
"schedule": ["at any time"],
|
||||
"prHourlyLimit": 0,
|
||||
|
||||
@@ -67,6 +67,29 @@
|
||||
"subtitle subtitle subtitle";
|
||||
}
|
||||
|
||||
/* Hide everything but the subtitle in small windows */
|
||||
@media (max-height: 450px) {
|
||||
.bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bar:has(.subtitle) {
|
||||
display: initial;
|
||||
|
||||
.title,
|
||||
.primaryButton,
|
||||
.secondaryButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> header {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: var(--cpd-space-5x) minmax(var(--cpd-space-5x), auto);
|
||||
grid-template-areas: "." "subtitle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
grid-area: primaryButton;
|
||||
justify-self: start;
|
||||
@@ -141,4 +164,16 @@ body[data-platform="ios"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide everything but the subtitle in small windows */
|
||||
@media (max-height: 450px) {
|
||||
.bar:has(.subtitle) > header {
|
||||
grid-template-rows: var(--cpd-space-4x) minmax(var(--cpd-space-5x), auto);
|
||||
grid-template-areas: "." "subtitle";
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
|
||||
};
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: Error | unknown;
|
||||
error: unknown;
|
||||
widget: WidgetHelpers | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Config } from "./config/Config";
|
||||
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "./e2ee/e2eeType";
|
||||
import { platform } from "./Platform";
|
||||
import { redact } from "./utils/redact";
|
||||
|
||||
interface RoomIdentifier {
|
||||
roomAlias: string | null;
|
||||
@@ -494,7 +495,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
"intent:",
|
||||
intent,
|
||||
"\nproperties:",
|
||||
properties,
|
||||
redact(properties, "password"),
|
||||
"configuration:",
|
||||
configuration,
|
||||
);
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
exports[`AppBar > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="bar"
|
||||
class="_bar_221541"
|
||||
>
|
||||
<header>
|
||||
<button
|
||||
aria-labelledby="_r_0_"
|
||||
class="_icon-button_1215g_8 primaryButton"
|
||||
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
@@ -33,7 +33,7 @@ exports[`AppBar > renders 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="secondaryButton"
|
||||
class="_secondaryButton_221541"
|
||||
/>
|
||||
</header>
|
||||
</div>
|
||||
@@ -46,12 +46,12 @@ exports[`AppBar > renders 1`] = `
|
||||
exports[`AppBar > renders with title and subtitle 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="bar"
|
||||
class="_bar_221541"
|
||||
>
|
||||
<header>
|
||||
<button
|
||||
aria-labelledby="_r_6_"
|
||||
class="_icon-button_1215g_8 primaryButton"
|
||||
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
@@ -76,17 +76,17 @@ exports[`AppBar > renders with title and subtitle 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-body-lg-medium_6v6n8_79 title"
|
||||
class="_typography_6v6n8_153 _font-body-lg-medium_6v6n8_79 _title_221541"
|
||||
>
|
||||
Title
|
||||
</h1>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69 subtitle"
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69 _subtitle_221541"
|
||||
>
|
||||
Subtitle
|
||||
</span>
|
||||
<div
|
||||
class="secondaryButton"
|
||||
class="_secondaryButton_221541"
|
||||
/>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`the content is rendered when the modal is open 1`] = `
|
||||
<div
|
||||
aria-labelledby="radix-_r_4_"
|
||||
class="overlay animate modal dialog _glass_sepwu_8"
|
||||
class="_overlay_2f5303 _animate_2f5303 _modal_dbeffe _dialog_dbeffe _glass_sepwu_8"
|
||||
data-state="open"
|
||||
id="radix-_r_3_"
|
||||
role="dialog"
|
||||
@@ -11,10 +11,10 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_dbeffe"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
class="_header_dbeffe"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
@@ -24,7 +24,7 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
class="_body_dbeffe"
|
||||
>
|
||||
<p>
|
||||
This is the content.
|
||||
@@ -37,7 +37,7 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
<div
|
||||
aria-labelledby="radix-_r_a_"
|
||||
class="overlay modal drawer"
|
||||
class="_overlay_2f5303 _modal_dbeffe _drawer_dbeffe"
|
||||
data-state="open"
|
||||
data-vaul-animate="true"
|
||||
data-vaul-custom-container="false"
|
||||
@@ -51,13 +51,13 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_dbeffe"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
class="_header_dbeffe"
|
||||
>
|
||||
<div
|
||||
class="handle"
|
||||
class="_handle_dbeffe"
|
||||
/>
|
||||
<h2
|
||||
id="radix-_r_a_"
|
||||
@@ -67,7 +67,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
class="_body_dbeffe"
|
||||
>
|
||||
<p>
|
||||
This is the content.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`QrCode > renders 1`] = `
|
||||
<div
|
||||
class="qrCode bar"
|
||||
class="_qrCode_458264 bar"
|
||||
>
|
||||
<img
|
||||
alt="QR Code"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`Toast > renders 1`] = `
|
||||
<button
|
||||
aria-labelledby="radix-_r_4_"
|
||||
class="overlay animate toast"
|
||||
class="_overlay_2f5303 _animate_2f5303 _toast_15a045"
|
||||
data-state="open"
|
||||
id="radix-_r_3_"
|
||||
role="dialog"
|
||||
|
||||
@@ -173,7 +173,7 @@ export const LoudspeakerButton: FC<LoudspeakerButtonProps> = ({
|
||||
iconOnly
|
||||
Icon={loudspeakerModeEnabled ? VolumeOnSolidIcon : VolumeOffSolidIcon}
|
||||
{...props}
|
||||
kind={loudspeakerModeEnabled ? "primary" : "secondary"}
|
||||
kind={loudspeakerModeEnabled ? "secondary" : "primary"}
|
||||
aria-checked={loudspeakerModeEnabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby="_r_1j_"
|
||||
class="_button_1nw83_8 raisedButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _raisedButton_fb25ab _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
|
||||
@@ -180,12 +180,12 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Microphone" }));
|
||||
screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 1" });
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 2" });
|
||||
await user.keyboard("[Escape]");
|
||||
await user.click(screen.getByRole("button", { name: "Camera" }));
|
||||
screen.getByRole("menuitem", { name: "Camera 1" });
|
||||
screen.getByRole("menuitem", { name: "Camera 2" });
|
||||
screen.getByRole("menuitemradio", { name: "Camera 1" });
|
||||
screen.getByRole("menuitemradio", { name: "Camera 2" });
|
||||
});
|
||||
|
||||
test("calls select callback on menu click", async () => {
|
||||
@@ -206,7 +206,9 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
await user.click(
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 2" }),
|
||||
);
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith("mic2");
|
||||
});
|
||||
@@ -228,7 +230,9 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 1" }));
|
||||
await user.click(
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 1" }),
|
||||
);
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -264,18 +268,24 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
const { getByRole } = renderComponent(<Wrapper />);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
await user.click(
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 2" }),
|
||||
);
|
||||
|
||||
expect(onSelectPressed).toHaveBeenCalled();
|
||||
expect(onOptionUpdated).not.toHaveBeenCalled();
|
||||
// After clicking, plannedSelection="mic2" but selectedOption is still "mic1",
|
||||
// so a spinner should appear on the mic2 item
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
expect(mic2Item.querySelector(".rotate")).toBeTruthy();
|
||||
// so mic2 should be in an activating state
|
||||
screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 2 Activating…",
|
||||
checked: false,
|
||||
});
|
||||
|
||||
// The currently-selected mic1 item should not have a spinner
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
expect(mic1Item.querySelector(".rotate")).toBeNull();
|
||||
// The currently-selected mic1 item should not be activating
|
||||
screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 1",
|
||||
checked: true,
|
||||
});
|
||||
await act(async () => {
|
||||
// resolve the promise that acutally updates the select option.
|
||||
resolve();
|
||||
@@ -284,7 +294,7 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
|
||||
expect(onOptionUpdated).toHaveBeenCalled();
|
||||
// Spinner should now be gone since the selection has caught up
|
||||
const mic2ItemAfter = screen.getByRole("menuitem", {
|
||||
const mic2ItemAfter = screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 2",
|
||||
});
|
||||
expect(mic2ItemAfter.querySelector(".rotate")).toBeNull();
|
||||
@@ -336,11 +346,15 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
|
||||
// The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
const mic1Item = screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 2",
|
||||
});
|
||||
expect(mic1Item.querySelectorAll("svg").length).toBe(2);
|
||||
|
||||
// The unselected item (mic1) only renders its IconOptions SVG
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
const mic2Item = screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 1",
|
||||
});
|
||||
expect(mic2Item.querySelectorAll("svg").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,6 +191,7 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.itemIcon}
|
||||
aria-hidden
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -201,10 +202,23 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
onSelect?.(id);
|
||||
}}
|
||||
key={id}
|
||||
role="menuitemradio"
|
||||
aria-checked={selectedOption === id}
|
||||
>
|
||||
{selectedOption === id && <CheckIcon width={24} height={24} />}
|
||||
{selectedOption === id && (
|
||||
<CheckIcon
|
||||
width={24}
|
||||
height={24}
|
||||
aria-hidden // A label would be redundant to aria-checked above
|
||||
/>
|
||||
)}
|
||||
{selectedOption !== id && plannedSelection === id && (
|
||||
<SpinnerIcon width={24} height={24} className={styles.rotate} />
|
||||
<SpinnerIcon
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.rotate}
|
||||
aria-label={t("settings.devices.activating")}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_e649de"
|
||||
>
|
||||
<button
|
||||
aria-busy="false"
|
||||
@@ -35,7 +35,7 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Microphone"
|
||||
class="_button_1nw83_8 menuButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _menuButton_e649de _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
data-state="closed"
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import {
|
||||
ProcessorWrapper,
|
||||
supportsBackgroundProcessors,
|
||||
supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk,
|
||||
type BackgroundOptions,
|
||||
} from "@livekit/track-processors";
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer";
|
||||
import { type Behavior } from "../state/Behavior";
|
||||
import { type ObservableScope } from "../state/ObservableScope";
|
||||
import { platform } from "../Platform";
|
||||
|
||||
//TODO-MULTI-SFU: This is not yet fully there.
|
||||
// it is a combination of exposing observable and react hooks.
|
||||
@@ -106,6 +107,10 @@ interface Props {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
function supportsBackgroundProcessors(): boolean {
|
||||
return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop";
|
||||
}
|
||||
|
||||
export const ProcessorProvider: FC<Props> = ({ children }) => {
|
||||
// The setting the user wants to have
|
||||
const [blurActivated] = useSetting(backgroundBlurSettings);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
exports[`RaisedHandIndicator > renders a smaller indicator when miniature is specified 1`] = `
|
||||
<div
|
||||
class="reactionIndicatorWidget"
|
||||
class="_reactionIndicatorWidget_abd277"
|
||||
>
|
||||
<div
|
||||
class="reaction"
|
||||
class="_reaction_abd277"
|
||||
>
|
||||
<span
|
||||
aria-label="Reaction"
|
||||
@@ -22,10 +22,10 @@ exports[`RaisedHandIndicator > renders a smaller indicator when miniature is spe
|
||||
|
||||
exports[`RaisedHandIndicator > renders an indicator when a hand has been raised 1`] = `
|
||||
<div
|
||||
class="reactionIndicatorWidget reactionIndicatorWidgetLarge"
|
||||
class="_reactionIndicatorWidget_abd277 _reactionIndicatorWidgetLarge_abd277"
|
||||
>
|
||||
<div
|
||||
class="reaction reactionLarge"
|
||||
class="_reaction_abd277 _reactionLarge_abd277"
|
||||
>
|
||||
<span
|
||||
aria-label="Reaction"
|
||||
@@ -42,10 +42,10 @@ exports[`RaisedHandIndicator > renders an indicator when a hand has been raised
|
||||
|
||||
exports[`RaisedHandIndicator > renders an indicator when a hand has been raised with the expected time 1`] = `
|
||||
<div
|
||||
class="reactionIndicatorWidget reactionIndicatorWidgetLarge"
|
||||
class="_reactionIndicatorWidget_abd277 _reactionIndicatorWidgetLarge_abd277"
|
||||
>
|
||||
<div
|
||||
class="reaction reactionLarge"
|
||||
class="_reaction_abd277 _reactionLarge_abd277"
|
||||
>
|
||||
<span
|
||||
aria-label="Reaction"
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
useAppBarSubtitle,
|
||||
} from "../AppBar.tsx";
|
||||
import { useBehavior } from "../useBehavior.ts";
|
||||
import { constant } from "../state/Behavior.ts";
|
||||
import { Toast } from "../Toast.tsx";
|
||||
import overlayStyles from "../Overlay.module.css";
|
||||
import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx";
|
||||
@@ -433,6 +434,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$);
|
||||
const showNameTags = useBehavior(vm.showNameTags$);
|
||||
const showRingingStatus = vm.ringingStatusLocation === "tile";
|
||||
const showOutline = useBehavior(
|
||||
model instanceof GridTileViewModel
|
||||
? model.showOutline$
|
||||
: constant(false),
|
||||
);
|
||||
|
||||
return model instanceof GridTileViewModel ? (
|
||||
<GridTile
|
||||
@@ -446,6 +452,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
showSpeakingIndicators={showSpeakingIndicators}
|
||||
showNameTags={showNameTags}
|
||||
showRingingStatus={showRingingStatus}
|
||||
showOutline={showOutline}
|
||||
focusable={!contentObscured}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -145,8 +145,7 @@ describe("LobbyView", () => {
|
||||
.querySelector("path")!
|
||||
.getAttribute("d");
|
||||
const primaryButtonSvgPath = container
|
||||
.querySelector(".primaryButton")
|
||||
?.querySelector("path")
|
||||
.querySelector("path")
|
||||
?.getAttribute("d");
|
||||
expect(primaryButtonSvgPath).toBe(expectedSvgPath);
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -169,8 +168,7 @@ describe("LobbyView", () => {
|
||||
.querySelector("path")!
|
||||
.getAttribute("d");
|
||||
const primaryButtonSvgPath = container
|
||||
.querySelector(".primaryButton")
|
||||
?.querySelector("path")
|
||||
.querySelector("path")
|
||||
?.getAttribute("d");
|
||||
expect(primaryButtonSvgPath).toBe(expectedSvgPath);
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -95,20 +95,20 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -161,17 +161,17 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -253,20 +253,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' er
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -318,17 +318,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' er
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -410,20 +410,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed'
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -475,17 +475,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed'
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreachable' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -567,20 +567,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreac
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -632,17 +632,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreac
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFound' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -724,20 +724,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFo
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -789,17 +789,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFo
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -881,20 +881,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' err
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -946,17 +946,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' err
|
||||
exports[`LiveKit ConnectionError variants > should link to troubleshoot guide when timeout error 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1038,20 +1038,20 @@ exports[`LiveKit ConnectionError variants > should link to troubleshoot guide wh
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1103,17 +1103,17 @@ exports[`LiveKit ConnectionError variants > should link to troubleshoot guide wh
|
||||
exports[`should have a close button in widget mode 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1195,20 +1195,20 @@ exports[`should have a close button in widget mode 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1257,17 +1257,17 @@ exports[`should have a close button in widget mode 1`] = `
|
||||
exports[`should render the error page with link back to home 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1349,20 +1349,20 @@ exports[`should render the error page with link back to home 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1411,17 +1411,17 @@ exports[`should render the error page with link back to home 1`] = `
|
||||
exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1503,20 +1503,20 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1565,17 +1565,17 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||
exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1657,20 +1657,20 @@ exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1723,17 +1723,17 @@ exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
exports[`should report correct error for 'Homeserver does not support Matrix 2.…' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1815,20 +1815,20 @@ exports[`should report correct error for 'Homeserver does not support Matrix 2.
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1877,17 +1877,17 @@ exports[`should report correct error for 'Homeserver does not support Matrix 2.
|
||||
exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1969,20 +1969,20 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -2026,17 +2026,17 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
exports[`should report correct error for 'Insufficient capacity' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -2118,20 +2118,20 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
exports[`InCallView > rendering > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="inRoom"
|
||||
class="_inRoom_4e7ff8"
|
||||
>
|
||||
<header
|
||||
class="header header"
|
||||
class="_header_e4b327 _header_4e7ff8"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<div
|
||||
class="roomHeaderInfo"
|
||||
class="_roomHeaderInfo_e4b327"
|
||||
data-size="lg"
|
||||
>
|
||||
<span
|
||||
aria-label=""
|
||||
class="_avatar_va14e_8 roomAvatar _avatar-imageless_va14e_55"
|
||||
class="_avatar_va14e_8 _roomAvatar_e4b327 _avatar-imageless_va14e_55"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 56px;"
|
||||
/>
|
||||
<div
|
||||
class="nameLine"
|
||||
class="_nameLine_e4b327"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
@@ -35,7 +35,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_0_"
|
||||
class="lock"
|
||||
class="_lock_edc97d"
|
||||
data-encrypted="false"
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
@@ -50,7 +50,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="participantsLine"
|
||||
class="_participantsLine_e4b327"
|
||||
>
|
||||
<svg
|
||||
aria-label="Participants"
|
||||
@@ -80,21 +80,21 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="scrollingGrid grid"
|
||||
class="_scrollingGrid_4e7ff8 _grid_b0d1cd"
|
||||
>
|
||||
<div
|
||||
class="layer"
|
||||
class="_layer_b00c5f"
|
||||
>
|
||||
<div
|
||||
class="container slot"
|
||||
class="_container_b00c5f _slot_b0d1cd"
|
||||
data-id="1"
|
||||
>
|
||||
<div
|
||||
class="slot local slot"
|
||||
class="_slot_b00c5f _local_b00c5f _slot_b0d1cd"
|
||||
data-block-alignment="start"
|
||||
data-id="0"
|
||||
data-inline-alignment="end"
|
||||
@@ -103,22 +103,22 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="fixedGrid grid"
|
||||
class="_fixedGrid_4e7ff8 _grid_b0d1cd"
|
||||
style="inset-block-start: NaNpx;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="bg animate"
|
||||
class="_bg_2f5303 _animate_2f5303"
|
||||
data-state="closed"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="overlay"
|
||||
class="_overlay_eb6724"
|
||||
data-show="false"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_eb6724"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -157,22 +157,22 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
Back to Speaker Mode
|
||||
</button>
|
||||
<div
|
||||
class="spacer"
|
||||
class="_spacer_eb6724"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_8084b5"
|
||||
/>
|
||||
<div
|
||||
class="footer"
|
||||
class="_footer_20b7b4"
|
||||
data-testid="footer-container"
|
||||
>
|
||||
<div
|
||||
class="settingsLogoContainer"
|
||||
class="_settingsLogoContainer_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_8_"
|
||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
||||
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||
data-kind="secondary"
|
||||
data-testid="settings-bottom-left"
|
||||
role="button"
|
||||
@@ -198,7 +198,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="logo"
|
||||
class="_logo_20b7b4"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -303,11 +303,11 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="buttons"
|
||||
class="_buttons_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_d_"
|
||||
class="_button_1nw83_8 settingsOnlyShowNarrow _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _settingsOnlyShowNarrow_20b7b4 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
data-testid="settings-bottom-center"
|
||||
@@ -382,7 +382,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby="_r_s_"
|
||||
class="_button_1nw83_8 raiseHand _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _raiseHand_20b7b4 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
@@ -405,7 +405,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="_r_14_"
|
||||
class="_button_1nw83_8 endCall _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
class="_button_1nw83_8 _endCall_204dcb _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-testid="incall_leave"
|
||||
@@ -428,7 +428,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
<fieldset
|
||||
aria-label="Layout"
|
||||
class="_toggle_13rnk_9 layout"
|
||||
class="_toggle_13rnk_9 _layout_20b7b4"
|
||||
data-size="lg"
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="bar"
|
||||
class="_bar_221541"
|
||||
>
|
||||
<header>
|
||||
<button
|
||||
aria-labelledby="_r_36_"
|
||||
class="_icon-button_1215g_8 primaryButton"
|
||||
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -33,18 +33,18 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="secondaryButton"
|
||||
class="_secondaryButton_221541"
|
||||
/>
|
||||
</header>
|
||||
</div>
|
||||
<div
|
||||
class="inRoom"
|
||||
class="_inRoom_4e7ff8"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_f9ee84"
|
||||
>
|
||||
<div
|
||||
class="preview"
|
||||
class="_preview_dd2178"
|
||||
>
|
||||
<video
|
||||
disablepictureinpicture=""
|
||||
@@ -52,7 +52,7 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
tabindex="-1"
|
||||
/>
|
||||
<div
|
||||
class="avatarContainer"
|
||||
class="_avatarContainer_dd2178"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
@@ -68,11 +68,11 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="buttonBar"
|
||||
class="_buttonBar_dd2178"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
class="_button_1nw83_8 join wait"
|
||||
class="_button_1nw83_8 _join_f9ee84 _wait_f9ee84"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
data-testid="lobby_joinCall"
|
||||
@@ -94,15 +94,15 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="footer"
|
||||
class="_footer_20b7b4"
|
||||
data-testid="footer-container"
|
||||
>
|
||||
<div
|
||||
class="settingsLogoContainer"
|
||||
class="_settingsLogoContainer_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_3c_"
|
||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
||||
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||
data-kind="secondary"
|
||||
data-testid="settings-bottom-left"
|
||||
role="button"
|
||||
@@ -129,11 +129,11 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="buttons"
|
||||
class="_buttons_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_3h_"
|
||||
class="_button_1nw83_8 settingsOnlyShowNarrow _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _settingsOnlyShowNarrow_20b7b4 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
data-testid="settings-bottom-center"
|
||||
@@ -205,7 +205,7 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="_r_40_"
|
||||
class="_button_1nw83_8 endCall _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
class="_button_1nw83_8 _endCall_204dcb _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-testid="incall_leave"
|
||||
@@ -234,12 +234,12 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
||||
exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="bar"
|
||||
class="_bar_221541"
|
||||
>
|
||||
<header>
|
||||
<button
|
||||
aria-labelledby="_r_4a_"
|
||||
class="_icon-button_1215g_8 primaryButton"
|
||||
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -264,18 +264,18 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="secondaryButton"
|
||||
class="_secondaryButton_221541"
|
||||
/>
|
||||
</header>
|
||||
</div>
|
||||
<div
|
||||
class="inRoom"
|
||||
class="_inRoom_4e7ff8"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_f9ee84"
|
||||
>
|
||||
<div
|
||||
class="preview"
|
||||
class="_preview_dd2178"
|
||||
>
|
||||
<video
|
||||
disablepictureinpicture=""
|
||||
@@ -283,7 +283,7 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
tabindex="-1"
|
||||
/>
|
||||
<div
|
||||
class="avatarContainer"
|
||||
class="_avatarContainer_dd2178"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
@@ -299,11 +299,11 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="buttonBar"
|
||||
class="_buttonBar_dd2178"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
class="_button_1nw83_8 join wait"
|
||||
class="_button_1nw83_8 _join_f9ee84 _wait_f9ee84"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
data-testid="lobby_joinCall"
|
||||
@@ -325,15 +325,15 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="footer"
|
||||
class="_footer_20b7b4"
|
||||
data-testid="footer-container"
|
||||
>
|
||||
<div
|
||||
class="settingsLogoContainer"
|
||||
class="_settingsLogoContainer_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_4g_"
|
||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
||||
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||
data-kind="secondary"
|
||||
data-testid="settings-bottom-left"
|
||||
role="button"
|
||||
@@ -360,11 +360,11 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="buttons"
|
||||
class="_buttons_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_4l_"
|
||||
class="_button_1nw83_8 settingsOnlyShowNarrow _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _settingsOnlyShowNarrow_20b7b4 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
data-testid="settings-bottom-center"
|
||||
@@ -436,7 +436,7 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="_r_54_"
|
||||
class="_button_1nw83_8 endCall _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
class="_button_1nw83_8 _endCall_204dcb _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-testid="incall_leave"
|
||||
@@ -465,21 +465,21 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||
exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="inRoom"
|
||||
class="_inRoom_4e7ff8"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<div
|
||||
class="roomHeaderInfo"
|
||||
class="_roomHeaderInfo_e4b327"
|
||||
data-size="lg"
|
||||
>
|
||||
<span
|
||||
aria-label="!room:example.org"
|
||||
class="_avatar_va14e_8 roomAvatar _avatar-imageless_va14e_55"
|
||||
class="_avatar_va14e_8 _roomAvatar_e4b327 _avatar-imageless_va14e_55"
|
||||
data-color="3"
|
||||
data-type="round"
|
||||
role="img"
|
||||
@@ -488,7 +488,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
T
|
||||
</span>
|
||||
<div
|
||||
class="nameLine"
|
||||
class="_nameLine_e4b327"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
@@ -501,7 +501,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_0_"
|
||||
class="lock"
|
||||
class="_lock_edc97d"
|
||||
data-encrypted="false"
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
@@ -516,7 +516,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="participantsLine"
|
||||
class="_participantsLine_e4b327"
|
||||
>
|
||||
<svg
|
||||
aria-label="Participants"
|
||||
@@ -546,14 +546,14 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_f9ee84"
|
||||
>
|
||||
<div
|
||||
class="preview"
|
||||
class="_preview_dd2178"
|
||||
>
|
||||
<video
|
||||
disablepictureinpicture=""
|
||||
@@ -561,7 +561,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
tabindex="-1"
|
||||
/>
|
||||
<div
|
||||
class="avatarContainer"
|
||||
class="_avatarContainer_dd2178"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
@@ -577,10 +577,10 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="buttonBar"
|
||||
class="_buttonBar_dd2178"
|
||||
>
|
||||
<button
|
||||
class="_button_1nw83_8 join"
|
||||
class="_button_1nw83_8 _join_f9ee84"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-testid="lobby_joinCall"
|
||||
@@ -602,15 +602,15 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="footer"
|
||||
class="_footer_20b7b4"
|
||||
data-testid="footer-container"
|
||||
>
|
||||
<div
|
||||
class="settingsLogoContainer"
|
||||
class="_settingsLogoContainer_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_6_"
|
||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
||||
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||
data-kind="secondary"
|
||||
data-testid="settings-bottom-left"
|
||||
role="button"
|
||||
@@ -636,7 +636,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="logo"
|
||||
class="_logo_20b7b4"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -741,11 +741,11 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="buttons"
|
||||
class="_buttons_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_b_"
|
||||
class="_button_1nw83_8 settingsOnlyShowNarrow _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _settingsOnlyShowNarrow_20b7b4 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
data-testid="settings-bottom-center"
|
||||
@@ -817,7 +817,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="_r_q_"
|
||||
class="_button_1nw83_8 endCall _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
class="_button_1nw83_8 _endCall_204dcb _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-testid="incall_leave"
|
||||
|
||||
@@ -18,10 +18,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
Device ID: DEVICE123
|
||||
</p>
|
||||
<div
|
||||
class="fieldRow"
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
<div
|
||||
class="field inputField"
|
||||
class="_field_1bd8c0 _inputField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_1_"
|
||||
@@ -38,10 +38,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="fieldRow"
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
<div
|
||||
class="field checkboxField"
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_2_"
|
||||
@@ -52,7 +52,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
for="debugTileLayout"
|
||||
>
|
||||
<div
|
||||
class="checkbox"
|
||||
class="_checkbox_1bd8c0"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
@@ -75,10 +75,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="fieldRow"
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
<div
|
||||
class="field checkboxField"
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_3_"
|
||||
@@ -89,7 +89,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
for="showConnectionStats"
|
||||
>
|
||||
<div
|
||||
class="checkbox"
|
||||
class="_checkbox_1bd8c0"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
@@ -112,10 +112,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="fieldRow"
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
<div
|
||||
class="field checkboxField"
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_4_"
|
||||
@@ -126,7 +126,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
for="muteAllAudio"
|
||||
>
|
||||
<div
|
||||
class="checkbox"
|
||||
class="_checkbox_1bd8c0"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
@@ -150,10 +150,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="fieldRow"
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
<div
|
||||
class="field checkboxField"
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_5_"
|
||||
@@ -164,7 +164,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
for="alwaysShowIphoneEarpiece"
|
||||
>
|
||||
<div
|
||||
class="checkbox"
|
||||
class="_checkbox_1bd8c0"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
@@ -187,10 +187,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="fieldRow"
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
<div
|
||||
class="field checkboxField"
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_6_"
|
||||
@@ -201,7 +201,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
for="enableLivekitExtendedLogs"
|
||||
>
|
||||
<div
|
||||
class="checkbox"
|
||||
class="_checkbox_1bd8c0"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
@@ -274,7 +274,6 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_9_"
|
||||
name="_r_0_"
|
||||
@@ -315,6 +314,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_b_"
|
||||
name="_r_0_"
|
||||
@@ -386,7 +386,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="livekit_room_box"
|
||||
class="_livekit_room_box_2ddec4"
|
||||
>
|
||||
<h4>
|
||||
LiveKit SFU: wss://local-sfu.example.org
|
||||
@@ -427,7 +427,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
<ul />
|
||||
</div>
|
||||
<div
|
||||
class="livekit_room_box"
|
||||
class="_livekit_room_box_2ddec4"
|
||||
>
|
||||
<h4>
|
||||
LiveKit SFU: wss://remote-sfu.example.org
|
||||
|
||||
@@ -137,7 +137,7 @@ export const enableExtendedLivekitLogs = new Setting<boolean>(
|
||||
|
||||
export const matrixRTCMode = new Setting<MatrixRTCMode>(
|
||||
"matrix-rtc-mode",
|
||||
MatrixRTCMode.Legacy,
|
||||
MatrixRTCMode.Compatibility,
|
||||
);
|
||||
|
||||
export const customLivekitUrl = new Setting<string | null>(
|
||||
|
||||
@@ -253,7 +253,7 @@ describe("Test mappings", () => {
|
||||
});
|
||||
|
||||
describe("Test select a device", () => {
|
||||
it(`Switch to correct device `, () => {
|
||||
it(`Switch to correct device`, () => {
|
||||
withTestScheduler(({ cold, schedule, expectObservable, flush }) => {
|
||||
const controlledAudioOutput = new AndroidControlledAudioOutput(
|
||||
cold("a", { a: FULL_DEVICE_LIST }),
|
||||
|
||||
@@ -22,6 +22,7 @@ import { SyncState } from "matrix-js-sdk";
|
||||
import {
|
||||
ConnectionState,
|
||||
type LocalTrackPublication,
|
||||
type Participant,
|
||||
type RemoteParticipant,
|
||||
} from "livekit-client";
|
||||
import * as ComponentsCore from "@livekit/components-core";
|
||||
@@ -1031,6 +1032,10 @@ describe.each([
|
||||
a: [localRtcMember],
|
||||
b: [localRtcMember, aliceRtcMember],
|
||||
}),
|
||||
videoEnabled: new Map<Participant, Behavior<boolean>>([
|
||||
[localParticipant, constant(true)],
|
||||
[aliceParticipant, constant(true)],
|
||||
]),
|
||||
},
|
||||
(vm) => {
|
||||
schedule(modeInputMarbles, {
|
||||
@@ -1060,6 +1065,33 @@ describe.each([
|
||||
});
|
||||
});
|
||||
|
||||
test("expanded spotlight layout hides PiP tile in one-on-one voice call", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
withCallViewModel(
|
||||
{
|
||||
remoteParticipants$: constant([aliceParticipant]),
|
||||
roomMembers: [local, alice],
|
||||
rtcMembers$: constant([localRtcMember, aliceRtcMember]),
|
||||
videoEnabled: new Map<Participant, Behavior<boolean>>([
|
||||
[localParticipant, constant(false)],
|
||||
[aliceParticipant, constant(false)],
|
||||
]),
|
||||
windowSize$: constant({ width: 700, height: 380 }), // Mobile phone in landscape
|
||||
},
|
||||
(vm) => {
|
||||
// Layout should show remote tile only
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
|
||||
a: {
|
||||
type: "spotlight-expanded",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("spotlight remembers whether it's expanded", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
// Start in spotlight mode, then switch to grid and back to spotlight a
|
||||
@@ -1096,7 +1128,7 @@ describe.each([
|
||||
b: {
|
||||
type: "spotlight-expanded",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: `${localId}:0`,
|
||||
pip: undefined,
|
||||
},
|
||||
c: {
|
||||
type: "grid",
|
||||
|
||||
@@ -155,6 +155,7 @@ import {
|
||||
createRingingMedia,
|
||||
type RingingMediaViewModel,
|
||||
} from "../media/RingingMediaViewModel.ts";
|
||||
import { type GridTileViewModel } from "../TileViewModel.ts";
|
||||
|
||||
const logger = rootLogger.getChild("[CallViewModel]");
|
||||
//TODO
|
||||
@@ -941,8 +942,8 @@ export function createCallViewModel$(
|
||||
);
|
||||
|
||||
/**
|
||||
* Local user media suitable for displaying in a PiP (undefined if not found
|
||||
* or if user prefers to not see themselves).
|
||||
* Local user media suitable for displaying in a PiP (undefined if not found,
|
||||
* video is muted, or if user prefers to not see themselves).
|
||||
*/
|
||||
const localUserMediaForPip$ = scope.behavior<
|
||||
LocalUserMediaViewModel | undefined
|
||||
@@ -954,8 +955,10 @@ export function createCallViewModel$(
|
||||
m.type === "user" && m.local,
|
||||
);
|
||||
if (!localUserMedia) return of(undefined);
|
||||
return localUserMedia.alwaysShow$.pipe(
|
||||
map((alwaysShow) => (alwaysShow ? localUserMedia : undefined)),
|
||||
return combineLatest(
|
||||
[localUserMedia.videoEnabled$, localUserMedia.alwaysShow$],
|
||||
(videoEnabled, alwaysShow) =>
|
||||
videoEnabled && alwaysShow ? localUserMedia : undefined,
|
||||
);
|
||||
}),
|
||||
),
|
||||
@@ -1424,7 +1427,7 @@ export function createCallViewModel$(
|
||||
windowMode$.pipe(
|
||||
switchMap((mode) => {
|
||||
// In small windows the header would be too obstructive
|
||||
if (mode === "pip" || mode === "flat") return of(false);
|
||||
if (mode === "pip") return of(false);
|
||||
// In edge-to-edge layouts, couple the visibility of the header
|
||||
// to that of the footer
|
||||
return edgeToEdge$.pipe(
|
||||
@@ -1482,6 +1485,7 @@ export function createCallViewModel$(
|
||||
({ tiles: prevTiles }, [media, visibleTiles]) => {
|
||||
let layout: Layout;
|
||||
let newTiles: TileStore;
|
||||
let pip: GridTileViewModel | undefined;
|
||||
switch (media.type) {
|
||||
case "grid":
|
||||
case "spotlight-landscape":
|
||||
@@ -1507,6 +1511,7 @@ export function createCallViewModel$(
|
||||
landscapePipAlignment$,
|
||||
prevTiles,
|
||||
);
|
||||
pip = layout.pip;
|
||||
break;
|
||||
case "one-on-one-portrait":
|
||||
[layout, newTiles] = oneOnOnePortraitLayout(
|
||||
@@ -1515,12 +1520,17 @@ export function createCallViewModel$(
|
||||
portraitPipAlignment$,
|
||||
prevTiles,
|
||||
);
|
||||
pip = layout.pip;
|
||||
break;
|
||||
case "pip":
|
||||
[layout, newTiles] = pipLayout(media, prevTiles);
|
||||
break;
|
||||
}
|
||||
|
||||
for (const tile of newTiles.gridTiles) {
|
||||
tile.setShowOutline(tile === pip);
|
||||
}
|
||||
|
||||
return { layout, tiles: newTiles };
|
||||
},
|
||||
{ layout: null, tiles: TileStore.empty() },
|
||||
|
||||
@@ -170,7 +170,7 @@ export const createLocalMembership$ = ({
|
||||
logger: parentLogger,
|
||||
muteStates,
|
||||
matrixRTCSession,
|
||||
roomId: roomId,
|
||||
roomId,
|
||||
}: Props): {
|
||||
/**
|
||||
* This request to start audio and video tracks.
|
||||
|
||||
@@ -102,7 +102,7 @@ export class RtcTransportAutoDiscovery {
|
||||
const transportList = await doNetworkOperationWithRetry(async () =>
|
||||
client._unstable_getRTCTransports(),
|
||||
);
|
||||
const first = transportList.filter(isLivekitTransportConfig)[0];
|
||||
const first = transportList.find(isLivekitTransportConfig);
|
||||
if (first) {
|
||||
return first;
|
||||
} else {
|
||||
|
||||
162
src/state/IOSControlledAudioOutput.test.ts
Normal file
162
src/state/IOSControlledAudioOutput.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
Copyright 2026 Element Corp.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { type Observable, of } from "rxjs";
|
||||
|
||||
import { ObservableScope } from "./ObservableScope";
|
||||
import { constant } from "./Behavior";
|
||||
import { type SelectedAudioOutputDevice } from "./MediaDevices";
|
||||
import {
|
||||
availableOutputDevices$,
|
||||
type Controls,
|
||||
type OutputDevice,
|
||||
outputDevice$,
|
||||
} from "../controls";
|
||||
import {
|
||||
EARPIECE_CONFIG_ID,
|
||||
IOSControlledAudioOutput,
|
||||
} from "./IOSControlledAudioOutput";
|
||||
|
||||
// `vi.mock` calls are hoisted above all imports, so the static imports below
|
||||
// already see these mocks. Force the iOS platform so that the virtual earpiece
|
||||
// is available, and stub the livekit device observer (only subscribed for its
|
||||
// side effects).
|
||||
vi.mock("../Platform", () => ({ platform: "ios" }));
|
||||
vi.mock("@livekit/components-core", () => ({
|
||||
createMediaDeviceObserver: (): Observable<MediaDeviceInfo[]> => of([]),
|
||||
}));
|
||||
|
||||
// On iOS the host reports a single device for the current route. When output is
|
||||
// on the loudspeaker it is flagged `forEarpiece`, which makes the controller
|
||||
// expose a virtual earpiece device.
|
||||
const SPEAKER: OutputDevice = {
|
||||
id: "speaker",
|
||||
name: "Speaker",
|
||||
isSpeaker: true,
|
||||
forEarpiece: true,
|
||||
};
|
||||
|
||||
// A connected headset (e.g. Bluetooth) is reported as a plain named device,
|
||||
// with neither the speaker nor earpiece flag set.
|
||||
const HEADSET: OutputDevice = {
|
||||
id: "bt",
|
||||
name: "AirPods",
|
||||
};
|
||||
|
||||
let testScope: ObservableScope;
|
||||
|
||||
beforeEach(() => {
|
||||
testScope = new ObservableScope();
|
||||
window.controls = {
|
||||
onAudioDeviceSelect: vi.fn(),
|
||||
onOutputDeviceSelect: vi.fn(),
|
||||
} as unknown as Controls;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testScope.end();
|
||||
});
|
||||
|
||||
/**
|
||||
* Subscribe to the controller's `selected$` and return a getter for the latest
|
||||
* emitted value.
|
||||
*/
|
||||
function latestSelection(
|
||||
output: InstanceType<typeof IOSControlledAudioOutput>,
|
||||
): () => SelectedAudioOutputDevice | undefined {
|
||||
let latest: SelectedAudioOutputDevice | undefined;
|
||||
output.selected$.subscribe((s) => {
|
||||
latest = s;
|
||||
});
|
||||
return () => latest;
|
||||
}
|
||||
|
||||
describe("Default selection", () => {
|
||||
it("defaults to the earpiece for voice (audio) calls", () => {
|
||||
const output = new IOSControlledAudioOutput(
|
||||
constant(false),
|
||||
testScope,
|
||||
"audio",
|
||||
);
|
||||
const selected = latestSelection(output);
|
||||
|
||||
availableOutputDevices$.next([SPEAKER]);
|
||||
|
||||
expect(selected()).toEqual({
|
||||
id: EARPIECE_CONFIG_ID,
|
||||
virtualEarpiece: true,
|
||||
});
|
||||
expect(window.controls.onAudioDeviceSelect).toHaveBeenLastCalledWith(
|
||||
EARPIECE_CONFIG_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to the speaker for video calls", () => {
|
||||
const output = new IOSControlledAudioOutput(
|
||||
constant(false),
|
||||
testScope,
|
||||
"video",
|
||||
);
|
||||
const selected = latestSelection(output);
|
||||
|
||||
availableOutputDevices$.next([SPEAKER]);
|
||||
|
||||
expect(selected()).toEqual({ id: SPEAKER.id, virtualEarpiece: false });
|
||||
});
|
||||
|
||||
it("keeps a headset for voice calls instead of forcing the earpiece", () => {
|
||||
const output = new IOSControlledAudioOutput(
|
||||
constant(false),
|
||||
testScope,
|
||||
"audio",
|
||||
);
|
||||
const selected = latestSelection(output);
|
||||
|
||||
// The host proposes the headset as the route (listed first), even though a
|
||||
// forEarpiece device is also present so the virtual earpiece exists.
|
||||
availableOutputDevices$.next([HEADSET, SPEAKER]);
|
||||
|
||||
expect(selected()).toEqual({ id: HEADSET.id, virtualEarpiece: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Explicit selection", () => {
|
||||
it("an explicit user selection overrides the earpiece default", () => {
|
||||
const output = new IOSControlledAudioOutput(
|
||||
constant(false),
|
||||
testScope,
|
||||
"audio",
|
||||
);
|
||||
const selected = latestSelection(output);
|
||||
|
||||
availableOutputDevices$.next([SPEAKER]);
|
||||
// Earpiece by default for a voice call...
|
||||
expect(selected()).toEqual({
|
||||
id: EARPIECE_CONFIG_ID,
|
||||
virtualEarpiece: true,
|
||||
});
|
||||
|
||||
// ...until the user explicitly picks the speaker.
|
||||
output.select(SPEAKER.id);
|
||||
expect(selected()).toEqual({ id: SPEAKER.id, virtualEarpiece: false });
|
||||
});
|
||||
|
||||
it("a host selection overrides the earpiece default", () => {
|
||||
const output = new IOSControlledAudioOutput(
|
||||
constant(false),
|
||||
testScope,
|
||||
"audio",
|
||||
);
|
||||
const selected = latestSelection(output);
|
||||
|
||||
availableOutputDevices$.next([SPEAKER]);
|
||||
outputDevice$.next(SPEAKER.id);
|
||||
|
||||
expect(selected()).toEqual({ id: SPEAKER.id, virtualEarpiece: false });
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import { combineLatest, merge, startWith, Subject, tap } from "rxjs";
|
||||
|
||||
import type { RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import {
|
||||
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
||||
outputDevice$ as controlledOutputSelection$,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
|
||||
// This hardcoded id is used in EX ios! It can only be changed in coordination with
|
||||
// the ios swift team.
|
||||
const EARPIECE_CONFIG_ID = "earpiece-id";
|
||||
export const EARPIECE_CONFIG_ID = "earpiece-id";
|
||||
|
||||
/**
|
||||
* A special implementation of audio output that allows the hosting application
|
||||
@@ -94,7 +95,7 @@ export class IOSControlledAudioOutput implements MediaDevice<
|
||||
),
|
||||
],
|
||||
(available, preferredId) => {
|
||||
const id = preferredId ?? available.keys().next().value;
|
||||
const id = preferredId ?? this.chooseDefaultId(available);
|
||||
return id === undefined
|
||||
? undefined
|
||||
: { id, virtualEarpiece: id === EARPIECE_CONFIG_ID };
|
||||
@@ -106,9 +107,41 @@ export class IOSControlledAudioOutput implements MediaDevice<
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Chooses the default output device when no explicit selection (from the user
|
||||
* or the hosting application) has been made yet.
|
||||
*
|
||||
* For voice calls (`initialIntent === "audio"`) we want to start on the
|
||||
* earpiece rather than the speaker, like a regular phone call. We only
|
||||
* override when the device that would otherwise be the default is the
|
||||
* speaker: if the host already routed to a headset (e.g. Bluetooth) — which
|
||||
* is reported as a plain named device, not "speaker"/"earpiece" — we keep it.
|
||||
* This mirrors the Android behaviour in {@link AndroidControlledAudioOutput}.
|
||||
*/
|
||||
private chooseDefaultId(
|
||||
available: Map<string, AudioOutputDeviceLabel>,
|
||||
): string | undefined {
|
||||
const firstId = available.keys().next().value;
|
||||
if (this.initialIntent === "audio") {
|
||||
const firstLabel =
|
||||
firstId !== undefined ? available.get(firstId) : undefined;
|
||||
if (firstLabel?.type === "speaker") {
|
||||
for (const [id, label] of available)
|
||||
if (label.type === "earpiece") {
|
||||
this.logger.info(
|
||||
`IOS routing: default to earpiece ${id} instead of speaker for voice call`,
|
||||
);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstId;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private readonly usingNames$: Behavior<boolean>,
|
||||
private readonly scope: ObservableScope,
|
||||
private readonly initialIntent: RTCCallIntent | undefined = undefined,
|
||||
) {
|
||||
this.selected$.subscribe((device) => {
|
||||
// Let the hosting application know which output device has been selected.
|
||||
|
||||
@@ -376,7 +376,11 @@ export class MediaDevices {
|
||||
getUrlParams().callIntent,
|
||||
window.controls,
|
||||
)
|
||||
: new IOSControlledAudioOutput(this.usingNames$, this.scope)
|
||||
: new IOSControlledAudioOutput(
|
||||
this.usingNames$,
|
||||
this.scope,
|
||||
getUrlParams().callIntent,
|
||||
)
|
||||
: new AudioOutput(this.usingNames$, this.scope);
|
||||
|
||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||
|
||||
@@ -131,9 +131,9 @@ export class TileStoreBuilder {
|
||||
private numGridEntries = 0;
|
||||
// A sparse array of grid entries which should be kept in the same spots as
|
||||
// which they appeared in the previous grid
|
||||
private readonly stationaryGridEntries: GridTileData[] = new Array(
|
||||
this.prevGrid.length,
|
||||
);
|
||||
private readonly stationaryGridEntries: GridTileData[] = Array.from({
|
||||
length: this.prevGrid.length,
|
||||
});
|
||||
// Grid entries which should now enter the visible section of the grid
|
||||
private readonly visibleGridEntries: GridTileData[] = [];
|
||||
// Grid entries which should now enter the invisible section of the grid
|
||||
|
||||
@@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { type Behavior } from "./Behavior";
|
||||
import { type MediaViewModel } from "./media/MediaViewModel";
|
||||
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
||||
@@ -17,12 +19,18 @@ function createId(): string {
|
||||
|
||||
export class GridTileViewModel {
|
||||
public readonly id = createId();
|
||||
private readonly _showOutline$ = new BehaviorSubject(false);
|
||||
public readonly showOutline$: Behavior<boolean> = this._showOutline$;
|
||||
|
||||
public constructor(
|
||||
public readonly media$: Behavior<
|
||||
UserMediaViewModel | RingingMediaViewModel
|
||||
>,
|
||||
) {}
|
||||
|
||||
public setShowOutline(value: boolean): void {
|
||||
this._showOutline$.next(value);
|
||||
}
|
||||
}
|
||||
|
||||
export class SpotlightTileViewModel {
|
||||
|
||||
@@ -66,6 +66,11 @@ borders don't support gradients */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tile.outline {
|
||||
outline: var(--cpd-border-width-1) solid
|
||||
var(--cpd-color-border-interactive-secondary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.tile:hover {
|
||||
outline: var(--cpd-border-width-2) solid
|
||||
|
||||
@@ -78,6 +78,7 @@ test("GridTile is accessible", async () => {
|
||||
showSpeakingIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
showOutline
|
||||
focusable
|
||||
/>
|
||||
</ReactionsSenderProvider>,
|
||||
@@ -110,6 +111,7 @@ test("GridTile displays ringing media", async () => {
|
||||
showSpeakingIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
showOutline
|
||||
focusable
|
||||
/>
|
||||
</ReactionsSenderProvider>,
|
||||
|
||||
@@ -398,6 +398,7 @@ interface GridTileProps {
|
||||
showSpeakingIndicators: boolean;
|
||||
showNameTags: boolean;
|
||||
showRingingStatus: boolean;
|
||||
showOutline: boolean;
|
||||
focusable: boolean;
|
||||
}
|
||||
|
||||
@@ -406,7 +407,9 @@ export const GridTile: FC<GridTileProps> = ({
|
||||
vm,
|
||||
showSpeakingIndicators,
|
||||
showRingingStatus,
|
||||
showOutline,
|
||||
onOpenProfile,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -423,6 +426,7 @@ export const GridTile: FC<GridTileProps> = ({
|
||||
displayName={displayName}
|
||||
mxcAvatarUrl={mxcAvatarUrl}
|
||||
showStatus={showRingingStatus}
|
||||
className={classNames(className, { [styles.outline]: showOutline })}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -435,6 +439,7 @@ export const GridTile: FC<GridTileProps> = ({
|
||||
onOpenProfile={onOpenProfile}
|
||||
displayName={displayName}
|
||||
mxcAvatarUrl={mxcAvatarUrl}
|
||||
className={classNames(className, { [styles.outline]: showOutline })}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -446,6 +451,7 @@ export const GridTile: FC<GridTileProps> = ({
|
||||
showSpeakingIndicators={showSpeakingIndicators}
|
||||
displayName={displayName}
|
||||
mxcAvatarUrl={mxcAvatarUrl}
|
||||
className={classNames(className, { [styles.outline]: showOutline })}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
type TrackReference,
|
||||
type TrackReferencePlaceholder,
|
||||
} from "@livekit/components-core";
|
||||
import { LocalTrackPublication, Track } from "livekit-client";
|
||||
import { TrackInfo } from "@livekit/protocol";
|
||||
import { type LocalTrackPublication, Track } from "livekit-client";
|
||||
import { type ComponentProps } from "react";
|
||||
|
||||
import { MediaView } from "./MediaView";
|
||||
@@ -28,10 +27,7 @@ describe("MediaView", () => {
|
||||
};
|
||||
const trackReference: TrackReference = {
|
||||
...trackReferencePlaceholder,
|
||||
publication: new LocalTrackPublication(
|
||||
Track.Kind.Video,
|
||||
new TrackInfo({ sid: "id", name: "name" }),
|
||||
),
|
||||
publication: {} as Partial<LocalTrackPublication> as LocalTrackPublication,
|
||||
};
|
||||
|
||||
const baseProps: ComponentProps<typeof MediaView> = {
|
||||
|
||||
@@ -6,22 +6,22 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { TileAvatar } from "./TileAvatar";
|
||||
|
||||
describe("TileAvatar", () => {
|
||||
it("should show loading spinner when loading", () => {
|
||||
const { container } = render(
|
||||
render(
|
||||
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={true} />,
|
||||
);
|
||||
expect(container.querySelector(".loading")).toBeInTheDocument();
|
||||
screen.getByLabelText("Loading…");
|
||||
});
|
||||
|
||||
it("should not show loading spinner when not loading", () => {
|
||||
const { container } = render(
|
||||
render(
|
||||
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={false} />,
|
||||
);
|
||||
expect(container.querySelector(".loading")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Loading…")).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { type FC } from "react";
|
||||
import { InlineSpinner } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./TileAvatar.module.css";
|
||||
import { Avatar, type Props as AvatarProps } from "../Avatar";
|
||||
@@ -17,11 +18,12 @@ interface Props extends AvatarProps {
|
||||
}
|
||||
|
||||
export const TileAvatar: FC<Props> = ({ size, loading, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{loading && (
|
||||
<div className={styles.loading}>
|
||||
<InlineSpinner size={size / 3} />
|
||||
<InlineSpinner size={size / 3} aria-label={t("common.loading")} />
|
||||
</div>
|
||||
)}
|
||||
<Avatar size={size} {...props} />
|
||||
|
||||
58
src/utils/redact.test.ts
Normal file
58
src/utils/redact.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2026 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import { redact } from "./redact";
|
||||
|
||||
test("empty object", () => {
|
||||
expect(redact({})).toEqual({});
|
||||
});
|
||||
|
||||
test("no keys", () => {
|
||||
expect(redact({ foo: "bar" })).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
test("redact one key", () => {
|
||||
expect(redact({ foo: "bar" }, "foo")).toEqual({ foo: "<redacted>" });
|
||||
});
|
||||
|
||||
test("redact two keys", () => {
|
||||
expect(redact({ foo: "bar", bar: "foo" }, "foo", "bar")).toEqual({
|
||||
foo: "<redacted>",
|
||||
bar: "<redacted>",
|
||||
});
|
||||
});
|
||||
|
||||
test("no redaction of unrelated keys", () => {
|
||||
expect(redact({ foo: "bar", bar: "foo" }, "foo")).toEqual({
|
||||
foo: "<redacted>",
|
||||
bar: "foo",
|
||||
});
|
||||
});
|
||||
|
||||
test("no redaction of missing keys", () => {
|
||||
expect(
|
||||
redact({ foo: "bar" } as { foo: string; bar: string | undefined }, "bar"),
|
||||
).toEqual({
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
test("no redaction of null values", () => {
|
||||
expect(redact({ foo: "bar", bar: null }, "bar")).toEqual({
|
||||
foo: "bar",
|
||||
bar: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("no redaction of undefined values", () => {
|
||||
expect(redact({ foo: "bar", bar: undefined }, "bar")).toEqual({
|
||||
foo: "bar",
|
||||
bar: undefined,
|
||||
});
|
||||
});
|
||||
25
src/utils/redact.ts
Normal file
25
src/utils/redact.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2026 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Redacts properties in the supplied object by replacing them with
|
||||
* a constant value.
|
||||
* @param obj Object in which to perform redaction
|
||||
* @param keys Keys to be redacted in the object
|
||||
* @returns A new object with the specified properties redacted
|
||||
*/
|
||||
export function redact<T extends object>(
|
||||
obj: T,
|
||||
...keys: (keyof T)[]
|
||||
): Record<keyof T, unknown> {
|
||||
const result: Record<keyof T, unknown> = { ...obj };
|
||||
for (const key of keys)
|
||||
if (key in result && result[key] != null) {
|
||||
result[key] = "<redacted>";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -128,8 +128,8 @@ export function getBasicRTCSession(
|
||||
|
||||
/**
|
||||
* Construct a basic CallViewModel to test components that make use of it.
|
||||
* @param members
|
||||
* @param initialRtcMemberships
|
||||
* @param members - Room members to include in the call.
|
||||
* @param initialRtcMemberships - RTC memberships to start with.
|
||||
* @returns
|
||||
*/
|
||||
export function getBasicCallViewModelEnvironment(
|
||||
|
||||
@@ -5,7 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import "global-jsdom/register";
|
||||
import "@formatjs/intl-durationformat/polyfill.js";
|
||||
import "@formatjs/intl-segmenter/polyfill";
|
||||
import i18n from "i18next";
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"livekit-client/dist/src/proto/livekit_models_pb": [
|
||||
"./node_modules/@livekit/protocol/src/gen/livekit_models_pb.d.ts"
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Enable the following options later.
|
||||
// "forceConsistentCasingInFileNames": true,
|
||||
@@ -48,8 +48,6 @@
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
// "noUncheckedIndexedAccess": true,
|
||||
// "noUnusedParameters": true,
|
||||
|
||||
"plugins": [{ "name": "typescript-eslint-language-service" }]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
|
||||
@@ -18,12 +18,7 @@ export default defineConfig((configEnv) =>
|
||||
extends: true,
|
||||
test: {
|
||||
name: "unit",
|
||||
css: {
|
||||
include: /.+/,
|
||||
modules: {
|
||||
classNameStrategy: "non-scoped",
|
||||
},
|
||||
},
|
||||
css: { include: /.+/ },
|
||||
setupFiles: ["src/vitest.setup.ts"],
|
||||
environment: "jsdom",
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
|
||||
Reference in New Issue
Block a user