mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Compare commits
111 Commits
remote-mem
...
remove-sto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0afefa4889 | ||
|
|
5968554e4c | ||
|
|
8d826b2f31 | ||
|
|
32e30dd4f5 | ||
|
|
bb06905aa2 | ||
|
|
2684fa1d62 | ||
|
|
de75a69cad | ||
|
|
f2c42c314c | ||
|
|
4b50a191ba | ||
|
|
11fadebb87 | ||
|
|
943ce5b853 | ||
|
|
137093350b | ||
|
|
000ee66cfe | ||
|
|
58a8b7316e | ||
|
|
1e6d3d3357 | ||
|
|
9f681b87a0 | ||
|
|
dc21233767 | ||
|
|
27315a7090 | ||
|
|
bebe79d109 | ||
|
|
4ca0270115 | ||
|
|
374f56651c | ||
|
|
eefa6f843d | ||
|
|
2b84c587f2 | ||
|
|
706cddafb0 | ||
|
|
a6921e2bfb | ||
|
|
3cc396f42c | ||
|
|
59999b9b37 | ||
|
|
e68321e16c | ||
|
|
ad76969ce2 | ||
|
|
c170aad364 | ||
|
|
9e8bebc3bf | ||
|
|
d5cdbfd4d1 | ||
|
|
7840f4bccb | ||
|
|
9199dcb583 | ||
|
|
cbc4b77a4f | ||
|
|
ffb881436a | ||
|
|
9cf87efc6b | ||
|
|
b71454d63a | ||
|
|
b0feb33ec4 | ||
|
|
900d0abd27 | ||
|
|
a3d3a1e951 | ||
|
|
8fa3f33f37 | ||
|
|
360a4ff026 | ||
|
|
8c6a16f3d7 | ||
|
|
9d21581259 | ||
|
|
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 | ||
|
|
a456e47796 | ||
|
|
27abf816f5 | ||
|
|
c6188a8345 | ||
|
|
22ff0d34b7 | ||
|
|
96d1d1e1aa | ||
|
|
6f75e90fcd | ||
|
|
f78f507745 | ||
|
|
e11c04ac87 | ||
|
|
9b070052a0 | ||
|
|
bb3d2cd95c | ||
|
|
d417bd8b90 | ||
|
|
f72a593eb4 | ||
|
|
f6cd06fc01 | ||
|
|
049c432fc3 | ||
|
|
19fd7f42b1 | ||
|
|
b2f02b0ff6 | ||
|
|
b31825d05b | ||
|
|
c4fe0223ab | ||
|
|
b9d769364d | ||
|
|
164fb99ace | ||
|
|
db26db266d | ||
|
|
8c8c9eef24 | ||
|
|
0a572a9528 | ||
|
|
659da15cc2 | ||
|
|
9b2e1279bc | ||
|
|
2b05c415c1 | ||
|
|
ba1957ef52 | ||
|
|
abf9022fd9 |
116
.eslintrc.cjs
116
.eslintrc.cjs
@@ -1,116 +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"],
|
|
||||||
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: {
|
|
||||||
"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: {
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
10
.github/workflows/lint.yaml
vendored
10
.github/workflows/lint.yaml
vendored
@@ -2,7 +2,7 @@ name: Lint, format & type check
|
|||||||
on:
|
on:
|
||||||
pull_request: {}
|
pull_request: {}
|
||||||
jobs:
|
jobs:
|
||||||
prettier:
|
lint:
|
||||||
name: Lint, format & type check
|
name: Lint, format & type check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -20,12 +20,12 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
# ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present)
|
# ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present)
|
||||||
run: "pnpm install --frozen-lockfile --ignore-pnpmfile"
|
run: "pnpm install --frozen-lockfile --ignore-pnpmfile"
|
||||||
- name: Prettier
|
- name: Formatting
|
||||||
run: "pnpm run prettier:check"
|
run: "pnpm run format:check"
|
||||||
- name: i18n
|
- name: i18n
|
||||||
run: "pnpm run i18n:check"
|
run: "pnpm run i18n:check"
|
||||||
- name: ESLint
|
- name: Lint
|
||||||
run: "pnpm run lint:eslint"
|
run: "pnpm run lint:oxlint"
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: "pnpm run lint:types"
|
run: "pnpm run lint:types"
|
||||||
- name: Dead code analysis
|
- name: Dead code analysis
|
||||||
|
|||||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
# Make sure to grab the latest version of the Playwright image
|
# Make sure to grab the latest version of the Playwright image
|
||||||
# https://playwright.dev/docs/docker#pull-the-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:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|||||||
4
.github/workflows/translations-download.yaml
vendored
4
.github/workflows/translations-download.yaml
vendored
@@ -40,8 +40,8 @@ jobs:
|
|||||||
- name: Fix the owner of the downloaded files
|
- name: Fix the owner of the downloaded files
|
||||||
run: "sudo chown runner:docker -R locales"
|
run: "sudo chown runner:docker -R locales"
|
||||||
|
|
||||||
- name: Prettier
|
- name: Formatting
|
||||||
run: pnpm prettier:format
|
run: pnpm format
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
|
|||||||
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.
|
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||||
|
|||||||
6
.oxfmtrc.json
Normal file
6
.oxfmtrc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"printWidth": 80,
|
||||||
|
"sortPackageJson": false,
|
||||||
|
"ignorePatterns": ["pnpm-lock.yaml", "node_modules", "dist"]
|
||||||
|
}
|
||||||
154
.oxlintrc.json
Normal file
154
.oxlintrc.json
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"$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",
|
||||||
|
"element-call/no-top-level-logger-get-child": "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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pnpm-lock.yaml
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"feature_use_device_session_member_events": true
|
"feature_use_device_session_member_events": true
|
||||||
},
|
},
|
||||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
"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": {
|
"matrix_rtc_session": {
|
||||||
"wait_for_key_rotation_ms": 3000,
|
"wait_for_key_rotation_ms": 3000,
|
||||||
"membership_event_expiry_ms": 180000000,
|
"membership_event_expiry_ms": 180000000,
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ These parameters are relevant to both [widget](./embedded_standalone.md) and [st
|
|||||||
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
|
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
|
||||||
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
|
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
|
||||||
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
||||||
|
| `background` | One of: `solid`, `gradient` | No, defaults to `gradient` | No, defaults to `gradient` | Visual style of the page background. |
|
||||||
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
||||||
| `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. |
|
| `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. |
|
||||||
| `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. |
|
| `autoLeave` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. |
|
||||||
| `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. |
|
| `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. |
|
||||||
|
|
||||||
### Widget-only parameters
|
### Widget-only parameters
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
|
|||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
|
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
|
||||||
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.36.0" }
|
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.36.0" }
|
||||||
|
|||||||
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;
|
||||||
78
eslint/NoObservableScopeLeak.js
Normal file
78
eslint/NoObservableScopeLeak.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
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";
|
||||||
|
|
||||||
|
// These ObservableScope methods will not generally cause resource leaks even if
|
||||||
|
// called from a callback
|
||||||
|
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",
|
||||||
|
)({
|
||||||
|
name: "no-observablescope-leak",
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
"Require referenced ObservableScopes to be defined in the very same scope to avoid resource leaks.",
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
scopeLeak:
|
||||||
|
"Do not reference ObservableScopes defined in an outer scope; this may create resource leaks.",
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
Identifier(node) {
|
||||||
|
const scope = context.sourceCode.getScope(node);
|
||||||
|
if (
|
||||||
|
// 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.has(node.parent.property.name) &&
|
||||||
|
/(^s|S)cope$/.test(node.name)
|
||||||
|
) {
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rule;
|
||||||
92
eslint/NoTopLevelLoggerGetChild.js
Normal file
92
eslint/NoTopLevelLoggerGetChild.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node types that introduce a new non-module scope. A getChild() call nested
|
||||||
|
* inside any of these is considered "not at the top level".
|
||||||
|
*/
|
||||||
|
const FUNCTION_OR_CLASS_TYPES = new Set([
|
||||||
|
"FunctionDeclaration",
|
||||||
|
"FunctionExpression",
|
||||||
|
"ArrowFunctionExpression",
|
||||||
|
"ClassBody",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rule = ESLintUtils.RuleCreator(
|
||||||
|
() => "https://github.com/element-hq/element-call",
|
||||||
|
)({
|
||||||
|
name: "no-top-level-logger-get-child",
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
"Disallow calling logger.getChild() at the top level of a module." +
|
||||||
|
"`getChild` has to be called after the rageshake logger `init()`." +
|
||||||
|
"If it is called at the top level the child logger will never be setup for rageshakes.",
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
noTopLevelGetChild:
|
||||||
|
"Do not call logger.getChild() at the top level of a module; move it inside a function or class instead that gets called after rageshake logger `init()` is called.",
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
// Tracks the local binding names that refer to the logger imported from
|
||||||
|
// 'matrix-js-sdk/lib/logger', e.g. both `logger` and `rootLogger` in:
|
||||||
|
// import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
// import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
|
const loggerNames = new Set();
|
||||||
|
|
||||||
|
return {
|
||||||
|
ImportDeclaration(node) {
|
||||||
|
if (node.source.value !== "matrix-js-sdk/lib/logger") return;
|
||||||
|
for (const specifier of node.specifiers) {
|
||||||
|
if (
|
||||||
|
specifier.type === "ImportSpecifier" &&
|
||||||
|
specifier.imported.name === "logger"
|
||||||
|
) {
|
||||||
|
loggerNames.add(specifier.local.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
CallExpression(node) {
|
||||||
|
// Must be a non-computed member expression call: something.getChild(...)
|
||||||
|
if (
|
||||||
|
node.callee.type !== "MemberExpression" ||
|
||||||
|
node.callee.computed ||
|
||||||
|
node.callee.property.type !== "Identifier" ||
|
||||||
|
node.callee.property.name !== "getChild"
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// The receiver must be one of the tracked logger names.
|
||||||
|
const object = node.callee.object;
|
||||||
|
if (object.type !== "Identifier" || !loggerNames.has(object.name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Flag the call only when it is at module top level — i.e. there is no
|
||||||
|
// enclosing function or class body anywhere in the ancestor chain.
|
||||||
|
const ancestors = context.sourceCode.getAncestors(node);
|
||||||
|
const isTopLevel = !ancestors.some((a) =>
|
||||||
|
FUNCTION_OR_CLASS_TYPES.has(a.type),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTopLevel) {
|
||||||
|
context.report({
|
||||||
|
messageId: "noTopLevelGetChild",
|
||||||
|
node,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rule;
|
||||||
8
eslint/index.js
Normal file
8
eslint/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
rules: {
|
||||||
|
"copyright-header": require("./CopyrightHeader").default,
|
||||||
|
"no-observablescope-leak": require("./NoObservableScopeLeak").default,
|
||||||
|
"no-top-level-logger-get-child": require("./NoTopLevelLoggerGetChild")
|
||||||
|
.default,
|
||||||
|
},
|
||||||
|
};
|
||||||
4
eslint/package.json
Normal file
4
eslint/package.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "eslint-plugin-element-call",
|
||||||
|
"version": "0.0.0"
|
||||||
|
}
|
||||||
7
knip.ts
7
knip.ts
@@ -11,12 +11,14 @@ export default {
|
|||||||
vite: {
|
vite: {
|
||||||
config: ["vite.config.ts", "vite-embedded.config.ts", "vite-sdk.config.ts"],
|
config: ["vite.config.ts", "vite-embedded.config.ts", "vite-sdk.config.ts"],
|
||||||
},
|
},
|
||||||
entry: ["src/main.tsx", "i18next.config.ts"],
|
entry: ["src/main.tsx", "eslint/index.js", "i18next.config.ts"],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// This is deprecated, so Knip doesn't actually recognize it as a globally
|
// This is deprecated, so Knip doesn't actually recognize it as a globally
|
||||||
// installed binary. TODO We should switch to Compose v2:
|
// installed binary. TODO We should switch to Compose v2:
|
||||||
// https://docs.docker.com/compose/migrate/
|
// https://docs.docker.com/compose/migrate/
|
||||||
"docker-compose",
|
"docker-compose",
|
||||||
|
// This is a shell built-in.
|
||||||
|
"printf",
|
||||||
],
|
],
|
||||||
ignoreFiles: ["scripts/.pnpmfile.cjs"],
|
ignoreFiles: ["scripts/.pnpmfile.cjs"],
|
||||||
ignoreDependencies: [
|
ignoreDependencies: [
|
||||||
@@ -30,6 +32,9 @@ export default {
|
|||||||
// https://github.com/webpro-nl/knip/issues/766
|
// https://github.com/webpro-nl/knip/issues/766
|
||||||
"@vector-im/compound-web",
|
"@vector-im/compound-web",
|
||||||
"matrix-widget-api",
|
"matrix-widget-api",
|
||||||
|
// Used by oxlint
|
||||||
|
"eslint-plugin-element-call",
|
||||||
|
"eslint-plugin-storybook",
|
||||||
],
|
],
|
||||||
ignoreExportsUsedInFile: true,
|
ignoreExportsUsedInFile: true,
|
||||||
} satisfies KnipConfig;
|
} satisfies KnipConfig;
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
"blur_not_supported_by_browser": "(Background blur is not supported by this device.)",
|
"blur_not_supported_by_browser": "(Background blur is not supported by this device.)",
|
||||||
"developer_tab_title": "Developer",
|
"developer_tab_title": "Developer",
|
||||||
"devices": {
|
"devices": {
|
||||||
|
"activating": "Activating…",
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"camera_numbered": "Camera {{n}}",
|
"camera_numbered": "Camera {{n}}",
|
||||||
"change_device_button": "Change audio device",
|
"change_device_button": "Change audio device",
|
||||||
|
|||||||
37
package.json
37
package.json
@@ -17,11 +17,11 @@
|
|||||||
"build:sdk": "pnpm build:full --config vite-sdk.config.js",
|
"build:sdk": "pnpm build:full --config vite-sdk.config.js",
|
||||||
"build:sdk:production": "pnpm build:sdk",
|
"build:sdk:production": "pnpm build:sdk",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"prettier:check": "prettier -c .",
|
"format": "oxfmt",
|
||||||
"prettier:format": "prettier -w .",
|
"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": "pnpm lint:types && pnpm lint:oxlint && pnpm lint:knip",
|
||||||
"lint:eslint": "eslint --max-warnings 0 src playwright",
|
"lint:oxlint": "oxlint src playwright",
|
||||||
"lint:eslint-fix": "eslint --max-warnings 0 src playwright --fix",
|
"lint:oxlint-fix": "oxlint --fix src playwright",
|
||||||
"lint:knip": "knip",
|
"lint:knip": "knip",
|
||||||
"lint:types": "tsc",
|
"lint:types": "tsc",
|
||||||
"i18n": "npx i18next-cli extract",
|
"i18n": "npx i18next-cli extract",
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
"@formatjs/intl-segmenter": "^11.7.3",
|
"@formatjs/intl-segmenter": "^11.7.3",
|
||||||
"@livekit/components-core": "^0.12.0",
|
"@livekit/components-core": "^0.12.0",
|
||||||
"@livekit/components-react": "^2.0.0",
|
"@livekit/components-react": "^2.0.0",
|
||||||
"@livekit/protocol": "^1.42.2",
|
|
||||||
"@livekit/track-processors": "^0.7.1",
|
"@livekit/track-processors": "^0.7.1",
|
||||||
"@mediapipe/tasks-vision": "^0.10.18",
|
"@mediapipe/tasks-vision": "^0.10.18",
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.60.0",
|
||||||
@@ -60,11 +59,11 @@
|
|||||||
"@storybook/addon-docs": "^10.3.6",
|
"@storybook/addon-docs": "^10.3.6",
|
||||||
"@storybook/addon-vitest": "^10.3.6",
|
"@storybook/addon-vitest": "^10.3.6",
|
||||||
"@storybook/react-vite": "^10.3.6",
|
"@storybook/react-vite": "^10.3.6",
|
||||||
"@stylistic/eslint-plugin": "^3.0.0",
|
|
||||||
"@testing-library/dom": "^10.1.0",
|
"@testing-library/dom": "^10.1.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/grecaptcha": "^3.0.9",
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
@@ -74,32 +73,19 @@
|
|||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@types/sdp-transform": "^2.4.5",
|
"@types/sdp-transform": "^2.4.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
"@typescript-eslint/utils": "^8.61.0",
|
||||||
"@typescript-eslint/parser": "^8.31.0",
|
|
||||||
"@use-gesture/react": "^10.2.11",
|
"@use-gesture/react": "^10.2.11",
|
||||||
"@vector-im/compound-design-tokens": "^10.0.0",
|
"@vector-im/compound-design-tokens": "^10.0.0",
|
||||||
"@vector-im/compound-web": "^9.3.0",
|
"@vector-im/compound-web": "^9.3.0",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@vitest/browser-playwright": "^4.1.5",
|
"@vitest/browser-playwright": "^4.1.5",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"@vitest/ui": "4.1.7",
|
"@vitest/ui": "4.1.9",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"eslint": "^8.14.0",
|
"eslint-plugin-element-call": "link:eslint",
|
||||||
"eslint-config-google": "^0.14.0",
|
|
||||||
"eslint-config-prettier": "^10.0.0",
|
|
||||||
"eslint-plugin-deprecate": "^0.9.0",
|
|
||||||
"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-storybook": "^10.3.6",
|
||||||
"eslint-plugin-unicorn": "^56.0.0",
|
|
||||||
"fetch-mock": "11.1.5",
|
"fetch-mock": "11.1.5",
|
||||||
"global-jsdom": "^26.0.0",
|
|
||||||
"i18next": "^25.0.0",
|
"i18next": "^25.0.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-cli": "^1.61.0",
|
"i18next-cli": "^1.61.0",
|
||||||
@@ -113,11 +99,13 @@
|
|||||||
"node-stdlib-browser": "^1.3.1",
|
"node-stdlib-browser": "^1.3.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"observable-hooks": "^4.2.3",
|
"observable-hooks": "^4.2.3",
|
||||||
|
"oxfmt": "^0.56.0",
|
||||||
|
"oxlint": "^1.70.0",
|
||||||
|
"oxlint-tsgolint": "^0.23.0",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
"posthog-js": "1.374.0",
|
"posthog-js": "1.374.0",
|
||||||
"prettier": "^3.0.0",
|
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "19",
|
"react": "19",
|
||||||
"react-dom": "19",
|
"react-dom": "19",
|
||||||
@@ -128,7 +116,6 @@
|
|||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"storybook": "^10.3.6",
|
"storybook": "^10.3.6",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint-language-service": "^5.0.5",
|
|
||||||
"unique-names-generator": "^4.6.0",
|
"unique-names-generator": "^4.6.0",
|
||||||
"uuid": "^14.0.0",
|
"uuid": "^14.0.0",
|
||||||
"vaul": "^1.0.0",
|
"vaul": "^1.0.0",
|
||||||
|
|||||||
@@ -120,6 +120,11 @@ export const widgetTest = test.extend<MyFixtures>({
|
|||||||
.getByRole("heading", { name: "Welcome Room" }),
|
.getByRole("heading", { name: "Welcome Room" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
} else if (callType === "dm") {
|
} else if (callType === "dm") {
|
||||||
|
await TestHelpers.closeReleaseAnnouncement(
|
||||||
|
ewPage1,
|
||||||
|
"Introducing Sections",
|
||||||
|
);
|
||||||
|
|
||||||
await ewPage1
|
await ewPage1
|
||||||
.getByRole("navigation", { name: "Room list" })
|
.getByRole("navigation", { name: "Room list" })
|
||||||
.getByRole("button", { name: "New conversation" })
|
.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.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
await guestCPage.waitForTimeout(1000);
|
await guestCPage.waitForTimeout(1000);
|
||||||
|
if (wsConnectionCount === 2) {
|
||||||
|
console.warn("wsConnectionCount is 2, expecting 1 after join");
|
||||||
|
}
|
||||||
|
const wsConnectionCountBeforeLeave = wsConnectionCount;
|
||||||
// ========
|
// ========
|
||||||
// the creator leaves the call
|
// the creator leaves the call
|
||||||
await creatorPage.getByTestId("incall_leave").click();
|
await creatorPage.getByTestId("incall_leave").click();
|
||||||
|
|
||||||
// https://github.com/element-hq/element-call/issues/3344
|
// https://github.com/element-hq/element-call/issues/3344
|
||||||
// The app used to request a new jwt token then to reconnect to the SFU
|
// 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
|
// Wait a bit to be sure that if there was a reconnect, it would have happened by now
|
||||||
await guestCPage.waitForTimeout(6000);
|
await guestCPage.waitForTimeout(6000);
|
||||||
expect(wsConnectionCount).toBe(1);
|
expect(wsConnectionCount).toBe(wsConnectionCountBeforeLeave);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,21 +125,10 @@ async function expectVideoTilesCount(page: Page, count: number): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// There should be `count` video elements, visible and autoplaying
|
// There should be `count` video elements, visible and autoplaying
|
||||||
await expect(page.locator("video")).toHaveCount(count);
|
await expect(page.locator("video").filter({ visible: true })).toHaveCount(
|
||||||
|
count,
|
||||||
await expect(async () => {
|
{ timeout: 10000 },
|
||||||
const videoBlockCount = await page
|
);
|
||||||
.locator("video")
|
|
||||||
.evaluateAll(
|
|
||||||
(videos: Element[]) =>
|
|
||||||
videos.filter(
|
|
||||||
(v: Element) => window.getComputedStyle(v).display === "block",
|
|
||||||
).length,
|
|
||||||
);
|
|
||||||
expect(videoBlockCount).toBe(count);
|
|
||||||
}).toPass({
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpaHelpers = {
|
export const SpaHelpers = {
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export class TestHelpers {
|
|||||||
const expectedToasts = [
|
const expectedToasts = [
|
||||||
{ title: "Failed to load service worker", button: "OK" },
|
{ title: "Failed to load service worker", button: "OK" },
|
||||||
{ title: "Back up your chats", button: "Dismiss" },
|
{ title: "Back up your chats", button: "Dismiss" },
|
||||||
|
{ title: "Turn on key storage", button: "Dismiss" },
|
||||||
{ title: "Element does not support this browser", 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(
|
public static async createRoom(
|
||||||
name: string,
|
name: string,
|
||||||
page: Page,
|
page: Page,
|
||||||
andInvite: string[] = [],
|
andInvite: string[] = [],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await TestHelpers.closeReleaseAnnouncement(page, "Introducing Sections");
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole("navigation", { name: "Room list" })
|
.getByRole("navigation", { name: "Room list" })
|
||||||
.getByRole("button", { name: "New conversation" })
|
.getByRole("button", { name: "New conversation" })
|
||||||
@@ -368,25 +385,9 @@ export class TestHelpers {
|
|||||||
frame: FrameLocator,
|
frame: FrameLocator,
|
||||||
count: number,
|
count: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// XXX we need to be better at our HTML markup and accessibility, it would make
|
await expect(frame.locator("video").filter({ visible: true })).toHaveCount(
|
||||||
// this kind of stuff way easier to test if we could look out for aria attributes.
|
count,
|
||||||
await expect
|
{ timeout: 10000 },
|
||||||
.poll(
|
);
|
||||||
async () => {
|
|
||||||
return await frame
|
|
||||||
.locator("video")
|
|
||||||
.evaluateAll(
|
|
||||||
(videos: Element[]) =>
|
|
||||||
videos.filter(
|
|
||||||
(v: Element) =>
|
|
||||||
window.getComputedStyle(v).display === "block",
|
|
||||||
).length,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: 10000,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toBe(count);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,10 @@ widgetTest(
|
|||||||
const brooksFrame = brooks.page
|
const brooksFrame = brooks.page
|
||||||
.locator('iframe[title="Element Call"]')
|
.locator('iframe[title="Element Call"]')
|
||||||
.contentFrame();
|
.contentFrame();
|
||||||
|
|
||||||
// We should show a ringing tile, let's check for that
|
// We should show a ringing tile, let's check for that
|
||||||
await expect(
|
await expect(
|
||||||
brooksFrame
|
brooksFrame
|
||||||
.getByTestId("videoTile")
|
.getByTestId("videoTile")
|
||||||
.filter({ has: brooksFrame.getByText(whistler.displayName) })
|
|
||||||
.filter({ has: brooksFrame.getByText("Calling…") }),
|
.filter({ has: brooksFrame.getByText("Calling…") }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
@@ -83,23 +81,22 @@ widgetTest(
|
|||||||
}),
|
}),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
|
// We confirm that we started in Pip mode (voice call) and check that we still see the composer.
|
||||||
// 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
|
|
||||||
await expect(
|
await expect(
|
||||||
whistler.page.locator(".mx_BasicMessageComposer"),
|
whistler.page.locator(".mx_BasicMessageComposer"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(brooks.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();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
5825
pnpm-lock.yaml
generated
5825
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"semanticCommits": "disabled",
|
"semanticCommits": "disabled",
|
||||||
"ignoreDeps": ["eslint-plugin-matrix-org"],
|
"ignoreDeps": [],
|
||||||
"vulnerabilityAlerts": {
|
"vulnerabilityAlerts": {
|
||||||
"schedule": ["at any time"],
|
"schedule": ["at any time"],
|
||||||
"prHourlyLimit": 0,
|
"prHourlyLimit": 0,
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ import { scan } from "rxjs";
|
|||||||
import { type WidgetHelpers } from "../src/widget";
|
import { type WidgetHelpers } from "../src/widget";
|
||||||
import { type LivekitRoomItem } from "../src/state/CallViewModel/CallViewModel";
|
import { type LivekitRoomItem } from "../src/state/CallViewModel/CallViewModel";
|
||||||
|
|
||||||
export const logger = rootLogger.getChild("[MatrixRTCSdk]");
|
|
||||||
|
|
||||||
export const tryMakeSticky = (widget: WidgetHelpers): void => {
|
export const tryMakeSticky = (widget: WidgetHelpers): void => {
|
||||||
|
const logger = rootLogger.getChild("[MatrixRTCSdk]");
|
||||||
logger.info("try making sticky MatrixRTCSdk");
|
logger.info("try making sticky MatrixRTCSdk");
|
||||||
void widget.api
|
void widget.api
|
||||||
.setAlwaysOnScreen(true)
|
.setAlwaysOnScreen(true)
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ import { getUrlParams } from "../src/UrlParams";
|
|||||||
import { MuteStates } from "../src/state/MuteStates";
|
import { MuteStates } from "../src/state/MuteStates";
|
||||||
import { MediaDevices } from "../src/state/MediaDevices";
|
import { MediaDevices } from "../src/state/MediaDevices";
|
||||||
import { E2eeType } from "../src/e2ee/e2eeType";
|
import { E2eeType } from "../src/e2ee/e2eeType";
|
||||||
import { currentAndPrev, logger, TEXT_LK_TOPIC, tryMakeSticky } from "./helper";
|
import { currentAndPrev, TEXT_LK_TOPIC, tryMakeSticky } from "./helper";
|
||||||
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import {
|
import {
|
||||||
ElementWidgetActions,
|
ElementWidgetActions,
|
||||||
widget as _widget,
|
widget as _widget,
|
||||||
@@ -104,6 +105,7 @@ export async function createMatrixRTCSdk(
|
|||||||
id: string = "",
|
id: string = "",
|
||||||
sticky: boolean = false,
|
sticky: boolean = false,
|
||||||
): Promise<MatrixRTCSdk> {
|
): Promise<MatrixRTCSdk> {
|
||||||
|
const logger = rootLogger.getChild("[MatrixRTCSdk]");
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
|
|
||||||
// widget client
|
// widget client
|
||||||
|
|||||||
18
src/App.tsx
18
src/App.tsx
@@ -30,7 +30,7 @@ import { useTheme } from "./useTheme";
|
|||||||
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
||||||
import { type AppViewModel } from "./state/AppViewModel";
|
import { type AppViewModel } from "./state/AppViewModel";
|
||||||
import { MediaDevicesContext } from "./MediaDevicesContext";
|
import { MediaDevicesContext } from "./MediaDevicesContext";
|
||||||
import { getUrlParams, HeaderStyle } from "./UrlParams";
|
import { getUrlParams, HeaderStyle, useUrlParams } from "./UrlParams";
|
||||||
import { AppBar } from "./AppBar";
|
import { AppBar } from "./AppBar";
|
||||||
|
|
||||||
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
|
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
|
||||||
@@ -41,19 +41,17 @@ interface SimpleProviderProps {
|
|||||||
|
|
||||||
const BackgroundProvider: FC<SimpleProviderProps> = ({ children }) => {
|
const BackgroundProvider: FC<SimpleProviderProps> = ({ children }) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const { background } = useUrlParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let backgroundImage = "";
|
document
|
||||||
if (!["/login", "/register"].includes(pathname) && !widget) {
|
.getElementsByTagName("body")[0]
|
||||||
backgroundImage = "var(--background-gradient)";
|
.setAttribute("data-background", background);
|
||||||
}
|
}, [pathname, background]);
|
||||||
|
|
||||||
document.getElementsByTagName("body")[0].style.backgroundImage =
|
return children;
|
||||||
backgroundImage;
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
||||||
useTheme();
|
useTheme();
|
||||||
return children;
|
return children;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
.bar {
|
.bar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: var(--call-view-header-footer-layer);
|
||||||
|
padding-left: var(--content-inset-left);
|
||||||
|
padding-right: var(--content-inset-right);
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pseudo-element for the gradient background */
|
/* Pseudo-element for the gradient background */
|
||||||
@@ -9,8 +15,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline: 0;
|
inset-inline: 0;
|
||||||
/* Extend the gradient beyond the bottom of the header for readability */
|
/* Extend the gradient beyond the bottom of the header for readability */
|
||||||
inset-block: -24px;
|
inset-block: 0 -16px;
|
||||||
z-index: var(--call-view-header-footer-layer);
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
0deg,
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
@@ -18,21 +23,157 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bar.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
/* Switch to position: absolute so the bar takes up no space in the layout
|
||||||
|
when hidden. */
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0;
|
||||||
|
inset-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:has(:focus-visible) {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.bar > header {
|
.bar > header {
|
||||||
|
flex-shrink: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
inset-inline: 0;
|
inset-inline: 0;
|
||||||
inset-block-start: 0;
|
inset-block-start: 0;
|
||||||
block-size: 64px;
|
display: grid;
|
||||||
z-index: var(--call-view-header-footer-layer);
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
grid-template-rows:
|
||||||
|
var(--cpd-space-3x) minmax(var(--cpd-space-10x), auto)
|
||||||
|
var(--cpd-space-3x);
|
||||||
|
grid-template-areas:
|
||||||
|
". . ."
|
||||||
|
"primaryButton title secondaryButton"
|
||||||
|
". . .";
|
||||||
|
place-items: center;
|
||||||
|
column-gap: var(--cpd-space-2x);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar svg path {
|
.bar:has(.subtitle) > header {
|
||||||
fill: var(--cpd-color-icon-primary);
|
grid-template-rows:
|
||||||
|
var(--cpd-space-3x) minmax(var(--cpd-space-10x), auto) var(--cpd-space-5x)
|
||||||
|
minmax(var(--cpd-space-8x), auto);
|
||||||
|
grid-template-areas:
|
||||||
|
". . ."
|
||||||
|
"primaryButton title secondaryButton"
|
||||||
|
". . ."
|
||||||
|
"subtitle subtitle subtitle";
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar > header > h1 {
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
grid-area: title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
grid-area: subtitle;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--cpd-color-icon-tertiary);
|
||||||
|
margin-inline-end: var(--cpd-space-2x);
|
||||||
|
block-size: 1.2em;
|
||||||
|
inline-size: 1.2em;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondaryButton {
|
||||||
|
grid-area: secondaryButton;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryButton,
|
||||||
|
.secondaryButton {
|
||||||
|
svg * {
|
||||||
|
color: var(--cpd-color-icon-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-platform="ios"] {
|
||||||
|
.bar > header {
|
||||||
|
grid-template-rows: minmax(var(--cpd-space-11x), auto) var(--cpd-space-4x);
|
||||||
|
grid-template-areas: "primaryButton title secondaryButton";
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:has(.subtitle) > header {
|
||||||
|
grid-template-rows:
|
||||||
|
minmax(var(--cpd-space-6x), auto) minmax(var(--cpd-space-5x), auto)
|
||||||
|
var(--cpd-space-4x);
|
||||||
|
grid-template-areas:
|
||||||
|
"primaryButton title secondaryButton"
|
||||||
|
"primaryButton subtitle secondaryButton";
|
||||||
|
|
||||||
|
.title {
|
||||||
|
align-self: end;
|
||||||
|
/* Nudge the title and subtitle even closer together to replicate native
|
||||||
|
iOS styles */
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,21 +5,33 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { type FC, type ReactNode } from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { AppBar } from "./AppBar";
|
import { AppBar, useAppBarSubtitle, useAppBarTitle } from "./AppBar";
|
||||||
|
|
||||||
|
const content = <p>This is the content.</p>;
|
||||||
|
|
||||||
|
function snapshotAppBar(content: ReactNode): void {
|
||||||
|
const { container } = render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<AppBar>{content}</AppBar>
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
describe("AppBar", () => {
|
describe("AppBar", () => {
|
||||||
it("renders", () => {
|
it("renders", () => snapshotAppBar(content));
|
||||||
const { container } = render(
|
|
||||||
<TooltipProvider>
|
it("renders with title and subtitle", () => {
|
||||||
<AppBar>
|
const TestComponent: FC = () => {
|
||||||
<p>This is the content.</p>
|
useAppBarTitle("Title");
|
||||||
</AppBar>
|
useAppBarSubtitle("Subtitle");
|
||||||
</TooltipProvider>,
|
return content;
|
||||||
);
|
};
|
||||||
expect(container).toMatchSnapshot();
|
snapshotAppBar(<TestComponent />);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
124
src/AppBar.tsx
124
src/AppBar.tsx
@@ -6,28 +6,34 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
|
||||||
type FC,
|
|
||||||
type MouseEvent,
|
|
||||||
type ReactNode,
|
|
||||||
use,
|
use,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
|
createContext,
|
||||||
|
type FC,
|
||||||
|
type MouseEvent,
|
||||||
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Heading, IconButton, Tooltip } from "@vector-im/compound-web";
|
import classNames from "classnames";
|
||||||
import { CollapseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
import { Heading, IconButton, Text, Tooltip } from "@vector-im/compound-web";
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
CollapseIcon,
|
||||||
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { Header, LeftNav, RightNav } from "./Header";
|
|
||||||
import { platform } from "./Platform";
|
import { platform } from "./Platform";
|
||||||
import styles from "./AppBar.module.css";
|
import styles from "./AppBar.module.css";
|
||||||
|
|
||||||
interface AppBarContext {
|
interface AppBarContext {
|
||||||
setTitle: (value: string) => void;
|
setTitle: (value: string) => void;
|
||||||
|
setSubtitle: (value: ReactNode) => void;
|
||||||
setSecondaryButton: (value: ReactNode) => void;
|
setSecondaryButton: (value: ReactNode) => void;
|
||||||
|
setPrimaryButtonIconKind: (value: "back" | "minimise") => void;
|
||||||
setHidden: (value: boolean) => void;
|
setHidden: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,44 +55,76 @@ export const AppBar: FC<Props> = ({ children }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
|
const [subtitle, setSubtitle] = useState<ReactNode>(undefined);
|
||||||
const [hidden, setHidden] = useState<boolean>(false);
|
const [hidden, setHidden] = useState<boolean>(false);
|
||||||
const [secondaryButton, setSecondaryButton] = useState<ReactNode | null>(
|
const [secondaryButton, setSecondaryButton] = useState<ReactNode | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [primaryButtonIcon, setPrimaryButtonIconKind] = useState<
|
||||||
|
"back" | "minimise"
|
||||||
|
>("minimise");
|
||||||
|
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => ({ setTitle, setSecondaryButton, setHidden }),
|
() => ({
|
||||||
[setTitle, setHidden, setSecondaryButton],
|
setTitle,
|
||||||
|
setSubtitle,
|
||||||
|
setSecondaryButton,
|
||||||
|
setHidden,
|
||||||
|
setPrimaryButtonIconKind,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
setTitle,
|
||||||
|
setSubtitle,
|
||||||
|
setHidden,
|
||||||
|
setSecondaryButton,
|
||||||
|
setPrimaryButtonIconKind,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const BackIcon = platform === "android" ? ArrowLeftIcon : ChevronLeftIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
{/* Wrap the header in a div due to annoying z-index issues with the
|
||||||
style={{ display: hidden ? "none" : "block" }}
|
gradient background */}
|
||||||
className={styles.bar}
|
<div className={classNames(styles.bar, { [styles.hidden]: hidden })}>
|
||||||
>
|
<header>
|
||||||
<Header
|
<Tooltip label={t("common.back")}>
|
||||||
// App bar is mainly seen in the call view, which has its own
|
<IconButton
|
||||||
// 'reconnecting' toast
|
className={styles.primaryButton}
|
||||||
disconnectedBanner={false}
|
// We render the back button (PrimaryButtonIcon) the same size as the native os.
|
||||||
>
|
// We render the minimise icon (default) smaller as per designs.
|
||||||
<LeftNav>
|
size={primaryButtonIcon === "back" ? "32px" : "24px"}
|
||||||
<Tooltip label={t("common.back")}>
|
onClick={onBackClick}
|
||||||
<IconButton size="24px" onClick={onBackClick}>
|
>
|
||||||
|
{primaryButtonIcon === "back" ? (
|
||||||
|
<BackIcon aria-hidden />
|
||||||
|
) : (
|
||||||
<CollapseIcon aria-hidden />
|
<CollapseIcon aria-hidden />
|
||||||
</IconButton>
|
)}
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
</LeftNav>
|
</Tooltip>
|
||||||
{title && (
|
{title && (
|
||||||
<Heading
|
<Heading
|
||||||
|
className={styles.title}
|
||||||
type="body"
|
type="body"
|
||||||
size="lg"
|
size={platform === "ios" ? "md" : "lg"}
|
||||||
weight={platform === "android" ? "medium" : "semibold"}
|
weight={platform === "ios" ? "semibold" : "medium"}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
)}
|
)}
|
||||||
<RightNav>{secondaryButton}</RightNav>
|
{subtitle && (
|
||||||
</Header>
|
<Text
|
||||||
|
className={styles.subtitle}
|
||||||
|
as="span"
|
||||||
|
size={platform === "ios" ? "sm" : "lg"}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<div className={styles.secondaryButton}>{secondaryButton}</div>
|
||||||
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<AppBarContext value={context}>{children}</AppBarContext>
|
<AppBarContext value={context}>{children}</AppBarContext>
|
||||||
</>
|
</>
|
||||||
@@ -107,6 +145,36 @@ export function useAppBarTitle(title: string): void {
|
|||||||
}, [title, setTitle]);
|
}, [title, setTitle]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook which sets the subtitle to be shown in the app bar, if present. It
|
||||||
|
* is an error to call this hook from multiple sites in the same component tree.
|
||||||
|
*/
|
||||||
|
export function useAppBarSubtitle(subtitle: ReactNode): void {
|
||||||
|
const setSubtitle = use(AppBarContext)?.setSubtitle;
|
||||||
|
useEffect(() => {
|
||||||
|
if (setSubtitle !== undefined) {
|
||||||
|
setSubtitle(subtitle);
|
||||||
|
return (): void => setSubtitle("");
|
||||||
|
}
|
||||||
|
}, [subtitle, setSubtitle]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook which sets the primary button icon kind. Can only be "minimise" or "back"
|
||||||
|
* It is an error to call this hook from multiple sites in the same component tree.
|
||||||
|
*/
|
||||||
|
export function useAppBarPrimaryButtonIconKind(
|
||||||
|
icon: "back" | "minimise",
|
||||||
|
): void {
|
||||||
|
const setIconKind = use(AppBarContext)?.setPrimaryButtonIconKind;
|
||||||
|
useEffect(() => {
|
||||||
|
if (setIconKind !== undefined) {
|
||||||
|
setIconKind(icon);
|
||||||
|
return (): void => setIconKind("minimise");
|
||||||
|
}
|
||||||
|
}, [setIconKind, icon]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React hook which sets the title to be shown in the app bar, if present. It is
|
* React hook which sets the title to be shown in the app bar, if present. It is
|
||||||
* an error to call this hook from multiple sites in the same component tree.
|
* an error to call this hook from multiple sites in the same component tree.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ErrorPageProps {
|
interface ErrorPageProps {
|
||||||
error: Error | unknown;
|
error: unknown;
|
||||||
widget: WidgetHelpers | null;
|
widget: WidgetHelpers | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Config } from "./config/Config";
|
|||||||
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||||
import { E2eeType } from "./e2ee/e2eeType";
|
import { E2eeType } from "./e2ee/e2eeType";
|
||||||
import { platform } from "./Platform";
|
import { platform } from "./Platform";
|
||||||
|
import { redact } from "./utils/redact";
|
||||||
|
|
||||||
interface RoomIdentifier {
|
interface RoomIdentifier {
|
||||||
roomAlias: string | null;
|
roomAlias: string | null;
|
||||||
@@ -44,6 +45,11 @@ export enum HeaderStyle {
|
|||||||
AppBar = "app_bar",
|
AppBar = "app_bar",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum BackgroundStyle {
|
||||||
|
Solid = "solid",
|
||||||
|
Gradient = "gradient",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UrlProperties are used to pass required data to the widget.
|
* The UrlProperties are used to pass required data to the widget.
|
||||||
* Those are different in different rooms, users, devices. They do not configure the behavior of the
|
* Those are different in different rooms, users, devices. They do not configure the behavior of the
|
||||||
@@ -144,6 +150,10 @@ export interface UrlProperties {
|
|||||||
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
||||||
*/
|
*/
|
||||||
theme: string | null;
|
theme: string | null;
|
||||||
|
/**
|
||||||
|
* The visual style of the page background.
|
||||||
|
*/
|
||||||
|
background: BackgroundStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -451,6 +461,9 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
|||||||
fonts: parser.getAllParams("font"),
|
fonts: parser.getAllParams("font"),
|
||||||
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
||||||
theme: parser.getParam("theme"),
|
theme: parser.getParam("theme"),
|
||||||
|
background:
|
||||||
|
parser.getEnumParam("background", BackgroundStyle) ??
|
||||||
|
BackgroundStyle.Gradient,
|
||||||
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
||||||
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
||||||
posthogApiHost: parser.getParam("posthogApiHost"),
|
posthogApiHost: parser.getParam("posthogApiHost"),
|
||||||
@@ -494,7 +507,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
|||||||
"intent:",
|
"intent:",
|
||||||
intent,
|
intent,
|
||||||
"\nproperties:",
|
"\nproperties:",
|
||||||
properties,
|
redact(properties, "password"),
|
||||||
"configuration:",
|
"configuration:",
|
||||||
configuration,
|
configuration,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,44 +3,90 @@
|
|||||||
exports[`AppBar > renders 1`] = `
|
exports[`AppBar > renders 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="bar"
|
class="_bar_221541"
|
||||||
style="display: block;"
|
|
||||||
>
|
>
|
||||||
<header
|
<header>
|
||||||
class="header"
|
<button
|
||||||
>
|
aria-labelledby="_r_0_"
|
||||||
<div
|
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||||
class="nav leftNav"
|
data-kind="primary"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 24px;"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
aria-labelledby="_r_0_"
|
class="_indicator-icon_147l5_17"
|
||||||
class="_icon-button_1215g_8"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
data-kind="primary"
|
|
||||||
role="button"
|
|
||||||
style="--cpd-icon-button-size: 24px;"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<div
|
<svg
|
||||||
class="_indicator-icon_147l5_17"
|
aria-hidden="true"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<svg
|
<path
|
||||||
aria-hidden="true"
|
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z"
|
||||||
fill="currentColor"
|
/>
|
||||||
height="1em"
|
</svg>
|
||||||
viewBox="0 0 24 24"
|
</div>
|
||||||
width="1em"
|
</button>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_secondaryButton_221541"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
This is the content.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AppBar > renders with title and subtitle 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="_bar_221541"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_6_"
|
||||||
|
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||||
|
data-kind="primary"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 24px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<h1
|
||||||
|
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_221541"
|
||||||
|
>
|
||||||
|
Subtitle
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="_secondaryButton_221541"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
exports[`the content is rendered when the modal is open 1`] = `
|
exports[`the content is rendered when the modal is open 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-labelledby="radix-_r_4_"
|
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"
|
data-state="open"
|
||||||
id="radix-_r_3_"
|
id="radix-_r_3_"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -11,10 +11,10 @@ exports[`the content is rendered when the modal is open 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_dbeffe"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="header"
|
class="_header_dbeffe"
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
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>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="body"
|
class="_body_dbeffe"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
This is the content.
|
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`] = `
|
exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-labelledby="radix-_r_a_"
|
aria-labelledby="radix-_r_a_"
|
||||||
class="overlay modal drawer"
|
class="_overlay_2f5303 _modal_dbeffe _drawer_dbeffe"
|
||||||
data-state="open"
|
data-state="open"
|
||||||
data-vaul-animate="true"
|
data-vaul-animate="true"
|
||||||
data-vaul-custom-container="false"
|
data-vaul-custom-container="false"
|
||||||
@@ -51,13 +51,13 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_dbeffe"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="header"
|
class="_header_dbeffe"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="handle"
|
class="_handle_dbeffe"
|
||||||
/>
|
/>
|
||||||
<h2
|
<h2
|
||||||
id="radix-_r_a_"
|
id="radix-_r_a_"
|
||||||
@@ -67,7 +67,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="body"
|
class="_body_dbeffe"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
This is the content.
|
This is the content.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`QrCode > renders 1`] = `
|
exports[`QrCode > renders 1`] = `
|
||||||
<div
|
<div
|
||||||
class="qrCode bar"
|
class="_qrCode_458264 bar"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="QR Code"
|
alt="QR Code"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
exports[`Toast > renders 1`] = `
|
exports[`Toast > renders 1`] = `
|
||||||
<button
|
<button
|
||||||
aria-labelledby="radix-_r_4_"
|
aria-labelledby="radix-_r_4_"
|
||||||
class="overlay animate toast"
|
class="_overlay_2f5303 _animate_2f5303 _toast_15a045"
|
||||||
data-state="open"
|
data-state="open"
|
||||||
id="radix-_r_3_"
|
id="radix-_r_3_"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.endCall > svg {
|
|
||||||
color: var(--stopgap-color-on-solid-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rotate > svg {
|
.rotate > svg {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,21 +135,12 @@ interface EndCallButtonProps extends ComponentPropsWithoutRef<"button"> {
|
|||||||
size?: "md" | "lg";
|
size?: "md" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EndCallButton: FC<EndCallButtonProps> = ({
|
export const EndCallButton: FC<EndCallButtonProps> = (props) => {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={t("hangup_button_label")}>
|
<Tooltip label={t("hangup_button_label")}>
|
||||||
<CpdButton
|
<CpdButton iconOnly Icon={EndCallIcon} destructive {...props} />
|
||||||
className={classNames(className, styles.endCall)}
|
|
||||||
iconOnly
|
|
||||||
Icon={EndCallIcon}
|
|
||||||
destructive
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -173,7 +164,7 @@ export const LoudspeakerButton: FC<LoudspeakerButtonProps> = ({
|
|||||||
iconOnly
|
iconOnly
|
||||||
Icon={loudspeakerModeEnabled ? VolumeOnSolidIcon : VolumeOffSolidIcon}
|
Icon={loudspeakerModeEnabled ? VolumeOnSolidIcon : VolumeOffSolidIcon}
|
||||||
{...props}
|
{...props}
|
||||||
kind={loudspeakerModeEnabled ? "primary" : "secondary"}
|
kind={loudspeakerModeEnabled ? "secondary" : "primary"}
|
||||||
aria-checked={loudspeakerModeEnabled}
|
aria-checked={loudspeakerModeEnabled}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby="_r_1j_"
|
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-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ Please see LICENSE in the repository root for full details.
|
|||||||
padding-right: calc(env(safe-area-inset-right) + var(--cpd-space-6x));
|
padding-right: calc(env(safe-area-inset-right) + var(--cpd-space-6x));
|
||||||
padding-block: var(--cpd-space-10x)
|
padding-block: var(--cpd-space-10x)
|
||||||
calc(env(safe-area-inset-bottom) + var(--cpd-space-10x));
|
calc(env(safe-area-inset-bottom) + var(--cpd-space-10x));
|
||||||
background: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(0, 0, 0, 0) 0%,
|
|
||||||
var(--cpd-color-bg-canvas-default) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer.hidden {
|
.footer.hidden {
|
||||||
|
|||||||
@@ -112,11 +112,17 @@ export interface FooterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FooterProps {
|
export interface FooterProps {
|
||||||
|
className?: string;
|
||||||
ref?: Ref<HTMLDivElement>;
|
ref?: Ref<HTMLDivElement>;
|
||||||
children?: JSX.Element | JSX.Element[] | false;
|
children?: JSX.Element | JSX.Element[] | false;
|
||||||
vm: ViewModel<FooterSnapshot>;
|
vm: ViewModel<FooterSnapshot>;
|
||||||
}
|
}
|
||||||
export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
export const CallFooter: FC<FooterProps> = ({
|
||||||
|
className,
|
||||||
|
ref,
|
||||||
|
children,
|
||||||
|
vm,
|
||||||
|
}) => {
|
||||||
const asOverlay = useBehavior(vm.asOverlay$);
|
const asOverlay = useBehavior(vm.asOverlay$);
|
||||||
const showFooter = useBehavior(vm.showFooter$);
|
const showFooter = useBehavior(vm.showFooter$);
|
||||||
const hideControls = useBehavior(vm.hideControls$);
|
const hideControls = useBehavior(vm.hideControls$);
|
||||||
@@ -292,7 +298,7 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-testid="footer-container"
|
data-testid="footer-container"
|
||||||
className={classNames(styles.footer, {
|
className={classNames(className, styles.footer, {
|
||||||
[styles.overlay]: asOverlay,
|
[styles.overlay]: asOverlay,
|
||||||
[styles.hidden]: !showFooter,
|
[styles.hidden]: !showFooter,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -173,9 +173,7 @@ export function createCallFooterViewModel(
|
|||||||
callModel.setSettingsOpen$,
|
callModel.setSettingsOpen$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([isPip, showHeader, setSettingsOpen]) =>
|
map(([isPip, showHeader, setSettingsOpen]) =>
|
||||||
!isPip &&
|
!isPip && headerStyle !== HeaderStyle.AppBar && showControls
|
||||||
!(headerStyle === HeaderStyle.AppBar && showHeader) &&
|
|
||||||
showControls
|
|
||||||
? (): void => setSettingsOpen(true)
|
? (): void => setSettingsOpen(true)
|
||||||
: undefined,
|
: undefined,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -180,12 +180,12 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Microphone" }));
|
await user.click(screen.getByRole("button", { name: "Microphone" }));
|
||||||
screen.getByRole("menuitem", { name: "Microphone 1" });
|
screen.getByRole("menuitemradio", { name: "Microphone 1" });
|
||||||
screen.getByRole("menuitem", { name: "Microphone 2" });
|
screen.getByRole("menuitemradio", { name: "Microphone 2" });
|
||||||
await user.keyboard("[Escape]");
|
await user.keyboard("[Escape]");
|
||||||
await user.click(screen.getByRole("button", { name: "Camera" }));
|
await user.click(screen.getByRole("button", { name: "Camera" }));
|
||||||
screen.getByRole("menuitem", { name: "Camera 1" });
|
screen.getByRole("menuitemradio", { name: "Camera 1" });
|
||||||
screen.getByRole("menuitem", { name: "Camera 2" });
|
screen.getByRole("menuitemradio", { name: "Camera 2" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("calls select callback on menu click", async () => {
|
test("calls select callback on menu click", async () => {
|
||||||
@@ -206,7 +206,9 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await user.click(getByRole("button", { name: "Microphone" }));
|
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");
|
expect(onSelect).toHaveBeenCalledWith("mic2");
|
||||||
});
|
});
|
||||||
@@ -228,7 +230,9 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await user.click(getByRole("button", { name: "Microphone" }));
|
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();
|
expect(onSelect).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -264,18 +268,24 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
const { getByRole } = renderComponent(<Wrapper />);
|
const { getByRole } = renderComponent(<Wrapper />);
|
||||||
|
|
||||||
await user.click(getByRole("button", { name: "Microphone" }));
|
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(onSelectPressed).toHaveBeenCalled();
|
||||||
expect(onOptionUpdated).not.toHaveBeenCalled();
|
expect(onOptionUpdated).not.toHaveBeenCalled();
|
||||||
// After clicking, plannedSelection="mic2" but selectedOption is still "mic1",
|
// After clicking, plannedSelection="mic2" but selectedOption is still "mic1",
|
||||||
// so a spinner should appear on the mic2 item
|
// so mic2 should be in an activating state
|
||||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
screen.getByRole("menuitemradio", {
|
||||||
expect(mic2Item.querySelector(".rotate")).toBeTruthy();
|
name: "Microphone 2 Activating…",
|
||||||
|
checked: false,
|
||||||
|
});
|
||||||
|
|
||||||
// The currently-selected mic1 item should not have a spinner
|
// The currently-selected mic1 item should not be activating
|
||||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
screen.getByRole("menuitemradio", {
|
||||||
expect(mic1Item.querySelector(".rotate")).toBeNull();
|
name: "Microphone 1",
|
||||||
|
checked: true,
|
||||||
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
// resolve the promise that acutally updates the select option.
|
// resolve the promise that acutally updates the select option.
|
||||||
resolve();
|
resolve();
|
||||||
@@ -284,7 +294,7 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
|
|
||||||
expect(onOptionUpdated).toHaveBeenCalled();
|
expect(onOptionUpdated).toHaveBeenCalled();
|
||||||
// Spinner should now be gone since the selection has caught up
|
// Spinner should now be gone since the selection has caught up
|
||||||
const mic2ItemAfter = screen.getByRole("menuitem", {
|
const mic2ItemAfter = screen.getByRole("menuitemradio", {
|
||||||
name: "Microphone 2",
|
name: "Microphone 2",
|
||||||
});
|
});
|
||||||
expect(mic2ItemAfter.querySelector(".rotate")).toBeNull();
|
expect(mic2ItemAfter.querySelector(".rotate")).toBeNull();
|
||||||
@@ -336,11 +346,15 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
await user.click(getByRole("button", { name: "Microphone" }));
|
await user.click(getByRole("button", { name: "Microphone" }));
|
||||||
|
|
||||||
// The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG
|
// 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);
|
expect(mic1Item.querySelectorAll("svg").length).toBe(2);
|
||||||
|
|
||||||
// The unselected item (mic1) only renders its IconOptions SVG
|
// 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);
|
expect(mic2Item.querySelectorAll("svg").length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
className={styles.itemIcon}
|
className={styles.itemIcon}
|
||||||
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -201,10 +202,23 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
onSelect?.(id);
|
onSelect?.(id);
|
||||||
}}
|
}}
|
||||||
key={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 && (
|
{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>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_e649de"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-busy="false"
|
aria-busy="false"
|
||||||
@@ -35,7 +35,7 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="Microphone"
|
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-kind="tertiary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import {
|
|||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
const logger = rootLogger.getChild("[MatrixKeyProvider]");
|
|
||||||
|
|
||||||
export class MatrixKeyProvider extends BaseKeyProvider {
|
export class MatrixKeyProvider extends BaseKeyProvider {
|
||||||
private rtcSession?: MatrixRTCSession;
|
private rtcSession?: MatrixRTCSession;
|
||||||
|
private logger: Logger;
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super({ ratchetWindowSize: 10, keyringSize: 256 });
|
super({ ratchetWindowSize: 10, keyringSize: 256 });
|
||||||
|
this.logger = rootLogger.getChild("[MatrixKeyProvider]");
|
||||||
}
|
}
|
||||||
|
|
||||||
public setRTCSession(rtcSession: MatrixRTCSession): void {
|
public setRTCSession(rtcSession: MatrixRTCSession): void {
|
||||||
@@ -60,12 +60,12 @@ export class MatrixKeyProvider extends BaseKeyProvider {
|
|||||||
encryptionKeyIndex,
|
encryptionKeyIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug(
|
this.logger.debug(
|
||||||
`Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${rtcBackendIdentity} (before hash: ${membershipParts.userId}:${membershipParts.deviceId}) encryptionKeyIndex=${encryptionKeyIndex}`,
|
`Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${rtcBackendIdentity} (before hash: ${membershipParts.userId}:${membershipParts.deviceId}) encryptionKeyIndex=${encryptionKeyIndex}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(e) => {
|
(e) => {
|
||||||
logger.error(
|
this.logger.error(
|
||||||
`Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipParts.userId}:${membershipParts.deviceId} encryptionKeyIndex=${encryptionKeyIndex}`,
|
`Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipParts.userId}:${membershipParts.deviceId} encryptionKeyIndex=${encryptionKeyIndex}`,
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 938 B After Width: | Height: | Size: 938 B |
@@ -1,48 +0,0 @@
|
|||||||
<svg width="1440" height="500" viewBox="0 0 1440 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g filter="url(#filter0_f_4162_80135)">
|
|
||||||
<circle cx="720" cy="1620" r="1500" fill="url(#paint0_linear_4162_80135)"/>
|
|
||||||
<circle cx="720" cy="1620" r="1498.92" stroke="white" stroke-opacity="0.5" stroke-width="2.16028" style="mix-blend-mode:overlay"/>
|
|
||||||
</g>
|
|
||||||
<g filter="url(#filter1_f_4162_80135)">
|
|
||||||
<circle cx="720" cy="1550.86" r="1272.86" fill="white"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_f_4162_80135" x="-900" y="0" width="3240" height="3240" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_4162_80135"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="filter1_f_4162_80135" x="-672.863" y="158" width="2785.73" height="2785.73" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_4162_80135"/>
|
|
||||||
</filter>
|
|
||||||
<linearGradient id="paint0_linear_4162_80135" x1="549.5" y1="120" x2="549.5" y2="505.5" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#062993"/>
|
|
||||||
<stop offset="0.040404" stop-color="#02389D"/>
|
|
||||||
<stop offset="0.0808081" stop-color="#0045A6"/>
|
|
||||||
<stop offset="0.121212" stop-color="#0051AD"/>
|
|
||||||
<stop offset="0.161616" stop-color="#005DB4"/>
|
|
||||||
<stop offset="0.20202" stop-color="#0069BA"/>
|
|
||||||
<stop offset="0.242424" stop-color="#0075BB"/>
|
|
||||||
<stop offset="0.282828" stop-color="#0081BB"/>
|
|
||||||
<stop offset="0.323232" stop-color="#008CB9"/>
|
|
||||||
<stop offset="0.363636" stop-color="#0098B7"/>
|
|
||||||
<stop offset="0.40404" stop-color="#00A3B3"/>
|
|
||||||
<stop offset="0.444444" stop-color="#00AEAD"/>
|
|
||||||
<stop offset="0.484848" stop-color="#00B8A4"/>
|
|
||||||
<stop offset="0.525253" stop-color="#00C2A0"/>
|
|
||||||
<stop offset="0.565657" stop-color="#00CC99"/>
|
|
||||||
<stop offset="0.606061" stop-color="#3AD396"/>
|
|
||||||
<stop offset="0.646465" stop-color="#5DD898"/>
|
|
||||||
<stop offset="0.686869" stop-color="#79DD99"/>
|
|
||||||
<stop offset="0.727273" stop-color="#92E29B"/>
|
|
||||||
<stop offset="0.767677" stop-color="#A8E69F"/>
|
|
||||||
<stop offset="0.808081" stop-color="#BBEAA5"/>
|
|
||||||
<stop offset="0.848485" stop-color="#CDEEAE"/>
|
|
||||||
<stop offset="0.888889" stop-color="#DCF2B9"/>
|
|
||||||
<stop offset="0.929293" stop-color="#EAF6C7"/>
|
|
||||||
<stop offset="0.969697" stop-color="#F5FBD5"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
86
src/graphics/mobile-gradient.svg
Normal file
86
src/graphics/mobile-gradient.svg
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="1200"
|
||||||
|
height="285"
|
||||||
|
viewBox="0 0 1200 285"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<g
|
||||||
|
filter="url(#filter0_f_3970_9366)"
|
||||||
|
id="g2"
|
||||||
|
transform="translate(422.00201)">
|
||||||
|
<path
|
||||||
|
d="m -164.342,495.134 c 188.2881,-188.288 494.684,-190 684.684,0"
|
||||||
|
stroke="url(#paint0_linear_3970_9366)"
|
||||||
|
stroke-width="235.517"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="path1"
|
||||||
|
style="stroke:url(#paint0_linear_3970_9366)" />
|
||||||
|
<path
|
||||||
|
d="m -164.342,495.134 c 188.2881,-188.288 494.684,-190 684.684,0"
|
||||||
|
stroke="url(#paint1_linear_3970_9366)"
|
||||||
|
style="mix-blend-mode:overlay;stroke:url(#paint1_linear_3970_9366)"
|
||||||
|
stroke-width="235.517"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="path2" />
|
||||||
|
</g>
|
||||||
|
<defs
|
||||||
|
id="defs5">
|
||||||
|
<filter
|
||||||
|
id="filter0_f_3970_9366"
|
||||||
|
x="-517.617"
|
||||||
|
y="-0.00012207"
|
||||||
|
width="1391.23"
|
||||||
|
height="848.409"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
color-interpolation-filters="sRGB">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="0"
|
||||||
|
result="BackgroundImageFix"
|
||||||
|
id="feFlood2" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="BackgroundImageFix"
|
||||||
|
result="shape"
|
||||||
|
id="feBlend2" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="117.758"
|
||||||
|
result="effect1_foregroundBlur_3970_9366"
|
||||||
|
id="feGaussianBlur2" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_3970_9366"
|
||||||
|
x1="349.17099"
|
||||||
|
y1="323.96301"
|
||||||
|
x2="6.82898"
|
||||||
|
y2="666.30499"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#0D5CBD"
|
||||||
|
id="stop2" />
|
||||||
|
<stop
|
||||||
|
offset="0.730863"
|
||||||
|
stop-color="#0DBDA8"
|
||||||
|
id="stop3" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint1_linear_3970_9366"
|
||||||
|
x1="349.17099"
|
||||||
|
y1="323.96301"
|
||||||
|
x2="6.82898"
|
||||||
|
y2="666.30499"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#0D5CBD"
|
||||||
|
id="stop4" />
|
||||||
|
<stop
|
||||||
|
offset="0.730863"
|
||||||
|
stop-color="#0DBDA8"
|
||||||
|
id="stop5" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -28,12 +28,6 @@ layer(compound);
|
|||||||
--font-size-title: calc(24px * var(--font-scale));
|
--font-size-title: calc(24px * var(--font-scale));
|
||||||
--font-size-headline: calc(32px * var(--font-scale));
|
--font-size-headline: calc(32px * var(--font-scale));
|
||||||
|
|
||||||
/* These colors are needed during the transitionary period between the old and
|
|
||||||
new Compound design systems, but should be removed ASAP */
|
|
||||||
--stopgap-color-on-solid-accent: var(--cpd-color-bg-canvas-default);
|
|
||||||
--stopgap-background-85: rgba(255, 255, 255, 0.85);
|
|
||||||
--stopgap-bgColor3: #444;
|
|
||||||
|
|
||||||
--cpd-color-border-accent: var(--cpd-color-green-800);
|
--cpd-color-border-accent: var(--cpd-color-green-800);
|
||||||
/* The distance to inset non-full-width content from the edge of the window
|
/* The distance to inset non-full-width content from the edge of the window
|
||||||
along the inline axis. This ramps up from 16px for typical mobile windows, to
|
along the inline axis. This ramps up from 16px for typical mobile windows, to
|
||||||
@@ -55,7 +49,6 @@ layer(compound);
|
|||||||
--small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
|
--small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
|
||||||
--big-drop-shadow: 0px 0px 24px 0px #1b1d221a;
|
--big-drop-shadow: 0px 0px 24px 0px #1b1d221a;
|
||||||
--subtle-drop-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
--subtle-drop-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||||
--background-gradient: url("graphics/backgroundGradient.svg");
|
|
||||||
|
|
||||||
--call-view-overlay-layer: 1;
|
--call-view-overlay-layer: 1;
|
||||||
--call-view-header-footer-layer: 2;
|
--call-view-header-footer-layer: 2;
|
||||||
@@ -74,9 +67,6 @@ layer(compound);
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--cpd-color-bg-canvas-default);
|
background-color: var(--cpd-color-bg-canvas-default);
|
||||||
background-size: calc(max(1440px, 100vw)) calc(max(800px, 100vh));
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
color: var(--cpd-color-text-primary);
|
color: var(--cpd-color-text-primary);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -85,6 +75,24 @@ body {
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-height: 330px) {
|
||||||
|
body[data-background="gradient"]::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-image: url("graphics/mobile-gradient.svg");
|
||||||
|
background-size: auto;
|
||||||
|
background-position: bottom;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-background="gradient"][data-platform="desktop"]::before {
|
||||||
|
background-image: url("graphics/desktop-gradient.svg");
|
||||||
|
background-size: calc(max(1440px, 100vw)) calc(max(800px, 100vh));
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* This prohibits the view to scroll for pages smaller than 122px in width
|
/* This prohibits the view to scroll for pages smaller than 122px in width
|
||||||
we use this for mobile pip webviews */
|
we use this for mobile pip webviews */
|
||||||
.no-scroll-body {
|
.no-scroll-body {
|
||||||
|
|||||||
@@ -173,14 +173,6 @@ Please see LICENSE in the repository root for full details.
|
|||||||
border-color: var(--cpd-color-border-disabled);
|
border-color: var(--cpd-color-border-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox svg {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox svg * {
|
|
||||||
stroke: var(--stopgap-color-on-solid-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
|
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
|
||||||
background: var(--cpd-color-text-action-accent);
|
background: var(--cpd-color-text-action-accent);
|
||||||
border-color: var(--cpd-color-text-action-accent);
|
border-color: var(--cpd-color-text-action-accent);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
AudioTrack,
|
AudioTrack,
|
||||||
type AudioTrackProps,
|
type AudioTrackProps,
|
||||||
} from "@livekit/components-react";
|
} from "@livekit/components-react";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { useEarpieceAudioConfig } from "../MediaDevicesContext";
|
import { useEarpieceAudioConfig } from "../MediaDevicesContext";
|
||||||
import { useReactiveState } from "../useReactiveState";
|
import { useReactiveState } from "../useReactiveState";
|
||||||
@@ -40,7 +40,6 @@ export interface MatrixAudioRendererProps {
|
|||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefixedLogger = logger.getChild("[MatrixAudioRenderer]");
|
|
||||||
/**
|
/**
|
||||||
* Takes care of handling remote participants’ audio tracks and makes sure that microphones and screen share are audible.
|
* Takes care of handling remote participants’ audio tracks and makes sure that microphones and screen share are audible.
|
||||||
*
|
*
|
||||||
@@ -60,6 +59,7 @@ export function LivekitRoomAudioRenderer({
|
|||||||
validIdentities,
|
validIdentities,
|
||||||
muted,
|
muted,
|
||||||
}: MatrixAudioRendererProps): ReactNode {
|
}: MatrixAudioRendererProps): ReactNode {
|
||||||
|
const logger = rootLogger.getChild("[MatrixAudioRenderer]");
|
||||||
const tracks = useTracks(
|
const tracks = useTracks(
|
||||||
[
|
[
|
||||||
Track.Source.Microphone,
|
Track.Source.Microphone,
|
||||||
@@ -80,7 +80,7 @@ export function LivekitRoomAudioRenderer({
|
|||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
// TODO make sure to also skip the warn logging for the local identity
|
// TODO make sure to also skip the warn logging for the local identity
|
||||||
// Log that there is an invalid identity, that means that someone is publishing audio that is not expected to be in the call.
|
// Log that there is an invalid identity, that means that someone is publishing audio that is not expected to be in the call.
|
||||||
prefixedLogger.warn(
|
logger.warn(
|
||||||
`Audio track ${ref.participant.identity} from ${url} has no matching matrix call member`,
|
`Audio track ${ref.participant.identity} from ${url} has no matching matrix call member`,
|
||||||
`current members: ${validIdentities.join()}`,
|
`current members: ${validIdentities.join()}`,
|
||||||
`track will not get rendered`,
|
`track will not get rendered`,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ProcessorWrapper,
|
ProcessorWrapper,
|
||||||
supportsBackgroundProcessors,
|
supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk,
|
||||||
type BackgroundOptions,
|
type BackgroundOptions,
|
||||||
} from "@livekit/track-processors";
|
} from "@livekit/track-processors";
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer";
|
import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer";
|
||||||
import { type Behavior } from "../state/Behavior";
|
import { type Behavior } from "../state/Behavior";
|
||||||
import { type ObservableScope } from "../state/ObservableScope";
|
import { type ObservableScope } from "../state/ObservableScope";
|
||||||
|
import { platform } from "../Platform";
|
||||||
|
|
||||||
//TODO-MULTI-SFU: This is not yet fully there.
|
//TODO-MULTI-SFU: This is not yet fully there.
|
||||||
// it is a combination of exposing observable and react hooks.
|
// it is a combination of exposing observable and react hooks.
|
||||||
@@ -106,6 +107,10 @@ interface Props {
|
|||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function supportsBackgroundProcessors(): boolean {
|
||||||
|
return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop";
|
||||||
|
}
|
||||||
|
|
||||||
export const ProcessorProvider: FC<Props> = ({ children }) => {
|
export const ProcessorProvider: FC<Props> = ({ children }) => {
|
||||||
// The setting the user wants to have
|
// The setting the user wants to have
|
||||||
const [blurActivated] = useSetting(backgroundBlurSettings);
|
const [blurActivated] = useSetting(backgroundBlurSettings);
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
exports[`RaisedHandIndicator > renders a smaller indicator when miniature is specified 1`] = `
|
exports[`RaisedHandIndicator > renders a smaller indicator when miniature is specified 1`] = `
|
||||||
<div
|
<div
|
||||||
class="reactionIndicatorWidget"
|
class="_reactionIndicatorWidget_abd277"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="reaction"
|
class="_reaction_abd277"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="Reaction"
|
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`] = `
|
exports[`RaisedHandIndicator > renders an indicator when a hand has been raised 1`] = `
|
||||||
<div
|
<div
|
||||||
class="reactionIndicatorWidget reactionIndicatorWidgetLarge"
|
class="_reactionIndicatorWidget_abd277 _reactionIndicatorWidgetLarge_abd277"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="reaction reactionLarge"
|
class="_reaction_abd277 _reactionLarge_abd277"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="Reaction"
|
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`] = `
|
exports[`RaisedHandIndicator > renders an indicator when a hand has been raised with the expected time 1`] = `
|
||||||
<div
|
<div
|
||||||
class="reactionIndicatorWidget reactionIndicatorWidgetLarge"
|
class="_reactionIndicatorWidget_abd277 _reactionIndicatorWidgetLarge_abd277"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="reaction reactionLarge"
|
class="_reaction_abd277 _reactionLarge_abd277"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="Reaction"
|
aria-label="Reaction"
|
||||||
|
|||||||
@@ -553,11 +553,9 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onError={
|
onError={(_error) => {
|
||||||
(/**error*/) => {
|
if (rtcSession.isJoined()) onLeft("error");
|
||||||
if (rtcSession.isJoined()) onLeft("error");
|
}}
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</GroupCallErrorBoundary>
|
</GroupCallErrorBoundary>
|
||||||
|
|||||||
@@ -14,6 +14,23 @@ Please see LICENSE in the repository root for full details.
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Normally the footer uses a transparent background to allow our expressive
|
||||||
|
page gradients to shine through. However, we sometimes need to visually separate
|
||||||
|
it from the content underneath. If the call layout is overflowing, or if the
|
||||||
|
spotlight tile is maximised and displaying video, apply a gradient background. */
|
||||||
|
.overflowing > .footer,
|
||||||
|
.fixedGrid:has(
|
||||||
|
> .tile[data-maximised="true"]
|
||||||
|
.spotlightItem[data-background="transparent"][data-video-enabled="true"][aria-hidden="false"]
|
||||||
|
)
|
||||||
|
~ .footer {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
var(--cpd-color-bg-canvas-default) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -82,19 +99,18 @@ Please see LICENSE in the repository root for full details.
|
|||||||
/* Disable pointer events so the overlay doesn't block interaction with
|
/* Disable pointer events so the overlay doesn't block interaction with
|
||||||
elements behind it */
|
elements behind it */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
|
||||||
|
|
||||||
.fixedGrid > :not(:first-child),
|
> :not(:first-child) {
|
||||||
.scrollingGrid > :not(:first-child) {
|
pointer-events: initial;
|
||||||
pointer-events: initial;
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
position: absolute;
|
|
||||||
inset-block-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile.maximised {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
afterEach,
|
|
||||||
beforeEach,
|
beforeEach,
|
||||||
describe,
|
describe,
|
||||||
expect,
|
expect,
|
||||||
@@ -15,12 +14,7 @@ import {
|
|||||||
type MockedFunction,
|
type MockedFunction,
|
||||||
vi,
|
vi,
|
||||||
} from "vitest";
|
} from "vitest";
|
||||||
import {
|
import { render, type RenderResult } from "@testing-library/react";
|
||||||
render,
|
|
||||||
type RenderResult,
|
|
||||||
getByRole,
|
|
||||||
screen,
|
|
||||||
} from "@testing-library/react";
|
|
||||||
import { type LocalParticipant } from "livekit-client";
|
import { type LocalParticipant } from "livekit-client";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
@@ -50,7 +44,6 @@ import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
|||||||
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
||||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
||||||
import { constant } from "../state/Behavior";
|
|
||||||
import { AppBar } from "../AppBar";
|
import { AppBar } from "../AppBar";
|
||||||
import { initializeWidget } from "../widget";
|
import { initializeWidget } from "../widget";
|
||||||
|
|
||||||
@@ -195,45 +188,6 @@ describe("InCallView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("settings button with AppBar header", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// getUrlParams() reads window.location directly rather than from the
|
|
||||||
// React Router context, so MemoryRouter alone is not enough to make
|
|
||||||
// it see "header=app_bar". Push the real URL so both paths agree.
|
|
||||||
window.history.pushState({}, "", "?header=app_bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
window.history.pushState({}, "", "/");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mobile portrait, is visible in the header", () => {
|
|
||||||
createInCallView({
|
|
||||||
withAppBar: true,
|
|
||||||
callViewModelOptions: {
|
|
||||||
// Narrow like a mobile phone in portrait orientation
|
|
||||||
windowSize$: constant({ width: 400, height: 700 }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
getByRole(screen.getByRole("banner"), "button", {
|
|
||||||
name: "Settings",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mobile landscape, is not visible anywhere", () => {
|
|
||||||
const { queryByRole } = createInCallView({
|
|
||||||
withAppBar: true,
|
|
||||||
callViewModelOptions: {
|
|
||||||
// Flat like a mobile phone in landscape orientation
|
|
||||||
windowSize$: constant({ width: 700, height: 400 }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(queryByRole("button", { name: "Settings" })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("audioOutputSwitcher", () => {
|
describe("audioOutputSwitcher", () => {
|
||||||
it("is visible and can be clicked", async () => {
|
it("is visible and can be clicked", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import {
|
|||||||
createCallViewModel$,
|
createCallViewModel$,
|
||||||
} from "../state/CallViewModel/CallViewModel.ts";
|
} from "../state/CallViewModel/CallViewModel.ts";
|
||||||
import { Grid, type TileProps } from "../grid/Grid";
|
import { Grid, type TileProps } from "../grid/Grid";
|
||||||
import { useInitial } from "../useInitial";
|
|
||||||
import { SpotlightTile } from "../tile/SpotlightTile";
|
import { SpotlightTile } from "../tile/SpotlightTile";
|
||||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
@@ -69,22 +68,24 @@ import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
|
|||||||
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
||||||
import { useMediaDevices } from "../MediaDevicesContext.ts";
|
import { useMediaDevices } from "../MediaDevicesContext.ts";
|
||||||
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
||||||
import { useAppBarHidden, useAppBarSecondaryButton } from "../AppBar.tsx";
|
import {
|
||||||
|
useAppBarHidden,
|
||||||
|
useAppBarSecondaryButton,
|
||||||
|
useAppBarSubtitle,
|
||||||
|
} from "../AppBar.tsx";
|
||||||
import { useBehavior } from "../useBehavior.ts";
|
import { useBehavior } from "../useBehavior.ts";
|
||||||
|
import { constant } from "../state/Behavior.ts";
|
||||||
import { Toast } from "../Toast.tsx";
|
import { Toast } from "../Toast.tsx";
|
||||||
import overlayStyles from "../Overlay.module.css";
|
import overlayStyles from "../Overlay.module.css";
|
||||||
import { prefetchSounds } from "../soundUtils";
|
|
||||||
import { useAudioContext } from "../useAudioContext";
|
|
||||||
import ringtoneMp3 from "../sound/ringtone.mp3?url";
|
|
||||||
import ringtoneOgg from "../sound/ringtone.ogg?url";
|
|
||||||
import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx";
|
import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx";
|
||||||
import { type Layout } from "../state/layout-types.ts";
|
import { type Layout } from "../state/layout-types.ts";
|
||||||
import { ObservableScope } from "../state/ObservableScope.ts";
|
import { ObservableScope } from "../state/ObservableScope.ts";
|
||||||
import { useLatest } from "../useLatest.ts";
|
|
||||||
import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx";
|
import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx";
|
||||||
import { SettingsIconButton } from "../button/Button.tsx";
|
import { SettingsIconButton } from "../button/Button.tsx";
|
||||||
import { createCallFooterViewModel } from "../components/CallFooterViewModel.tsx";
|
import { createCallFooterViewModel } from "../components/CallFooterViewModel.tsx";
|
||||||
import { type ViewModel } from "../state/ViewModel.ts";
|
import { type ViewModel } from "../state/ViewModel.ts";
|
||||||
|
import { RingingStatus } from "../tile/RingingStatus.tsx";
|
||||||
|
import { RingingAudioRenderer } from "./RingingAudioRenderer.tsx";
|
||||||
|
|
||||||
declare module "react" {
|
declare module "react" {
|
||||||
interface CSSProperties {
|
interface CSSProperties {
|
||||||
@@ -93,8 +94,6 @@ declare module "react" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = rootLogger.getChild("[InCallView]");
|
|
||||||
|
|
||||||
export interface ActiveCallProps extends Omit<
|
export interface ActiveCallProps extends Omit<
|
||||||
InCallViewProps,
|
InCallViewProps,
|
||||||
"vm" | "livekitRoom" | "connState" | "footerVm"
|
"vm" | "livekitRoom" | "connState" | "footerVm"
|
||||||
@@ -115,7 +114,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
const mediaDevices = useMediaDevices();
|
const mediaDevices = useMediaDevices();
|
||||||
const trackProcessorState$ = useTrackProcessorObservable$();
|
const trackProcessorState$ = useTrackProcessorObservable$();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.info("START CALL VIEW SCOPE");
|
rootLogger.info("START CALL VIEW SCOPE");
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
const reactionsReader = new ReactionsReader(scope, props.rtcSession);
|
const reactionsReader = new ReactionsReader(scope, props.rtcSession);
|
||||||
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
|
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
|
||||||
@@ -217,6 +216,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
muteStates,
|
muteStates,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
const logger = rootLogger.getChild("[InCallView]");
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { sendReaction, toggleRaisedHand } = useReactionsSender();
|
const { sendReaction, toggleRaisedHand } = useReactionsSender();
|
||||||
|
|
||||||
@@ -240,20 +240,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
const { showControls, header: headerStyle } = useUrlParams();
|
const { showControls, header: headerStyle } = useUrlParams();
|
||||||
|
|
||||||
const muteAllAudio = useBehavior(muteAllAudio$);
|
const muteAllAudio = useBehavior(muteAllAudio$);
|
||||||
|
|
||||||
// Preload a waiting and decline sounds
|
|
||||||
const pickupPhaseSoundCache = useInitial(async () => {
|
|
||||||
return prefetchSounds({
|
|
||||||
waiting: { mp3: ringtoneMp3, ogg: ringtoneOgg },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const pickupPhaseAudio = useAudioContext({
|
|
||||||
sounds: pickupPhaseSoundCache,
|
|
||||||
latencyHint: "interactive",
|
|
||||||
muted: muteAllAudio,
|
|
||||||
});
|
|
||||||
const latestPickupPhaseAudio = useLatest(pickupPhaseAudio);
|
|
||||||
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
||||||
const toggleVideo = useBehavior(muteStates.video.toggle$);
|
const toggleVideo = useBehavior(muteStates.video.toggle$);
|
||||||
const setAudioEnabled = useBehavior(muteStates.audio.setEnabled$);
|
const setAudioEnabled = useBehavior(muteStates.audio.setEnabled$);
|
||||||
@@ -266,12 +252,13 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
() => void toggleRaisedHand(),
|
() => void toggleRaisedHand(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ringingIntent = useBehavior(vm.ringingIntent$);
|
const ringingVm = useBehavior(vm.ringingVm$);
|
||||||
const audioParticipants = useBehavior(vm.livekitRoomItems$);
|
const audioParticipants = useBehavior(vm.livekitRoomItems$);
|
||||||
const participantCount = useBehavior(vm.participantCount$);
|
const participantCount = useBehavior(vm.participantCount$);
|
||||||
const reconnecting = useBehavior(vm.reconnecting$);
|
const reconnecting = useBehavior(vm.reconnecting$);
|
||||||
const layout = useBehavior(vm.layout$);
|
const layout = useBehavior(vm.layout$);
|
||||||
const edgeToEdge = useBehavior(vm.edgeToEdge$);
|
const edgeToEdge = useBehavior(vm.edgeToEdge$);
|
||||||
|
const overflowing = useBehavior(vm.overflowing$);
|
||||||
const showNameTags = useBehavior(vm.showNameTags$);
|
const showNameTags = useBehavior(vm.showNameTags$);
|
||||||
const showHeader = useBehavior(vm.showHeader$);
|
const showHeader = useBehavior(vm.showHeader$);
|
||||||
const settingsOpen = useBehavior(vm.settingsOpen$);
|
const settingsOpen = useBehavior(vm.settingsOpen$);
|
||||||
@@ -286,22 +273,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
throw fatalCallError;
|
throw fatalCallError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// While ringing, loop the ringtone
|
|
||||||
useEffect((): void | (() => void) => {
|
|
||||||
const audio = latestPickupPhaseAudio.current;
|
|
||||||
if (ringingIntent !== null && audio) {
|
|
||||||
const endSound = audio.playSoundLooping(
|
|
||||||
"waiting",
|
|
||||||
audio.soundDuration["waiting"] ?? 1,
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
void endSound().catch((e) => {
|
|
||||||
logger.error("Failed to stop ringing sound", e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [ringingIntent, latestPickupPhaseAudio]);
|
|
||||||
|
|
||||||
// iOS Safari doesn't reliably fire `click` on plain <div>s, so we listen
|
// iOS Safari doesn't reliably fire `click` on plain <div>s, so we listen
|
||||||
// for `pointerup` instead. Scrolls end in `pointercancel`, not `pointerup`,
|
// for `pointerup` instead. Scrolls end in `pointercancel`, not `pointerup`,
|
||||||
// so this still only fires for taps.
|
// so this still only fires for taps.
|
||||||
@@ -363,6 +334,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useAppBarHidden(!showHeader);
|
useAppBarHidden(!showHeader);
|
||||||
|
useAppBarSubtitle(
|
||||||
|
ringingVm && vm.ringingStatusLocation === "app_bar" && (
|
||||||
|
<RingingStatus vm={ringingVm} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let header: ReactNode = null;
|
let header: ReactNode = null;
|
||||||
switch (headerStyle) {
|
switch (headerStyle) {
|
||||||
@@ -457,6 +433,12 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
);
|
);
|
||||||
const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$);
|
const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$);
|
||||||
const showNameTags = useBehavior(vm.showNameTags$);
|
const showNameTags = useBehavior(vm.showNameTags$);
|
||||||
|
const showRingingStatus = vm.ringingStatusLocation === "tile";
|
||||||
|
const showOutline = useBehavior(
|
||||||
|
model instanceof GridTileViewModel
|
||||||
|
? model.showOutline$
|
||||||
|
: constant(false),
|
||||||
|
);
|
||||||
|
|
||||||
return model instanceof GridTileViewModel ? (
|
return model instanceof GridTileViewModel ? (
|
||||||
<GridTile
|
<GridTile
|
||||||
@@ -469,6 +451,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
style={style}
|
style={style}
|
||||||
showSpeakingIndicators={showSpeakingIndicators}
|
showSpeakingIndicators={showSpeakingIndicators}
|
||||||
showNameTags={showNameTags}
|
showNameTags={showNameTags}
|
||||||
|
showRingingStatus={showRingingStatus}
|
||||||
|
showOutline={showOutline}
|
||||||
focusable={!contentObscured}
|
focusable={!contentObscured}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -481,8 +465,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
targetHeight={targetHeight}
|
targetHeight={targetHeight}
|
||||||
showIndicators={showSpotlightIndicators}
|
showIndicators={showSpotlightIndicators}
|
||||||
showNameTags={showNameTags}
|
showNameTags={showNameTags}
|
||||||
|
showRingingStatus={showRingingStatus}
|
||||||
focusable={!contentObscured}
|
focusable={!contentObscured}
|
||||||
className={classNames(className, styles.tile)}
|
className={classNames(className, styles.tile)}
|
||||||
|
itemClassName={styles.spotlightItem}
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -507,7 +493,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
if (layout.type === "pip") {
|
if (layout.type === "pip") {
|
||||||
return (
|
return (
|
||||||
<SpotlightTile
|
<SpotlightTile
|
||||||
className={classNames(styles.tile, styles.maximised)}
|
className={styles.tile}
|
||||||
|
itemClassName={styles.spotlightItem}
|
||||||
|
data-maximised
|
||||||
vm={layout.spotlight}
|
vm={layout.spotlight}
|
||||||
expanded
|
expanded
|
||||||
onToggleExpanded={null}
|
onToggleExpanded={null}
|
||||||
@@ -515,6 +503,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
targetHeight={gridBounds.height}
|
targetHeight={gridBounds.height}
|
||||||
showIndicators={false}
|
showIndicators={false}
|
||||||
showNameTags={showNameTags}
|
showNameTags={showNameTags}
|
||||||
|
showRingingStatus={vm.ringingStatusLocation === "tile"}
|
||||||
focusable={!contentObscured}
|
focusable={!contentObscured}
|
||||||
aria-hidden={contentObscured}
|
aria-hidden={contentObscured}
|
||||||
/>
|
/>
|
||||||
@@ -598,7 +587,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
|
|
||||||
// Only hide the settings button if we have an AppBar header and we are showing the header
|
// Only hide the settings button if we have an AppBar header and we are showing the header
|
||||||
const footer = footerVm !== null && (
|
const footer = footerVm !== null && (
|
||||||
<CallFooter ref={footerRef} vm={footerVm} />
|
<CallFooter className={styles.footer} ref={footerRef} vm={footerVm} />
|
||||||
);
|
);
|
||||||
const allConnections = useBehavior(vm.allConnections$);
|
const allConnections = useBehavior(vm.allConnections$);
|
||||||
|
|
||||||
@@ -607,7 +596,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
// and the footer is also viewable by moving focus into it, so this is fine.
|
// and the footer is also viewable by moving focus into it, so this is fine.
|
||||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||||
<div
|
<div
|
||||||
className={styles.inRoom}
|
className={classNames(styles.inRoom, {
|
||||||
|
[styles.overflowing]: overflowing,
|
||||||
|
})}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onPointerUp={onViewPointerUp}
|
onPointerUp={onViewPointerUp}
|
||||||
onPointerMove={onPointerMove}
|
onPointerMove={onPointerMove}
|
||||||
@@ -626,6 +617,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
{renderContent()}
|
{renderContent()}
|
||||||
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||||
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||||
|
<RingingAudioRenderer vm={ringingVm} muted={muteAllAudio} />
|
||||||
{reconnectingToast}
|
{reconnectingToast}
|
||||||
{earpieceOverlay}
|
{earpieceOverlay}
|
||||||
<ReactionsOverlay vm={vm} />
|
<ReactionsOverlay vm={vm} />
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import { BrowserRouter } from "react-router-dom";
|
|||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import { axe } from "vitest-axe";
|
import { axe } from "vitest-axe";
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
@@ -20,6 +24,7 @@ import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
|||||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import lobbyStyles from "./LobbyView.module.css";
|
import lobbyStyles from "./LobbyView.module.css";
|
||||||
import headerStyles from "../Header.module.css";
|
import headerStyles from "../Header.module.css";
|
||||||
|
import { AppBar } from "../AppBar";
|
||||||
|
|
||||||
vi.mock("@livekit/components-react", () => ({
|
vi.mock("@livekit/components-react", () => ({
|
||||||
usePreviewTracks: (): unknown[] => [],
|
usePreviewTracks: (): unknown[] => [],
|
||||||
@@ -47,6 +52,13 @@ const mockClient = {
|
|||||||
getDeviceId: () => "DEVICE",
|
getDeviceId: () => "DEVICE",
|
||||||
} as Partial<MatrixClient> as MatrixClient;
|
} as Partial<MatrixClient> as MatrixClient;
|
||||||
|
|
||||||
|
const platformMock = vi.hoisted(() => vi.fn(() => "desktop"));
|
||||||
|
vi.mock("../Platform", () => ({
|
||||||
|
get platform(): string {
|
||||||
|
return platformMock();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const matrixInfo = {
|
const matrixInfo = {
|
||||||
userId: "@user:example.org",
|
userId: "@user:example.org",
|
||||||
displayName: "Test User",
|
displayName: "Test User",
|
||||||
@@ -60,25 +72,32 @@ const matrixInfo = {
|
|||||||
|
|
||||||
function renderLobbyView(
|
function renderLobbyView(
|
||||||
props: Partial<Parameters<typeof LobbyView>[0]> = {},
|
props: Partial<Parameters<typeof LobbyView>[0]> = {},
|
||||||
|
withAppBar = false,
|
||||||
|
platform = "android",
|
||||||
): ReturnType<typeof render> {
|
): ReturnType<typeof render> {
|
||||||
|
platformMock.mockReturnValue(platform);
|
||||||
const mediaDevices = mockMediaDevices({});
|
const mediaDevices = mockMediaDevices({});
|
||||||
const muteStates = mockMuteStates();
|
const muteStates = mockMuteStates();
|
||||||
|
const hideHeader = withAppBar ? true : false;
|
||||||
|
const lobbyView = (
|
||||||
|
<LobbyView
|
||||||
|
client={mockClient}
|
||||||
|
matrixInfo={matrixInfo}
|
||||||
|
muteStates={muteStates}
|
||||||
|
onEnter={() => {}}
|
||||||
|
confineToRoom={false}
|
||||||
|
hideHeader={hideHeader}
|
||||||
|
participantCount={3}
|
||||||
|
onShareClick={null}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
return render(
|
return render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<MediaDevicesContext value={mediaDevices}>
|
<MediaDevicesContext value={mediaDevices}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<LobbyView
|
{withAppBar && <AppBar>{lobbyView}</AppBar>}
|
||||||
client={mockClient}
|
{!withAppBar && lobbyView}
|
||||||
matrixInfo={matrixInfo}
|
|
||||||
muteStates={muteStates}
|
|
||||||
onEnter={() => {}}
|
|
||||||
confineToRoom={false}
|
|
||||||
hideHeader={false}
|
|
||||||
participantCount={3}
|
|
||||||
onShareClick={null}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</MediaDevicesContext>
|
</MediaDevicesContext>
|
||||||
</BrowserRouter>,
|
</BrowserRouter>,
|
||||||
@@ -97,9 +116,10 @@ describe("LobbyView", () => {
|
|||||||
|
|
||||||
it("renders without header", () => {
|
it("renders without header", () => {
|
||||||
const { container } = renderLobbyView({ hideHeader: true });
|
const { container } = renderLobbyView({ hideHeader: true });
|
||||||
expect(
|
const els = container.getElementsByClassName(headerStyles.header);
|
||||||
container.getElementsByClassName(headerStyles.header).length,
|
for (const el of els) {
|
||||||
).toBeFalsy();
|
expect(el).not.toBeVisible();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders with waiting for invite state", () => {
|
it("renders with waiting for invite state", () => {
|
||||||
@@ -108,4 +128,50 @@ describe("LobbyView", () => {
|
|||||||
});
|
});
|
||||||
expect(getByTestId("lobby_joinCall")).toHaveClass(lobbyStyles.wait);
|
expect(getByTestId("lobby_joinCall")).toHaveClass(lobbyStyles.wait);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders with AppBar android", async () => {
|
||||||
|
const { container, getByRole } = renderLobbyView(
|
||||||
|
{
|
||||||
|
waitingForInvite: true,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
"android",
|
||||||
|
);
|
||||||
|
getByRole("banner");
|
||||||
|
// Check that the primary button uses ArrowLeftIcon (the back/return icon),
|
||||||
|
// not the default CollapseIcon
|
||||||
|
const { container: iconContainer } = render(<ArrowLeftIcon />);
|
||||||
|
const expectedSvgPath = iconContainer
|
||||||
|
.querySelector("path")!
|
||||||
|
.getAttribute("d");
|
||||||
|
const primaryButtonSvgPath = container
|
||||||
|
.querySelector("path")
|
||||||
|
?.getAttribute("d");
|
||||||
|
expect(primaryButtonSvgPath).toBe(expectedSvgPath);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
expect(await axe(container)).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with AppBar ios", async () => {
|
||||||
|
const { container, getByRole } = renderLobbyView(
|
||||||
|
{
|
||||||
|
waitingForInvite: true,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
"ios",
|
||||||
|
);
|
||||||
|
getByRole("banner");
|
||||||
|
// Check that the primary button uses ArrowLeftIcon (the back/return icon),
|
||||||
|
// not the default CollapseIcon
|
||||||
|
const { container: iconContainer } = render(<ChevronLeftIcon />);
|
||||||
|
const expectedSvgPath = iconContainer
|
||||||
|
.querySelector("path")!
|
||||||
|
.getAttribute("d");
|
||||||
|
const primaryButtonSvgPath = container
|
||||||
|
.querySelector("path")
|
||||||
|
?.getAttribute("d");
|
||||||
|
expect(primaryButtonSvgPath).toBe(expectedSvgPath);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
expect(await axe(container)).toHaveNoViolations();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { CallFooter, type FooterSnapshot } from "../components/CallFooter";
|
|||||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||||
import { createLobbyFooterViewModel } from "../components/CallFooterViewModel";
|
import { createLobbyFooterViewModel } from "../components/CallFooterViewModel";
|
||||||
import { type ViewModel } from "../state/ViewModel";
|
import { type ViewModel } from "../state/ViewModel";
|
||||||
|
import { useAppBarPrimaryButtonIconKind } from "../AppBar";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
@@ -85,8 +86,9 @@ export const LobbyView: FC<Props> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
usePageTitle(matrixInfo.roomName);
|
|
||||||
|
|
||||||
|
usePageTitle(matrixInfo.roomName);
|
||||||
|
useAppBarPrimaryButtonIconKind("back");
|
||||||
const audioEnabled = useBehavior(muteStates.audio.enabled$);
|
const audioEnabled = useBehavior(muteStates.audio.enabled$);
|
||||||
const videoEnabled = useBehavior(muteStates.video.enabled$);
|
const videoEnabled = useBehavior(muteStates.video.enabled$);
|
||||||
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
||||||
|
|||||||
59
src/room/RingingAudioRenderer.test.tsx
Normal file
59
src/room/RingingAudioRenderer.test.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
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 { expect, type MockedFunction, test, vi } from "vitest";
|
||||||
|
import { act, render } from "@testing-library/react";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { useAudioContext } from "../useAudioContext";
|
||||||
|
import { createRingingMedia } from "../state/media/RingingMediaViewModel";
|
||||||
|
import { alice, aliceId } from "../utils/test-fixtures";
|
||||||
|
import { constant } from "../state/Behavior";
|
||||||
|
import { RingingAudioRenderer } from "./RingingAudioRenderer";
|
||||||
|
import { prefetchSounds } from "../soundUtils";
|
||||||
|
|
||||||
|
vi.mock("../useAudioContext");
|
||||||
|
vi.mock("../soundUtils");
|
||||||
|
|
||||||
|
test("ringtone plays on loop while ringing", () => {
|
||||||
|
(prefetchSounds as MockedFunction<typeof prefetchSounds>).mockResolvedValue({
|
||||||
|
sound: new ArrayBuffer(0),
|
||||||
|
});
|
||||||
|
const endSoundLooping = vi.fn().mockReturnValue(Promise.resolve());
|
||||||
|
const playSoundLooping = vi.fn().mockReturnValue(endSoundLooping);
|
||||||
|
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||||
|
playSound: vi.fn(),
|
||||||
|
playSoundLooping,
|
||||||
|
soundDuration: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickupState$ = new BehaviorSubject<"ringing" | "timeout" | "decline">(
|
||||||
|
"ringing",
|
||||||
|
);
|
||||||
|
const vm = createRingingMedia({
|
||||||
|
id: aliceId,
|
||||||
|
userId: alice.userId,
|
||||||
|
displayName$: constant("Alice"),
|
||||||
|
mxcAvatarUrl$: constant(undefined),
|
||||||
|
intent: "audio",
|
||||||
|
pickupState$,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Begin ringing
|
||||||
|
render(<RingingAudioRenderer vm={vm} muted={false} />);
|
||||||
|
expect(playSoundLooping).toHaveBeenCalledExactlyOnceWith(
|
||||||
|
"ringtone",
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
expect(endSoundLooping).not.toHaveBeenCalled();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// End ringing
|
||||||
|
act(() => pickupState$.next("decline"));
|
||||||
|
expect(playSoundLooping).not.toHaveBeenCalled();
|
||||||
|
expect(endSoundLooping).toHaveBeenCalledExactlyOnceWith();
|
||||||
|
});
|
||||||
72
src/room/RingingAudioRenderer.tsx
Normal file
72
src/room/RingingAudioRenderer.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
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 { useEffect, type FC } from "react";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
|
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
||||||
|
import { useBehavior } from "../useBehavior";
|
||||||
|
import { useInitial } from "../useInitial";
|
||||||
|
import { prefetchSounds } from "../soundUtils";
|
||||||
|
import ringtoneMp3 from "../sound/ringtone.mp3?url";
|
||||||
|
import ringtoneOgg from "../sound/ringtone.ogg?url";
|
||||||
|
import { type UseAudioContext, useAudioContext } from "../useAudioContext";
|
||||||
|
import { useLatest } from "../useLatest";
|
||||||
|
|
||||||
|
interface RingingAudioRendererProps {
|
||||||
|
vm: RingingMediaViewModel | null;
|
||||||
|
muted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RingingAudioRenderer: FC<RingingAudioRendererProps> = ({
|
||||||
|
vm,
|
||||||
|
muted,
|
||||||
|
}) => {
|
||||||
|
// Preload a waiting and decline sounds
|
||||||
|
const sounds = useInitial(async () => {
|
||||||
|
return prefetchSounds({
|
||||||
|
ringtone: { mp3: ringtoneMp3, ogg: ringtoneOgg },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const audio = useAudioContext({
|
||||||
|
sounds,
|
||||||
|
latencyHint: "interactive",
|
||||||
|
muted,
|
||||||
|
});
|
||||||
|
|
||||||
|
return vm && <ActiveRingingAudioRenderer vm={vm} audio={audio} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ActiveRingingAudioRendererProps {
|
||||||
|
vm: RingingMediaViewModel;
|
||||||
|
audio: UseAudioContext<"ringtone"> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActiveRingingAudioRenderer: FC<ActiveRingingAudioRendererProps> = ({
|
||||||
|
vm,
|
||||||
|
audio,
|
||||||
|
}) => {
|
||||||
|
const audio_ = useLatest(audio);
|
||||||
|
const pickupState = useBehavior(vm.pickupState$);
|
||||||
|
|
||||||
|
// While ringing, loop the ringtone
|
||||||
|
useEffect((): void | (() => void) => {
|
||||||
|
if (pickupState === "ringing" && audio_.current) {
|
||||||
|
const endSound = audio_.current.playSoundLooping(
|
||||||
|
"ringtone",
|
||||||
|
audio_.current.soundDuration["ringtone"] ?? 1,
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
void endSound().catch((e) => {
|
||||||
|
logger.error("Failed to stop ringing sound", e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [pickupState, audio_]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -3,17 +3,17 @@
|
|||||||
exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -95,20 +95,20 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' error correctly 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -253,20 +253,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' er
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed' error correctly 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -410,20 +410,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed'
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreachable' error correctly 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -567,20 +567,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreac
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFound' error correctly 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -724,20 +724,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFo
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' error correctly 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -881,20 +881,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' err
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`LiveKit ConnectionError variants > should link to troubleshoot guide when timeout error 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -1038,20 +1038,20 @@ exports[`LiveKit ConnectionError variants > should link to troubleshoot guide wh
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`should have a close button in widget mode 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -1195,20 +1195,20 @@ exports[`should have a close button in widget mode 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`should render the error page with link back to home 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -1349,20 +1349,20 @@ exports[`should render the error page with link back to home 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -1503,20 +1503,20 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`should report correct error for 'Connection lost' 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -1657,20 +1657,20 @@ exports[`should report correct error for 'Connection lost' 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`should report correct error for 'Homeserver does not support Matrix 2.…' 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -1815,20 +1815,20 @@ exports[`should report correct error for 'Homeserver does not support Matrix 2.
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -1969,20 +1969,20 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
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`] = `
|
exports[`should report correct error for 'Insufficient capacity' 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="page"
|
class="_page_4be5c0"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="Element Call Home"
|
aria-label="Element Call Home"
|
||||||
class="headerLogo"
|
class="_headerLogo_e4b327"
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
@@ -2118,20 +2118,20 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_4be5c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="error"
|
class="_error_a69dc5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,28 +3,28 @@
|
|||||||
exports[`InCallView > rendering > renders 1`] = `
|
exports[`InCallView > rendering > renders 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="inRoom"
|
class="_inRoom_4e7ff8"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header header"
|
class="_header_e4b327 _header_4e7ff8"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="roomHeaderInfo"
|
class="_roomHeaderInfo_e4b327"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label=""
|
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-color="1"
|
||||||
data-type="round"
|
data-type="round"
|
||||||
role="img"
|
role="img"
|
||||||
style="--cpd-avatar-size: 56px;"
|
style="--cpd-avatar-size: 56px;"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="nameLine"
|
class="_nameLine_e4b327"
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||||
@@ -35,7 +35,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-labelledby="_r_0_"
|
aria-labelledby="_r_0_"
|
||||||
class="lock"
|
class="_lock_edc97d"
|
||||||
data-encrypted="false"
|
data-encrypted="false"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="16"
|
height="16"
|
||||||
@@ -50,7 +50,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="participantsLine"
|
class="_participantsLine_e4b327"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Participants"
|
aria-label="Participants"
|
||||||
@@ -80,21 +80,21 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="scrollingGrid grid"
|
class="_scrollingGrid_4e7ff8 _grid_b0d1cd"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="layer"
|
class="_layer_b00c5f"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="container slot"
|
class="_container_b00c5f _slot_b0d1cd"
|
||||||
data-id="1"
|
data-id="1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="slot local slot"
|
class="_slot_b00c5f _local_b00c5f _slot_b0d1cd"
|
||||||
data-block-alignment="start"
|
data-block-alignment="start"
|
||||||
data-id="0"
|
data-id="0"
|
||||||
data-inline-alignment="end"
|
data-inline-alignment="end"
|
||||||
@@ -103,22 +103,22 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fixedGrid grid"
|
class="_fixedGrid_4e7ff8 _grid_b0d1cd"
|
||||||
style="inset-block-start: NaNpx;"
|
style="inset-block-start: NaNpx;"
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="bg animate"
|
class="_bg_2f5303 _animate_2f5303"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="overlay"
|
class="_overlay_eb6724"
|
||||||
data-show="false"
|
data-show="false"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_big-icon_1ssbv_8 icon"
|
class="_big-icon_1ssbv_8 _icon_eb6724"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
>
|
>
|
||||||
@@ -157,22 +157,22 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
Back to Speaker Mode
|
Back to Speaker Mode
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="spacer"
|
class="_spacer_eb6724"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="_container_8084b5"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="footer"
|
class="_footer_4e7ff8 _footer_20b7b4"
|
||||||
data-testid="footer-container"
|
data-testid="footer-container"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="settingsLogoContainer"
|
class="_settingsLogoContainer_20b7b4"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_8_"
|
aria-labelledby="_r_8_"
|
||||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||||
data-kind="secondary"
|
data-kind="secondary"
|
||||||
data-testid="settings-bottom-left"
|
data-testid="settings-bottom-left"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -198,7 +198,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="logo"
|
class="_logo_20b7b4"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -303,11 +303,11 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="buttons"
|
class="_buttons_20b7b4"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_d_"
|
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-kind="secondary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-testid="settings-bottom-center"
|
data-testid="settings-bottom-center"
|
||||||
@@ -382,7 +382,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby="_r_s_"
|
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-kind="secondary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -405,7 +405,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_14_"
|
aria-labelledby="_r_14_"
|
||||||
class="_button_1nw83_8 endCall _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-testid="incall_leave"
|
data-testid="incall_leave"
|
||||||
@@ -428,7 +428,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-label="Layout"
|
aria-label="Layout"
|
||||||
class="_toggle_13rnk_9 layout"
|
class="_toggle_13rnk_9 _layout_20b7b4"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,23 +1,485 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`LobbyView > renders with AppBar android 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="_bar_221541"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_36_"
|
||||||
|
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||||
|
data-kind="primary"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.207 5.293a1 1 0 0 1 0 1.414L7.914 11H18.5a1 1 0 1 1 0 2H7.914l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6a1 1 0 0 1 0-1.414l6-6a1 1 0 0 1 1.414 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="_secondaryButton_221541"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_inRoom_4e7ff8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_f9ee84"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_preview_dd2178"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
disablepictureinpicture=""
|
||||||
|
playsinline=""
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="_avatarContainer_dd2178"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
aria-label="@user:example.org"
|
||||||
|
class="_avatar_va14e_8 _avatar-imageless_va14e_55"
|
||||||
|
data-color="6"
|
||||||
|
data-type="round"
|
||||||
|
role="img"
|
||||||
|
style="--cpd-avatar-size: NaNpx;"
|
||||||
|
>
|
||||||
|
T
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_buttonBar_dd2178"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="true"
|
||||||
|
class="_button_1nw83_8 _join_f9ee84 _wait_f9ee84"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="md"
|
||||||
|
data-testid="lobby_joinCall"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Join call
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="_link_k9ljz_8"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="md"
|
||||||
|
href="/"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
Back to recents
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_footer_20b7b4"
|
||||||
|
data-testid="footer-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_settingsLogoContainer_20b7b4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_3c_"
|
||||||
|
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||||
|
data-kind="secondary"
|
||||||
|
data-testid="settings-bottom-left"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 20q-.825 0-1.412-.587A1.93 1.93 0 0 1 10 18q0-.824.588-1.413A1.93 1.93 0 0 1 12 16q.825 0 1.412.587Q14 17.176 14 18t-.588 1.413A1.93 1.93 0 0 1 12 20m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 6q0-.824.588-1.412A1.93 1.93 0 0 1 12 4q.825 0 1.412.588Q14 5.175 14 6q0 .824-.588 1.412A1.93 1.93 0 0 1 12 8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_buttons_20b7b4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_3h_"
|
||||||
|
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"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 20q-.825 0-1.412-.587A1.93 1.93 0 0 1 10 18q0-.824.588-1.413A1.93 1.93 0 0 1 12 16q.825 0 1.412.587Q14 17.176 14 18t-.588 1.413A1.93 1.93 0 0 1 12 20m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 6q0-.824.588-1.412A1.93 1.93 0 0 1 12 4q.825 0 1.412.588Q14 5.175 14 6q0 .824-.588 1.412A1.93 1.93 0 0 1 12 8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-busy="false"
|
||||||
|
aria-checked="false"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-labelledby="_r_3m_"
|
||||||
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
data-testid="incall_mute"
|
||||||
|
role="switch"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 8v-.006l6.831 6.832-.002.002 1.414 1.415.003-.003 1.414 1.414-.003.003L20.5 20.5a1 1 0 0 1-1.414 1.414l-3.022-3.022A7.95 7.95 0 0 1 13 19.938V21a1 1 0 0 1-2 0v-1.062A8 8 0 0 1 4 12a1 1 0 1 1 2 0 6 6 0 0 0 8.587 5.415l-1.55-1.55A4.005 4.005 0 0 1 8 12v-1.172L2.086 4.914A1 1 0 0 1 3.5 3.5zm9.417 6.583 1.478 1.477A7.96 7.96 0 0 0 20 12a1 1 0 0 0-2 0c0 .925-.21 1.8-.583 2.583M8.073 5.238l7.793 7.793q.132-.495.134-1.031V6a4 4 0 0 0-7.927-.762"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-busy="false"
|
||||||
|
aria-checked="false"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-labelledby="_r_3r_"
|
||||||
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
data-testid="incall_videomute"
|
||||||
|
role="switch"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.747 2.753 4.35 4.355l.007-.003L18 17.994v.012l3.247 3.247a1 1 0 0 1-1.414 1.414l-2.898-2.898A2 2 0 0 1 16 20H6a4 4 0 0 1-4-4V8c0-.892.292-1.715.785-2.38L1.333 4.166a1 1 0 0 1 1.414-1.414M18 15.166 6.834 4H16a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_40_"
|
||||||
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
data-testid="incall_leave"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m2.765 16.02-2.47-2.416A1.02 1.02 0 0 1 0 12.852q0-.456.295-.751a15.6 15.6 0 0 1 5.316-3.786A15.9 15.9 0 0 1 12 7q3.355 0 6.39 1.329a16 16 0 0 1 5.315 3.772q.295.294.295.751t-.295.752l-2.47 2.416a1.047 1.047 0 0 1-1.396.108l-3.114-2.363a1.1 1.1 0 0 1-.322-.376 1.1 1.1 0 0 1-.108-.483v-2.27a13.6 13.6 0 0 0-2.12-.524C13.459 9.996 12 9.937 12 9.937s-1.459.059-2.174.175q-1.074.174-2.121.523v2.271q0 .268-.108.483a1.1 1.1 0 0 1-.322.376l-3.114 2.363a1.047 1.047 0 0 1-1.396-.107"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`LobbyView > renders with AppBar ios 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="_bar_221541"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_4a_"
|
||||||
|
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||||
|
data-kind="primary"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="_secondaryButton_221541"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_inRoom_4e7ff8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_f9ee84"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_preview_dd2178"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
disablepictureinpicture=""
|
||||||
|
playsinline=""
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="_avatarContainer_dd2178"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
aria-label="@user:example.org"
|
||||||
|
class="_avatar_va14e_8 _avatar-imageless_va14e_55"
|
||||||
|
data-color="6"
|
||||||
|
data-type="round"
|
||||||
|
role="img"
|
||||||
|
style="--cpd-avatar-size: NaNpx;"
|
||||||
|
>
|
||||||
|
T
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_buttonBar_dd2178"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="true"
|
||||||
|
class="_button_1nw83_8 _join_f9ee84 _wait_f9ee84"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="md"
|
||||||
|
data-testid="lobby_joinCall"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Join call
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="_link_k9ljz_8"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="md"
|
||||||
|
href="/"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
Back to recents
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_footer_20b7b4"
|
||||||
|
data-testid="footer-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_settingsLogoContainer_20b7b4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_4g_"
|
||||||
|
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||||
|
data-kind="secondary"
|
||||||
|
data-testid="settings-bottom-left"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_buttons_20b7b4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_4l_"
|
||||||
|
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"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-busy="false"
|
||||||
|
aria-checked="false"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-labelledby="_r_4q_"
|
||||||
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
data-testid="incall_mute"
|
||||||
|
role="switch"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 8v-.006l6.831 6.832-.002.002 1.414 1.415.003-.003 1.414 1.414-.003.003L20.5 20.5a1 1 0 0 1-1.414 1.414l-3.022-3.022A7.95 7.95 0 0 1 13 19.938V21a1 1 0 0 1-2 0v-1.062A8 8 0 0 1 4 12a1 1 0 1 1 2 0 6 6 0 0 0 8.587 5.415l-1.55-1.55A4.005 4.005 0 0 1 8 12v-1.172L2.086 4.914A1 1 0 0 1 3.5 3.5zm9.417 6.583 1.478 1.477A7.96 7.96 0 0 0 20 12a1 1 0 0 0-2 0c0 .925-.21 1.8-.583 2.583M8.073 5.238l7.793 7.793q.132-.495.134-1.031V6a4 4 0 0 0-7.927-.762"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-busy="false"
|
||||||
|
aria-checked="false"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-labelledby="_r_4v_"
|
||||||
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
data-testid="incall_videomute"
|
||||||
|
role="switch"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.747 2.753 4.35 4.355l.007-.003L18 17.994v.012l3.247 3.247a1 1 0 0 1-1.414 1.414l-2.898-2.898A2 2 0 0 1 16 20H6a4 4 0 0 1-4-4V8c0-.892.292-1.715.785-2.38L1.333 4.166a1 1 0 0 1 1.414-1.414M18 15.166 6.834 4H16a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-labelledby="_r_54_"
|
||||||
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
data-testid="incall_leave"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m2.765 16.02-2.47-2.416A1.02 1.02 0 0 1 0 12.852q0-.456.295-.751a15.6 15.6 0 0 1 5.316-3.786A15.9 15.9 0 0 1 12 7q3.355 0 6.39 1.329a16 16 0 0 1 5.315 3.772q.295.294.295.751t-.295.752l-2.47 2.416a1.047 1.047 0 0 1-1.396.108l-3.114-2.363a1.1 1.1 0 0 1-.322-.376 1.1 1.1 0 0 1-.108-.483v-2.27a13.6 13.6 0 0 0-2.12-.524C13.459 9.996 12 9.937 12 9.937s-1.459.059-2.174.175q-1.074.174-2.121.523v2.271q0 .268-.108.483a1.1 1.1 0 0 1-.322.376l-3.114 2.363a1.047 1.047 0 0 1-1.396-.107"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`LobbyView > renders with header and participant count 1`] = `
|
exports[`LobbyView > renders with header and participant count 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="inRoom"
|
class="_inRoom_4e7ff8"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="header"
|
class="_header_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav leftNav"
|
class="_nav_e4b327 _leftNav_e4b327"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="roomHeaderInfo"
|
class="_roomHeaderInfo_e4b327"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="!room:example.org"
|
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-color="3"
|
||||||
data-type="round"
|
data-type="round"
|
||||||
role="img"
|
role="img"
|
||||||
@@ -26,7 +488,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
T
|
T
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
class="nameLine"
|
class="_nameLine_e4b327"
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||||
@@ -39,7 +501,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-labelledby="_r_0_"
|
aria-labelledby="_r_0_"
|
||||||
class="lock"
|
class="_lock_edc97d"
|
||||||
data-encrypted="false"
|
data-encrypted="false"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="16"
|
height="16"
|
||||||
@@ -54,7 +516,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="participantsLine"
|
class="_participantsLine_e4b327"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Participants"
|
aria-label="Participants"
|
||||||
@@ -84,14 +546,14 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="nav rightNav"
|
class="_nav_e4b327 _rightNav_e4b327"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="_content_f9ee84"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="preview"
|
class="_preview_dd2178"
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
disablepictureinpicture=""
|
disablepictureinpicture=""
|
||||||
@@ -99,7 +561,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="avatarContainer"
|
class="_avatarContainer_dd2178"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
@@ -115,10 +577,10 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="buttonBar"
|
class="_buttonBar_dd2178"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="_button_1nw83_8 join"
|
class="_button_1nw83_8 _join_f9ee84"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-testid="lobby_joinCall"
|
data-testid="lobby_joinCall"
|
||||||
@@ -140,15 +602,15 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="footer"
|
class="_footer_20b7b4"
|
||||||
data-testid="footer-container"
|
data-testid="footer-container"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="settingsLogoContainer"
|
class="_settingsLogoContainer_20b7b4"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_6_"
|
aria-labelledby="_r_6_"
|
||||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||||
data-kind="secondary"
|
data-kind="secondary"
|
||||||
data-testid="settings-bottom-left"
|
data-testid="settings-bottom-left"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -168,13 +630,13 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
d="M12 20q-.825 0-1.412-.587A1.93 1.93 0 0 1 10 18q0-.824.588-1.413A1.93 1.93 0 0 1 12 16q.825 0 1.412.587Q14 17.176 14 18t-.588 1.413A1.93 1.93 0 0 1 12 20m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 6q0-.824.588-1.412A1.93 1.93 0 0 1 12 4q.825 0 1.412.588Q14 5.175 14 6q0 .824-.588 1.412A1.93 1.93 0 0 1 12 8"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="logo"
|
class="_logo_20b7b4"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -279,11 +741,11 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="buttons"
|
class="_buttons_20b7b4"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_b_"
|
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-kind="secondary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-testid="settings-bottom-center"
|
data-testid="settings-bottom-center"
|
||||||
@@ -299,7 +761,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
d="M12 20q-.825 0-1.412-.587A1.93 1.93 0 0 1 10 18q0-.824.588-1.413A1.93 1.93 0 0 1 12 16q.825 0 1.412.587Q14 17.176 14 18t-.588 1.413A1.93 1.93 0 0 1 12 20m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m0-6q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 6q0-.824.588-1.412A1.93 1.93 0 0 1 12 4q.825 0 1.412.588Q14 5.175 14 6q0 .824-.588 1.412A1.93 1.93 0 0 1 12 8"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -355,7 +817,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_q_"
|
aria-labelledby="_r_q_"
|
||||||
class="_button_1nw83_8 endCall _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-testid="incall_leave"
|
data-testid="incall_leave"
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
Device ID: DEVICE123
|
Device ID: DEVICE123
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="fieldRow"
|
class="_fieldRow_1bd8c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="field inputField"
|
class="_field_1bd8c0 _inputField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_1_"
|
aria-describedby="_r_1_"
|
||||||
@@ -38,10 +38,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fieldRow"
|
class="_fieldRow_1bd8c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="field checkboxField"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_2_"
|
aria-describedby="_r_2_"
|
||||||
@@ -52,7 +52,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
for="debugTileLayout"
|
for="debugTileLayout"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="checkbox"
|
class="_checkbox_1bd8c0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -75,10 +75,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fieldRow"
|
class="_fieldRow_1bd8c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="field checkboxField"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_3_"
|
aria-describedby="_r_3_"
|
||||||
@@ -89,7 +89,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
for="showConnectionStats"
|
for="showConnectionStats"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="checkbox"
|
class="_checkbox_1bd8c0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -112,10 +112,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fieldRow"
|
class="_fieldRow_1bd8c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="field checkboxField"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_4_"
|
aria-describedby="_r_4_"
|
||||||
@@ -126,7 +126,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
for="muteAllAudio"
|
for="muteAllAudio"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="checkbox"
|
class="_checkbox_1bd8c0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -150,10 +150,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="fieldRow"
|
class="_fieldRow_1bd8c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="field checkboxField"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_5_"
|
aria-describedby="_r_5_"
|
||||||
@@ -164,7 +164,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
for="alwaysShowIphoneEarpiece"
|
for="alwaysShowIphoneEarpiece"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="checkbox"
|
class="_checkbox_1bd8c0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -187,10 +187,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fieldRow"
|
class="_fieldRow_1bd8c0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="field checkboxField"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_6_"
|
aria-describedby="_r_6_"
|
||||||
@@ -201,7 +201,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
for="enableLivekitExtendedLogs"
|
for="enableLivekitExtendedLogs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="checkbox"
|
class="_checkbox_1bd8c0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -274,7 +274,6 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
||||||
checked=""
|
|
||||||
class="_input_1ug7n_18"
|
class="_input_1ug7n_18"
|
||||||
id="radix-_r_9_"
|
id="radix-_r_9_"
|
||||||
name="_r_0_"
|
name="_r_0_"
|
||||||
@@ -315,6 +314,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
||||||
|
checked=""
|
||||||
class="_input_1ug7n_18"
|
class="_input_1ug7n_18"
|
||||||
id="radix-_r_b_"
|
id="radix-_r_b_"
|
||||||
name="_r_0_"
|
name="_r_0_"
|
||||||
@@ -386,7 +386,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
class="livekit_room_box"
|
class="_livekit_room_box_2ddec4"
|
||||||
>
|
>
|
||||||
<h4>
|
<h4>
|
||||||
LiveKit SFU: wss://local-sfu.example.org
|
LiveKit SFU: wss://local-sfu.example.org
|
||||||
@@ -427,7 +427,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
<ul />
|
<ul />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="livekit_room_box"
|
class="_livekit_room_box_2ddec4"
|
||||||
>
|
>
|
||||||
<h4>
|
<h4>
|
||||||
LiveKit SFU: wss://remote-sfu.example.org
|
LiveKit SFU: wss://remote-sfu.example.org
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ declare global {
|
|||||||
// eslint-disable-next-line no-var, camelcase
|
// eslint-disable-next-line no-var, camelcase
|
||||||
var mx_rage_initStoragePromise: Promise<void> | undefined;
|
var mx_rage_initStoragePromise: Promise<void> | undefined;
|
||||||
}
|
}
|
||||||
|
export let rageshakeLogger: Logger;
|
||||||
/**
|
/**
|
||||||
* Configure rage shaking support for sending bug reports.
|
* Configure rage shaking support for sending bug reports.
|
||||||
* Modifies globals.
|
* Modifies globals.
|
||||||
@@ -477,7 +477,8 @@ export async function init(): Promise<void> {
|
|||||||
global.mx_rage_logger = new ConsoleLogger();
|
global.mx_rage_logger = new ConsoleLogger();
|
||||||
|
|
||||||
// configure loglevel based loggers:
|
// configure loglevel based loggers:
|
||||||
setLogExtension(logger, global.mx_rage_logger.log);
|
rageshakeLogger = logger;
|
||||||
|
setLogExtension(rageshakeLogger, global.mx_rage_logger.log);
|
||||||
|
|
||||||
// intercept console logging so that we can get matrix_sdk logs:
|
// intercept console logging so that we can get matrix_sdk logs:
|
||||||
// this is nasty, but no logging hooks are provided
|
// this is nasty, but no logging hooks are provided
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const enableExtendedLivekitLogs = new Setting<boolean>(
|
|||||||
|
|
||||||
export const matrixRTCMode = new Setting<MatrixRTCMode>(
|
export const matrixRTCMode = new Setting<MatrixRTCMode>(
|
||||||
"matrix-rtc-mode",
|
"matrix-rtc-mode",
|
||||||
MatrixRTCMode.Legacy,
|
MatrixRTCMode.Compatibility,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const customLivekitUrl = new Setting<string | null>(
|
export const customLivekitUrl = new Setting<string | null>(
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ describe("Test mappings", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Test select a device", () => {
|
describe("Test select a device", () => {
|
||||||
it(`Switch to correct device `, () => {
|
it(`Switch to correct device`, () => {
|
||||||
withTestScheduler(({ cold, schedule, expectObservable, flush }) => {
|
withTestScheduler(({ cold, schedule, expectObservable, flush }) => {
|
||||||
const controlledAudioOutput = new AndroidControlledAudioOutput(
|
const controlledAudioOutput = new AndroidControlledAudioOutput(
|
||||||
cold("a", { a: FULL_DEVICE_LIST }),
|
cold("a", { a: FULL_DEVICE_LIST }),
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ import { type Behavior } from "../Behavior";
|
|||||||
import { type Epoch, type ObservableScope } from "../ObservableScope";
|
import { type Epoch, type ObservableScope } from "../ObservableScope";
|
||||||
import { type RoomMemberMap } from "./remoteMembers/MatrixMemberMetadata";
|
import { type RoomMemberMap } from "./remoteMembers/MatrixMemberMetadata";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("[CallNotificationLifecycle]");
|
|
||||||
|
|
||||||
export type AutoLeaveReason = "allOthersLeft" | "timeout" | "decline";
|
export type AutoLeaveReason = "allOthersLeft" | "timeout" | "decline";
|
||||||
|
|
||||||
export interface RingAttempt {
|
export interface RingAttempt {
|
||||||
@@ -114,6 +112,7 @@ export function createCallNotificationLifecycle$({
|
|||||||
*/
|
*/
|
||||||
autoLeave$: Observable<AutoLeaveReason>;
|
autoLeave$: Observable<AutoLeaveReason>;
|
||||||
} {
|
} {
|
||||||
|
const logger = rootLogger.getChild("[CallNotificationLifecycle]");
|
||||||
let ringAttempts$: Observable<RingAttempt> = NEVER;
|
let ringAttempts$: Observable<RingAttempt> = NEVER;
|
||||||
if (options.waitForCallPickup)
|
if (options.waitForCallPickup)
|
||||||
ringAttempts$ = sentCallNotification$.pipe(
|
ringAttempts$ = sentCallNotification$.pipe(
|
||||||
@@ -161,6 +160,10 @@ export function createCallNotificationLifecycle$({
|
|||||||
recipient,
|
recipient,
|
||||||
outcome$: race(timeout$, accept$, decline$).pipe(
|
outcome$: race(timeout$, accept$, decline$).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
|
// Make this observable 'hot' to avoid running multiple timers. This
|
||||||
|
// is not actually a resource leak since there will be at most one
|
||||||
|
// active ring attempt at any given time.
|
||||||
|
// eslint-disable-next-line element-call/no-observablescope-leak
|
||||||
scope.share,
|
scope.share,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { SyncState } from "matrix-js-sdk";
|
|||||||
import {
|
import {
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
type LocalTrackPublication,
|
type LocalTrackPublication,
|
||||||
|
type Participant,
|
||||||
type RemoteParticipant,
|
type RemoteParticipant,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import * as ComponentsCore from "@livekit/components-core";
|
import * as ComponentsCore from "@livekit/components-core";
|
||||||
@@ -1031,6 +1032,10 @@ describe.each([
|
|||||||
a: [localRtcMember],
|
a: [localRtcMember],
|
||||||
b: [localRtcMember, aliceRtcMember],
|
b: [localRtcMember, aliceRtcMember],
|
||||||
}),
|
}),
|
||||||
|
videoEnabled: new Map<Participant, Behavior<boolean>>([
|
||||||
|
[localParticipant, constant(true)],
|
||||||
|
[aliceParticipant, constant(true)],
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
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", () => {
|
test("spotlight remembers whether it's expanded", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
// Start in spotlight mode, then switch to grid and back to spotlight a
|
// Start in spotlight mode, then switch to grid and back to spotlight a
|
||||||
@@ -1096,7 +1128,7 @@ describe.each([
|
|||||||
b: {
|
b: {
|
||||||
type: "spotlight-expanded",
|
type: "spotlight-expanded",
|
||||||
spotlight: [`${aliceId}:0`],
|
spotlight: [`${aliceId}:0`],
|
||||||
pip: `${localId}:0`,
|
pip: undefined,
|
||||||
},
|
},
|
||||||
c: {
|
c: {
|
||||||
type: "grid",
|
type: "grid",
|
||||||
@@ -1420,10 +1452,13 @@ describe.each([
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should ring for 30ms and then time out
|
expectObservable(vm.ringingVm$).toBe("(ab)", {
|
||||||
expectObservable(vm.ringingIntent$).toBe("(ab) 26ms a", {
|
|
||||||
a: null,
|
a: null,
|
||||||
b: "audio",
|
b: expect.objectContaining({
|
||||||
|
type: "ringing",
|
||||||
|
userId: alice.userId,
|
||||||
|
intent: "audio",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
// Layout should show placeholder media for the participant we're
|
// Layout should show placeholder media for the participant we're
|
||||||
// ringing the entire time (even once timed out)
|
// ringing the entire time (even once timed out)
|
||||||
@@ -1463,9 +1498,13 @@ describe.each([
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Should ring until Alice joins
|
// Should ring until Alice joins
|
||||||
expectObservable(vm.ringingIntent$).toBe("(ab) 17ms a", {
|
expectObservable(vm.ringingVm$).toBe("(ab) 17ms a", {
|
||||||
a: null,
|
a: null,
|
||||||
b: "audio",
|
b: expect.objectContaining({
|
||||||
|
type: "ringing",
|
||||||
|
userId: alice.userId,
|
||||||
|
intent: "audio",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
// Layout should show placeholder media for the participant we're
|
// Layout should show placeholder media for the participant we're
|
||||||
// ringing the entire time
|
// ringing the entire time
|
||||||
|
|||||||
@@ -39,12 +39,10 @@ import {
|
|||||||
throttleTime,
|
throttleTime,
|
||||||
timer,
|
timer,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
concat,
|
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import {
|
import {
|
||||||
MembershipManagerEvent,
|
MembershipManagerEvent,
|
||||||
type RTCCallIntent,
|
|
||||||
type LivekitTransportConfig,
|
type LivekitTransportConfig,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
@@ -157,8 +155,8 @@ import {
|
|||||||
createRingingMedia,
|
createRingingMedia,
|
||||||
type RingingMediaViewModel,
|
type RingingMediaViewModel,
|
||||||
} from "../media/RingingMediaViewModel.ts";
|
} from "../media/RingingMediaViewModel.ts";
|
||||||
|
import { type GridTileViewModel } from "../TileViewModel.ts";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("[CallViewModel]");
|
|
||||||
//TODO
|
//TODO
|
||||||
// Larger rename
|
// Larger rename
|
||||||
// member,membership -> rtcMember
|
// member,membership -> rtcMember
|
||||||
@@ -209,6 +207,7 @@ export type WindowMode = "normal" | "narrow" | "flat" | "pip";
|
|||||||
|
|
||||||
interface LayoutScanState {
|
interface LayoutScanState {
|
||||||
layout: Layout | null;
|
layout: Layout | null;
|
||||||
|
overflowing: boolean;
|
||||||
tiles: TileStore;
|
tiles: TileStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,9 +230,13 @@ export interface CallViewModel {
|
|||||||
// lifecycle
|
// lifecycle
|
||||||
autoLeave$: Observable<AutoLeaveReason>;
|
autoLeave$: Observable<AutoLeaveReason>;
|
||||||
/**
|
/**
|
||||||
* Whether we are ringing a call recipient. Contains the ringing intent if so.
|
* View model for info relating to ringing, timing out, calling back, etc.
|
||||||
*/
|
*/
|
||||||
ringingIntent$: Behavior<RTCCallIntent | null>;
|
ringingVm$: Behavior<RingingMediaViewModel | null>;
|
||||||
|
/**
|
||||||
|
* Which visual element the ringing status should be shown in.
|
||||||
|
*/
|
||||||
|
ringingStatusLocation: "app_bar" | "tile";
|
||||||
/** Observable that emits when the user should leave the call (hangup pressed, widget action, error).
|
/** Observable that emits when the user should leave the call (hangup pressed, widget action, error).
|
||||||
* THIS DOES NOT LEAVE THE CALL YET. The only way to leave the call (send the hangup event) is
|
* THIS DOES NOT LEAVE THE CALL YET. The only way to leave the call (send the hangup event) is
|
||||||
* - by ending the scope
|
* - by ending the scope
|
||||||
@@ -357,6 +360,10 @@ export interface CallViewModel {
|
|||||||
* and header as overlays.
|
* and header as overlays.
|
||||||
*/
|
*/
|
||||||
edgeToEdge$: Behavior<boolean>;
|
edgeToEdge$: Behavior<boolean>;
|
||||||
|
/**
|
||||||
|
* Whether the call layout is overflowing the interface (causing it to scroll).
|
||||||
|
*/
|
||||||
|
overflowing$: Behavior<boolean>;
|
||||||
|
|
||||||
settingsOpen$: Behavior<boolean>;
|
settingsOpen$: Behavior<boolean>;
|
||||||
setSettingsOpen$: Behavior<(open: boolean) => void>;
|
setSettingsOpen$: Behavior<(open: boolean) => void>;
|
||||||
@@ -408,6 +415,7 @@ export function createCallViewModel$(
|
|||||||
reactionsSubject$: Observable<Record<string, ReactionInfo>>,
|
reactionsSubject$: Observable<Record<string, ReactionInfo>>,
|
||||||
trackProcessorState$: Behavior<ProcessorState>,
|
trackProcessorState$: Behavior<ProcessorState>,
|
||||||
): CallViewModel {
|
): CallViewModel {
|
||||||
|
const logger = rootLogger.getChild("[CallViewModel]");
|
||||||
const client = matrixRoom.client;
|
const client = matrixRoom.client;
|
||||||
const userId = client.getUserId();
|
const userId = client.getUserId();
|
||||||
const deviceId = client.getDeviceId();
|
const deviceId = client.getDeviceId();
|
||||||
@@ -417,6 +425,7 @@ export function createCallViewModel$(
|
|||||||
const livekitKeyProvider = getE2eeKeyProvider(
|
const livekitKeyProvider = getE2eeKeyProvider(
|
||||||
options.encryptionSystem,
|
options.encryptionSystem,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
|
logger,
|
||||||
);
|
);
|
||||||
// matrix_rtc_mode in config.json overrides the user's Developer Settings choice.
|
// matrix_rtc_mode in config.json overrides the user's Developer Settings choice.
|
||||||
// It is validated at config load (src/config/Config.ts) so the cast is safe.
|
// It is validated at config load (src/config/Config.ts) so the cast is safe.
|
||||||
@@ -939,8 +948,8 @@ export function createCallViewModel$(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local user media suitable for displaying in a PiP (undefined if not found
|
* Local user media suitable for displaying in a PiP (undefined if not found,
|
||||||
* or if user prefers to not see themselves).
|
* video is muted, or if user prefers to not see themselves).
|
||||||
*/
|
*/
|
||||||
const localUserMediaForPip$ = scope.behavior<
|
const localUserMediaForPip$ = scope.behavior<
|
||||||
LocalUserMediaViewModel | undefined
|
LocalUserMediaViewModel | undefined
|
||||||
@@ -952,8 +961,10 @@ export function createCallViewModel$(
|
|||||||
m.type === "user" && m.local,
|
m.type === "user" && m.local,
|
||||||
);
|
);
|
||||||
if (!localUserMedia) return of(undefined);
|
if (!localUserMedia) return of(undefined);
|
||||||
return localUserMedia.alwaysShow$.pipe(
|
return combineLatest(
|
||||||
map((alwaysShow) => (alwaysShow ? localUserMedia : undefined)),
|
[localUserMedia.videoEnabled$, localUserMedia.alwaysShow$],
|
||||||
|
(videoEnabled, alwaysShow) =>
|
||||||
|
videoEnabled && alwaysShow ? localUserMedia : undefined,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -1422,7 +1433,7 @@ export function createCallViewModel$(
|
|||||||
windowMode$.pipe(
|
windowMode$.pipe(
|
||||||
switchMap((mode) => {
|
switchMap((mode) => {
|
||||||
// In small windows the header would be too obstructive
|
// 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
|
// In edge-to-edge layouts, couple the visibility of the header
|
||||||
// to that of the footer
|
// to that of the footer
|
||||||
return edgeToEdge$.pipe(
|
return edgeToEdge$.pipe(
|
||||||
@@ -1462,7 +1473,7 @@ export function createCallViewModel$(
|
|||||||
|
|
||||||
// There is a cyclical dependency here: the layout algorithms want to know
|
// There is a cyclical dependency here: the layout algorithms want to know
|
||||||
// which tiles are on screen, but to know which tiles are on screen we have to
|
// which tiles are on screen, but to know which tiles are on screen we have to
|
||||||
// first render a layout. To deal with this we assume initially that no tiles
|
// first render a layout. To deal with this we assume initially that all tiles
|
||||||
// are visible, and loop the data back into the layouts with a Subject.
|
// are visible, and loop the data back into the layouts with a Subject.
|
||||||
const visibleTiles$ = new Subject<number>();
|
const visibleTiles$ = new Subject<number>();
|
||||||
const setVisibleTiles = (value: number): void => visibleTiles$.next(value);
|
const setVisibleTiles = (value: number): void => visibleTiles$.next(value);
|
||||||
@@ -1470,7 +1481,7 @@ export function createCallViewModel$(
|
|||||||
const layoutInternals$ = scope.behavior<LayoutScanState & { layout: Layout }>(
|
const layoutInternals$ = scope.behavior<LayoutScanState & { layout: Layout }>(
|
||||||
combineLatest([
|
combineLatest([
|
||||||
layoutMedia$,
|
layoutMedia$,
|
||||||
visibleTiles$.pipe(startWith(0), distinctUntilChanged()),
|
visibleTiles$.pipe(startWith(Infinity), distinctUntilChanged()),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
scan<
|
scan<
|
||||||
[LayoutMedia, number],
|
[LayoutMedia, number],
|
||||||
@@ -1480,6 +1491,8 @@ export function createCallViewModel$(
|
|||||||
({ tiles: prevTiles }, [media, visibleTiles]) => {
|
({ tiles: prevTiles }, [media, visibleTiles]) => {
|
||||||
let layout: Layout;
|
let layout: Layout;
|
||||||
let newTiles: TileStore;
|
let newTiles: TileStore;
|
||||||
|
let pip: GridTileViewModel | undefined;
|
||||||
|
let overflowing = false;
|
||||||
switch (media.type) {
|
switch (media.type) {
|
||||||
case "grid":
|
case "grid":
|
||||||
case "spotlight-landscape":
|
case "spotlight-landscape":
|
||||||
@@ -1491,6 +1504,7 @@ export function createCallViewModel$(
|
|||||||
setVisibleTiles,
|
setVisibleTiles,
|
||||||
prevTiles,
|
prevTiles,
|
||||||
);
|
);
|
||||||
|
overflowing = newTiles.gridTiles.length > visibleTiles;
|
||||||
break;
|
break;
|
||||||
case "spotlight-expanded":
|
case "spotlight-expanded":
|
||||||
[layout, newTiles] = spotlightExpandedLayout(
|
[layout, newTiles] = spotlightExpandedLayout(
|
||||||
@@ -1505,6 +1519,7 @@ export function createCallViewModel$(
|
|||||||
landscapePipAlignment$,
|
landscapePipAlignment$,
|
||||||
prevTiles,
|
prevTiles,
|
||||||
);
|
);
|
||||||
|
pip = layout.pip;
|
||||||
break;
|
break;
|
||||||
case "one-on-one-portrait":
|
case "one-on-one-portrait":
|
||||||
[layout, newTiles] = oneOnOnePortraitLayout(
|
[layout, newTiles] = oneOnOnePortraitLayout(
|
||||||
@@ -1513,15 +1528,20 @@ export function createCallViewModel$(
|
|||||||
portraitPipAlignment$,
|
portraitPipAlignment$,
|
||||||
prevTiles,
|
prevTiles,
|
||||||
);
|
);
|
||||||
|
pip = layout.pip;
|
||||||
break;
|
break;
|
||||||
case "pip":
|
case "pip":
|
||||||
[layout, newTiles] = pipLayout(media, prevTiles);
|
[layout, newTiles] = pipLayout(media, prevTiles);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { layout, tiles: newTiles };
|
for (const tile of newTiles.gridTiles) {
|
||||||
|
tile.setShowOutline(tile === pip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { layout, overflowing, tiles: newTiles };
|
||||||
},
|
},
|
||||||
{ layout: null, tiles: TileStore.empty() },
|
{ layout: null, overflowing: false, tiles: TileStore.empty() },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1533,6 +1553,10 @@ export function createCallViewModel$(
|
|||||||
layoutInternals$.pipe(map(({ layout }) => layout)),
|
layoutInternals$.pipe(map(({ layout }) => layout)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const overflowing$ = scope.behavior<boolean>(
|
||||||
|
layoutInternals$.pipe(map(({ overflowing }) => overflowing)),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current generation of the tile store, exposed for debugging purposes.
|
* The current generation of the tile store, exposed for debugging purposes.
|
||||||
*/
|
*/
|
||||||
@@ -1702,15 +1726,9 @@ export function createCallViewModel$(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
autoLeave$: autoLeave$,
|
autoLeave$: autoLeave$,
|
||||||
ringingIntent$: scope.behavior(
|
ringingVm$: ringingMedia$,
|
||||||
ringAttempts$.pipe(
|
ringingStatusLocation:
|
||||||
switchMap(({ intent, outcome$ }) =>
|
urlParams.header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
||||||
// Hold the intent as the value until the ring attempt completes
|
|
||||||
concat(of(intent), NEVER.pipe(takeUntil(outcome$)), of(null)),
|
|
||||||
),
|
|
||||||
startWith<RTCCallIntent | null>(null),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
leave$: leave$,
|
leave$: leave$,
|
||||||
hangup: (): void => userHangup$.next(),
|
hangup: (): void => userHangup$.next(),
|
||||||
join: localMembership.requestJoinAndPublish,
|
join: localMembership.requestJoinAndPublish,
|
||||||
@@ -1780,6 +1798,7 @@ export function createCallViewModel$(
|
|||||||
settingsOpen$: settingsOpen$,
|
settingsOpen$: settingsOpen$,
|
||||||
setSettingsOpen$: setSettingsOpen$,
|
setSettingsOpen$: setSettingsOpen$,
|
||||||
edgeToEdge$,
|
edgeToEdge$,
|
||||||
|
overflowing$,
|
||||||
earpieceMode$: earpieceMode$,
|
earpieceMode$: earpieceMode$,
|
||||||
audioOutputSwitcher$: audioOutputSwitcher$,
|
audioOutputSwitcher$: audioOutputSwitcher$,
|
||||||
reconnecting$: localMembership.reconnecting$,
|
reconnecting$: localMembership.reconnecting$,
|
||||||
@@ -1791,6 +1810,7 @@ export function createCallViewModel$(
|
|||||||
function getE2eeKeyProvider(
|
function getE2eeKeyProvider(
|
||||||
e2eeSystem: EncryptionSystem,
|
e2eeSystem: EncryptionSystem,
|
||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
|
logger: Logger,
|
||||||
): BaseKeyProvider | undefined {
|
): BaseKeyProvider | undefined {
|
||||||
if (e2eeSystem.kind === E2eeType.NONE) return undefined;
|
if (e2eeSystem.kind === E2eeType.NONE) return undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ import { type ObservableScope } from "../../ObservableScope";
|
|||||||
import { type Behavior } from "../../Behavior";
|
import { type Behavior } from "../../Behavior";
|
||||||
import { type NodeStyleEventEmitter } from "../../../utils/test";
|
import { type NodeStyleEventEmitter } from "../../../utils/test";
|
||||||
|
|
||||||
/**
|
|
||||||
* Logger instance (scoped child) for homeserver connection updates.
|
|
||||||
*/
|
|
||||||
const logger = rootLogger.getChild("[HomeserverConnected]");
|
|
||||||
|
|
||||||
export type HomeserverDisconnectReason = "sync" | "membership" | "probablyLeft";
|
export type HomeserverDisconnectReason = "sync" | "membership" | "probablyLeft";
|
||||||
|
|
||||||
export interface HomeserverConnected {
|
export interface HomeserverConnected {
|
||||||
@@ -70,6 +65,7 @@ export function createHomeserverConnected$(
|
|||||||
Pick<MatrixRTCSession, "membershipStatus" | "probablyLeft">,
|
Pick<MatrixRTCSession, "membershipStatus" | "probablyLeft">,
|
||||||
gracePeriodMs?: number,
|
gracePeriodMs?: number,
|
||||||
): HomeserverConnected {
|
): HomeserverConnected {
|
||||||
|
const logger = rootLogger.getChild("[HomeserverConnected]");
|
||||||
// Get grace period from parameter or config (default 10000ms)
|
// Get grace period from parameter or config (default 10000ms)
|
||||||
const graceMs = gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms;
|
const graceMs = gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms;
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export const createLocalMembership$ = ({
|
|||||||
logger: parentLogger,
|
logger: parentLogger,
|
||||||
muteStates,
|
muteStates,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
roomId: roomId,
|
roomId,
|
||||||
}: Props): {
|
}: Props): {
|
||||||
/**
|
/**
|
||||||
* This request to start audio and video tracks.
|
* This request to start audio and video tracks.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
tap,
|
tap,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
@@ -46,8 +46,6 @@ import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers
|
|||||||
import { customLivekitUrl } from "../../../settings/settings.ts";
|
import { customLivekitUrl } from "../../../settings/settings.ts";
|
||||||
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("[LocalTransport]");
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* It figures out “which LiveKit focus URL/alias the local user should use,”
|
* It figures out “which LiveKit focus URL/alias the local user should use,”
|
||||||
* optionally aligning with the oldest member, and ensures the SFU path is primed
|
* optionally aligning with the oldest member, and ensures the SFU path is primed
|
||||||
@@ -140,9 +138,14 @@ export const createLocalTransport$ = ({
|
|||||||
forceJwtEndpoint,
|
forceJwtEndpoint,
|
||||||
delayId$,
|
delayId$,
|
||||||
}: Props): LocalTransport => {
|
}: Props): LocalTransport => {
|
||||||
|
const logger = rootLogger.getChild("[LocalTransport]");
|
||||||
// The LiveKit transport in use by the oldest RTC membership. `null` when the
|
// The LiveKit transport in use by the oldest RTC membership. `null` when the
|
||||||
// oldest member has no such transport.
|
// oldest member has no such transport.
|
||||||
const oldestMemberTransport$ = observerOldestMembership$(scope, memberships$);
|
const oldestMemberTransport$ = observerOldestMembership$(
|
||||||
|
scope,
|
||||||
|
memberships$,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
const transportDiscovery = new RtcTransportAutoDiscovery({
|
const transportDiscovery = new RtcTransportAutoDiscovery({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -190,6 +193,7 @@ export const createLocalTransport$ = ({
|
|||||||
roomId,
|
roomId,
|
||||||
client,
|
client,
|
||||||
delayId ?? undefined,
|
delayId ?? undefined,
|
||||||
|
logger,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -209,6 +213,7 @@ export const createLocalTransport$ = ({
|
|||||||
client,
|
client,
|
||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
roomId,
|
roomId,
|
||||||
|
logger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +253,7 @@ export const createLocalTransport$ = ({
|
|||||||
function observerOldestMembership$(
|
function observerOldestMembership$(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
memberships$: Behavior<Epoch<CallMembership[]>>,
|
memberships$: Behavior<Epoch<CallMembership[]>>,
|
||||||
|
logger: Logger,
|
||||||
): Behavior<LivekitTransportConfig | null> {
|
): Behavior<LivekitTransportConfig | null> {
|
||||||
return scope.behavior<LivekitTransportConfig | null>(
|
return scope.behavior<LivekitTransportConfig | null>(
|
||||||
memberships$.pipe(
|
memberships$.pipe(
|
||||||
@@ -307,6 +313,7 @@ async function doOpenIdAndJWTFromUrl(
|
|||||||
> &
|
> &
|
||||||
OpenIDClientParts,
|
OpenIDClientParts,
|
||||||
delayId?: string,
|
delayId?: string,
|
||||||
|
logger?: Logger,
|
||||||
): Promise<LocalTransportWithSFUConfig> {
|
): Promise<LocalTransportWithSFUConfig> {
|
||||||
const sfuConfig = await getSFUConfigWithOpenID(
|
const sfuConfig = await getSFUConfigWithOpenID(
|
||||||
client,
|
client,
|
||||||
@@ -337,6 +344,7 @@ function observeLocalTransportForOldestMembership(
|
|||||||
OpenIDClientParts,
|
OpenIDClientParts,
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
|
logger: Logger,
|
||||||
): LocalTransport {
|
): LocalTransport {
|
||||||
// Ensure we can authenticate with the SFU.
|
// Ensure we can authenticate with the SFU.
|
||||||
const authenticatedOldestMemberTransport$ = oldestMemberTransport$.pipe(
|
const authenticatedOldestMemberTransport$ = oldestMemberTransport$.pipe(
|
||||||
@@ -355,6 +363,7 @@ function observeLocalTransportForOldestMembership(
|
|||||||
roomId,
|
roomId,
|
||||||
client,
|
client,
|
||||||
undefined,
|
undefined,
|
||||||
|
logger,
|
||||||
),
|
),
|
||||||
).pipe(
|
).pipe(
|
||||||
catchError((e: unknown) => {
|
catchError((e: unknown) => {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class RtcTransportAutoDiscovery {
|
|||||||
const transportList = await doNetworkOperationWithRetry(async () =>
|
const transportList = await doNetworkOperationWithRetry(async () =>
|
||||||
client._unstable_getRTCTransports(),
|
client._unstable_getRTCTransports(),
|
||||||
);
|
);
|
||||||
const first = transportList.filter(isLivekitTransportConfig)[0];
|
const first = transportList.find(isLivekitTransportConfig);
|
||||||
if (first) {
|
if (first) {
|
||||||
return first;
|
return first;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import {
|
|||||||
} from "../../../utils/displayname";
|
} from "../../../utils/displayname";
|
||||||
import { type Behavior } from "../../Behavior";
|
import { type Behavior } from "../../Behavior";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("[MatrixMemberMetadata]");
|
|
||||||
|
|
||||||
export type RoomMemberMap = Map<
|
export type RoomMemberMap = Map<
|
||||||
string,
|
string,
|
||||||
Pick<RoomMember, "userId" | "getMxcAvatarUrl" | "rawDisplayName">
|
Pick<RoomMember, "userId" | "getMxcAvatarUrl" | "rawDisplayName">
|
||||||
@@ -67,6 +65,7 @@ export const memberDisplaynames$ = (
|
|||||||
memberships$: Behavior<Pick<CallMembership, "userId">[]>,
|
memberships$: Behavior<Pick<CallMembership, "userId">[]>,
|
||||||
roomMembers$: Behavior<RoomMemberMap>,
|
roomMembers$: Behavior<RoomMemberMap>,
|
||||||
): Behavior<Map<string, string>> => {
|
): Behavior<Map<string, string>> => {
|
||||||
|
const logger = rootLogger.getChild("[MatrixMemberMetadata]");
|
||||||
// This map tracks userIds that at some point needed disambiguation.
|
// This map tracks userIds that at some point needed disambiguation.
|
||||||
// This is a memory leak bound to the number of participants.
|
// This is a memory leak bound to the number of participants.
|
||||||
// A call application will always increase the memory if there have been more members in a call.
|
// A call application will always increase the memory if there have been more members in a call.
|
||||||
|
|||||||
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 { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import { combineLatest, merge, startWith, Subject, tap } from "rxjs";
|
import { combineLatest, merge, startWith, Subject, tap } from "rxjs";
|
||||||
|
|
||||||
|
import type { RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import {
|
import {
|
||||||
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
||||||
outputDevice$ as controlledOutputSelection$,
|
outputDevice$ as controlledOutputSelection$,
|
||||||
@@ -24,7 +25,7 @@ import {
|
|||||||
|
|
||||||
// This hardcoded id is used in EX ios! It can only be changed in coordination with
|
// This hardcoded id is used in EX ios! It can only be changed in coordination with
|
||||||
// the ios swift team.
|
// 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
|
* A special implementation of audio output that allows the hosting application
|
||||||
@@ -94,7 +95,7 @@ export class IOSControlledAudioOutput implements MediaDevice<
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
(available, preferredId) => {
|
(available, preferredId) => {
|
||||||
const id = preferredId ?? available.keys().next().value;
|
const id = preferredId ?? this.chooseDefaultId(available);
|
||||||
return id === undefined
|
return id === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: { id, virtualEarpiece: id === EARPIECE_CONFIG_ID };
|
: { 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(
|
public constructor(
|
||||||
private readonly usingNames$: Behavior<boolean>,
|
private readonly usingNames$: Behavior<boolean>,
|
||||||
private readonly scope: ObservableScope,
|
private readonly scope: ObservableScope,
|
||||||
|
private readonly initialIntent: RTCCallIntent | undefined = undefined,
|
||||||
) {
|
) {
|
||||||
this.selected$.subscribe((device) => {
|
this.selected$.subscribe((device) => {
|
||||||
// Let the hosting application know which output device has been selected.
|
// Let the hosting application know which output device has been selected.
|
||||||
|
|||||||
@@ -376,7 +376,11 @@ export class MediaDevices {
|
|||||||
getUrlParams().callIntent,
|
getUrlParams().callIntent,
|
||||||
window.controls,
|
window.controls,
|
||||||
)
|
)
|
||||||
: new IOSControlledAudioOutput(this.usingNames$, this.scope)
|
: new IOSControlledAudioOutput(
|
||||||
|
this.usingNames$,
|
||||||
|
this.scope,
|
||||||
|
getUrlParams().callIntent,
|
||||||
|
)
|
||||||
: new AudioOutput(this.usingNames$, this.scope);
|
: new AudioOutput(this.usingNames$, this.scope);
|
||||||
|
|
||||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function oneOnOnePortraitLayout(
|
|||||||
prevTiles: TileStore,
|
prevTiles: TileStore,
|
||||||
): [OneOnOnePortraitLayout, TileStore] {
|
): [OneOnOnePortraitLayout, TileStore] {
|
||||||
const update = prevTiles.from(media.pip === undefined ? 0 : 1);
|
const update = prevTiles.from(media.pip === undefined ? 0 : 1);
|
||||||
update.registerSpotlight([media.spotlight], true);
|
update.registerSpotlight([media.spotlight], true, "transparent");
|
||||||
if (media.pip !== undefined) update.registerGridTile(media.pip);
|
if (media.pip !== undefined) update.registerGridTile(media.pip);
|
||||||
const tiles = update.build();
|
const tiles = update.build();
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function pipLayout(
|
|||||||
update.registerSpotlight(
|
update.registerSpotlight(
|
||||||
media.spotlight,
|
media.spotlight,
|
||||||
platform === "desktop" ? false : true,
|
platform === "desktop" ? false : true,
|
||||||
|
"transparent",
|
||||||
);
|
);
|
||||||
const tiles = update.build();
|
const tiles = update.build();
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function spotlightExpandedLayout(
|
|||||||
prevTiles: TileStore,
|
prevTiles: TileStore,
|
||||||
): [SpotlightExpandedLayout, TileStore] {
|
): [SpotlightExpandedLayout, TileStore] {
|
||||||
const update = prevTiles.from(1);
|
const update = prevTiles.from(1);
|
||||||
update.registerSpotlight(media.spotlight, true);
|
update.registerSpotlight(media.spotlight, true, "transparent");
|
||||||
if (media.pip !== undefined) update.registerPipTile(media.pip);
|
if (media.pip !== undefined) update.registerPipTile(media.pip);
|
||||||
const tiles = update.build();
|
const tiles = update.build();
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { type MediaViewModel } from "./media/MediaViewModel";
|
|||||||
import { type UserMediaViewModel } from "./media/UserMediaViewModel";
|
import { type UserMediaViewModel } from "./media/UserMediaViewModel";
|
||||||
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
||||||
|
|
||||||
|
type SpotlightBackground = "solid" | "transparent";
|
||||||
|
|
||||||
function debugEntries(entries: GridTileData[]): string[] {
|
function debugEntries(entries: GridTileData[]): string[] {
|
||||||
return entries.map((e) => e.media.displayName$.value);
|
return entries.map((e) => e.media.displayName$.value);
|
||||||
}
|
}
|
||||||
@@ -39,12 +41,29 @@ class SpotlightTileData {
|
|||||||
this.maximised$.next(value);
|
this.maximised$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly background$: BehaviorSubject<SpotlightBackground>;
|
||||||
|
public get background(): SpotlightBackground {
|
||||||
|
return this.background$.value;
|
||||||
|
}
|
||||||
|
public set background(value: SpotlightBackground) {
|
||||||
|
this.background$.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
public readonly vm: SpotlightTileViewModel;
|
public readonly vm: SpotlightTileViewModel;
|
||||||
|
|
||||||
public constructor(media: MediaViewModel[], maximised: boolean) {
|
public constructor(
|
||||||
|
media: MediaViewModel[],
|
||||||
|
maximised: boolean,
|
||||||
|
background: SpotlightBackground,
|
||||||
|
) {
|
||||||
this.media$ = new BehaviorSubject(media);
|
this.media$ = new BehaviorSubject(media);
|
||||||
this.maximised$ = new BehaviorSubject(maximised);
|
this.maximised$ = new BehaviorSubject(maximised);
|
||||||
this.vm = new SpotlightTileViewModel(this.media$, this.maximised$);
|
this.background$ = new BehaviorSubject(background);
|
||||||
|
this.vm = new SpotlightTileViewModel(
|
||||||
|
this.media$,
|
||||||
|
this.maximised$,
|
||||||
|
this.background$,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +150,9 @@ export class TileStoreBuilder {
|
|||||||
private numGridEntries = 0;
|
private numGridEntries = 0;
|
||||||
// A sparse array of grid entries which should be kept in the same spots as
|
// A sparse array of grid entries which should be kept in the same spots as
|
||||||
// which they appeared in the previous grid
|
// which they appeared in the previous grid
|
||||||
private readonly stationaryGridEntries: GridTileData[] = new Array(
|
private readonly stationaryGridEntries: GridTileData[] = Array.from({
|
||||||
this.prevGrid.length,
|
length: this.prevGrid.length,
|
||||||
);
|
});
|
||||||
// Grid entries which should now enter the visible section of the grid
|
// Grid entries which should now enter the visible section of the grid
|
||||||
private readonly visibleGridEntries: GridTileData[] = [];
|
private readonly visibleGridEntries: GridTileData[] = [];
|
||||||
// Grid entries which should now enter the invisible section of the grid
|
// Grid entries which should now enter the invisible section of the grid
|
||||||
@@ -157,7 +176,11 @@ export class TileStoreBuilder {
|
|||||||
* Sets the contents of the spotlight tile. If this is never called, there
|
* Sets the contents of the spotlight tile. If this is never called, there
|
||||||
* will be no spotlight tile.
|
* will be no spotlight tile.
|
||||||
*/
|
*/
|
||||||
public registerSpotlight(media: MediaViewModel[], maximised: boolean): void {
|
public registerSpotlight(
|
||||||
|
media: MediaViewModel[],
|
||||||
|
maximised: boolean,
|
||||||
|
background: SpotlightBackground = "solid",
|
||||||
|
): void {
|
||||||
if (DEBUG_ENABLED)
|
if (DEBUG_ENABLED)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[TileStore, ${this.generation}] register spotlight: ${media.map((m) => m.displayName$.value)}`,
|
`[TileStore, ${this.generation}] register spotlight: ${media.map((m) => m.displayName$.value)}`,
|
||||||
@@ -169,11 +192,12 @@ export class TileStoreBuilder {
|
|||||||
|
|
||||||
// Reuse the previous spotlight tile if it exists
|
// Reuse the previous spotlight tile if it exists
|
||||||
if (this.prevSpotlight === null) {
|
if (this.prevSpotlight === null) {
|
||||||
this.spotlight = new SpotlightTileData(media, maximised);
|
this.spotlight = new SpotlightTileData(media, maximised, background);
|
||||||
} else {
|
} else {
|
||||||
this.spotlight = this.prevSpotlight;
|
this.spotlight = this.prevSpotlight;
|
||||||
this.spotlight.media = media;
|
this.spotlight.media = media;
|
||||||
this.spotlight.maximised = maximised;
|
this.spotlight.maximised = maximised;
|
||||||
|
this.spotlight.background = background;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { type Behavior } from "./Behavior";
|
import { type Behavior } from "./Behavior";
|
||||||
import { type MediaViewModel } from "./media/MediaViewModel";
|
import { type MediaViewModel } from "./media/MediaViewModel";
|
||||||
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
||||||
@@ -17,18 +19,25 @@ function createId(): string {
|
|||||||
|
|
||||||
export class GridTileViewModel {
|
export class GridTileViewModel {
|
||||||
public readonly id = createId();
|
public readonly id = createId();
|
||||||
|
private readonly _showOutline$ = new BehaviorSubject(false);
|
||||||
|
public readonly showOutline$: Behavior<boolean> = this._showOutline$;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly media$: Behavior<
|
public readonly media$: Behavior<
|
||||||
UserMediaViewModel | RingingMediaViewModel
|
UserMediaViewModel | RingingMediaViewModel
|
||||||
>,
|
>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public setShowOutline(value: boolean): void {
|
||||||
|
this._showOutline$.next(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SpotlightTileViewModel {
|
export class SpotlightTileViewModel {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly media$: Behavior<MediaViewModel[]>,
|
public readonly media$: Behavior<MediaViewModel[]>,
|
||||||
public readonly maximised$: Behavior<boolean>,
|
public readonly maximised$: Behavior<boolean>,
|
||||||
|
public readonly background$: Behavior<"solid" | "transparent">,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export function createMemberMedia(
|
|||||||
}: MemberMediaInputs,
|
}: MemberMediaInputs,
|
||||||
): BaseMemberMediaViewModel {
|
): BaseMemberMediaViewModel {
|
||||||
const trackBehavior$ = (
|
const trackBehavior$ = (
|
||||||
|
scope: ObservableScope,
|
||||||
source: Track.Source,
|
source: Track.Source,
|
||||||
): Behavior<TrackReference | undefined> =>
|
): Behavior<TrackReference | undefined> =>
|
||||||
scope.behavior(
|
scope.behavior(
|
||||||
@@ -102,8 +103,8 @@ export function createMemberMedia(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const audio$ = trackBehavior$(audioSource);
|
const audio$ = trackBehavior$(scope, audioSource);
|
||||||
const video$ = trackBehavior$(videoSource);
|
const video$ = trackBehavior$(scope, videoSource);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...createBaseMedia(inputs),
|
...createBaseMedia(inputs),
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ borders don't support gradients */
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tile.outline {
|
||||||
|
outline: var(--cpd-border-width-1) solid
|
||||||
|
var(--cpd-color-border-interactive-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.tile:hover {
|
.tile:hover {
|
||||||
outline: var(--cpd-border-width-2) solid
|
outline: var(--cpd-border-width-2) solid
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ test("GridTile is accessible", async () => {
|
|||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
showSpeakingIndicators
|
showSpeakingIndicators
|
||||||
showNameTags
|
showNameTags
|
||||||
|
showRingingStatus
|
||||||
|
showOutline
|
||||||
focusable
|
focusable
|
||||||
/>
|
/>
|
||||||
</ReactionsSenderProvider>,
|
</ReactionsSenderProvider>,
|
||||||
@@ -108,6 +110,8 @@ test("GridTile displays ringing media", async () => {
|
|||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
showSpeakingIndicators
|
showSpeakingIndicators
|
||||||
showNameTags
|
showNameTags
|
||||||
|
showRingingStatus
|
||||||
|
showOutline
|
||||||
focusable
|
focusable
|
||||||
/>
|
/>
|
||||||
</ReactionsSenderProvider>,
|
</ReactionsSenderProvider>,
|
||||||
|
|||||||
@@ -29,15 +29,13 @@ import {
|
|||||||
UserProfileIcon,
|
UserProfileIcon,
|
||||||
VolumeOffSolidIcon,
|
VolumeOffSolidIcon,
|
||||||
SwitchCameraSolidIcon,
|
SwitchCameraSolidIcon,
|
||||||
VideoCallSolidIcon,
|
|
||||||
VoiceCallSolidIcon,
|
|
||||||
EndCallIcon,
|
|
||||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
ToggleMenuItem,
|
ToggleMenuItem,
|
||||||
Menu,
|
Menu,
|
||||||
|
Text,
|
||||||
} from "@vector-im/compound-web";
|
} from "@vector-im/compound-web";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
|
|
||||||
@@ -53,6 +51,7 @@ import { type LocalUserMediaViewModel } from "../state/media/LocalUserMediaViewM
|
|||||||
import { type RemoteUserMediaViewModel } from "../state/media/RemoteUserMediaViewModel";
|
import { type RemoteUserMediaViewModel } from "../state/media/RemoteUserMediaViewModel";
|
||||||
import { type UserMediaViewModel } from "../state/media/UserMediaViewModel";
|
import { type UserMediaViewModel } from "../state/media/UserMediaViewModel";
|
||||||
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
||||||
|
import { RingingStatus } from "./RingingStatus";
|
||||||
|
|
||||||
interface TileProps {
|
interface TileProps {
|
||||||
ref?: Ref<HTMLDivElement>;
|
ref?: Ref<HTMLDivElement>;
|
||||||
@@ -68,16 +67,15 @@ interface TileProps {
|
|||||||
|
|
||||||
interface RingingMediaTileProps extends TileProps {
|
interface RingingMediaTileProps extends TileProps {
|
||||||
vm: RingingMediaViewModel;
|
vm: RingingMediaViewModel;
|
||||||
|
showStatus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RingingMediaTile: FC<RingingMediaTileProps> = ({
|
const RingingMediaTile: FC<RingingMediaTileProps> = ({
|
||||||
vm,
|
vm,
|
||||||
|
showStatus,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const pickupState = useBehavior(vm.pickupState$);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaView
|
<MediaView
|
||||||
className={classNames(className, styles.tile)}
|
className={classNames(className, styles.tile)}
|
||||||
@@ -85,14 +83,13 @@ const RingingMediaTile: FC<RingingMediaTileProps> = ({
|
|||||||
userId={vm.userId}
|
userId={vm.userId}
|
||||||
unencryptedWarning={false}
|
unencryptedWarning={false}
|
||||||
status={
|
status={
|
||||||
pickupState === "ringing"
|
showStatus && (
|
||||||
? {
|
<Text as="span" size="sm" weight="medium">
|
||||||
text: t("video_tile.calling"),
|
<RingingStatus vm={vm} />
|
||||||
Icon:
|
</Text>
|
||||||
vm.intent === "video" ? VideoCallSolidIcon : VoiceCallSolidIcon,
|
)
|
||||||
}
|
|
||||||
: { text: t("video_tile.call_ended"), Icon: EndCallIcon }
|
|
||||||
}
|
}
|
||||||
|
avatarStyle="translucent"
|
||||||
videoEnabled={false}
|
videoEnabled={false}
|
||||||
videoFit="cover"
|
videoFit="cover"
|
||||||
mirror={false}
|
mirror={false}
|
||||||
@@ -400,6 +397,8 @@ interface GridTileProps {
|
|||||||
style?: ComponentProps<typeof animated.div>["style"];
|
style?: ComponentProps<typeof animated.div>["style"];
|
||||||
showSpeakingIndicators: boolean;
|
showSpeakingIndicators: boolean;
|
||||||
showNameTags: boolean;
|
showNameTags: boolean;
|
||||||
|
showRingingStatus: boolean;
|
||||||
|
showOutline: boolean;
|
||||||
focusable: boolean;
|
focusable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +406,10 @@ export const GridTile: FC<GridTileProps> = ({
|
|||||||
ref: theirRef,
|
ref: theirRef,
|
||||||
vm,
|
vm,
|
||||||
showSpeakingIndicators,
|
showSpeakingIndicators,
|
||||||
|
showRingingStatus,
|
||||||
|
showOutline,
|
||||||
onOpenProfile,
|
onOpenProfile,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -423,6 +425,8 @@ export const GridTile: FC<GridTileProps> = ({
|
|||||||
vm={media}
|
vm={media}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
mxcAvatarUrl={mxcAvatarUrl}
|
mxcAvatarUrl={mxcAvatarUrl}
|
||||||
|
showStatus={showRingingStatus}
|
||||||
|
className={classNames(className, { [styles.outline]: showOutline })}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -435,6 +439,7 @@ export const GridTile: FC<GridTileProps> = ({
|
|||||||
onOpenProfile={onOpenProfile}
|
onOpenProfile={onOpenProfile}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
mxcAvatarUrl={mxcAvatarUrl}
|
mxcAvatarUrl={mxcAvatarUrl}
|
||||||
|
className={classNames(className, { [styles.outline]: showOutline })}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -446,6 +451,7 @@ export const GridTile: FC<GridTileProps> = ({
|
|||||||
showSpeakingIndicators={showSpeakingIndicators}
|
showSpeakingIndicators={showSpeakingIndicators}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
mxcAvatarUrl={mxcAvatarUrl}
|
mxcAvatarUrl={mxcAvatarUrl}
|
||||||
|
className={classNames(className, { [styles.outline]: showOutline })}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ Please see LICENSE in the repository root for full details.
|
|||||||
transform: translate(0);
|
transform: translate(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media[data-video-enabled="false"] video {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.media.mirror video {
|
.media.mirror video {
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
@@ -41,24 +45,76 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
.bg {
|
.bg {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
background-color: var(--video-tile-background);
|
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
contain: strict;
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media[data-background="solid"] .bg {
|
||||||
|
background-color: var(--video-tile-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.waves {
|
||||||
|
transition: opacity ease 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waves[data-visible="true"] {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waves[data-visible="false"] {
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
@media not (prefers-reduced-motion) {
|
||||||
|
.wave {
|
||||||
|
transform: translate(-50%, -50%) scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave {
|
||||||
|
border: var(--cpd-border-width-1) solid var(--cpd-color-alpha-gray-300);
|
||||||
|
transition: transform ease 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave,
|
||||||
|
.speakingBorder {
|
||||||
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.speakingBorder {
|
||||||
|
background:
|
||||||
|
radial-gradient(#0467dd, #0bc491),
|
||||||
|
linear-gradient(0deg, #0467dd 0%, #0bc491 100%);
|
||||||
|
background-blend-mode: overlay, normal;
|
||||||
|
outline: var(--cpd-border-width-4) solid var(--cpd-color-bg-canvas-default);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: var(--cpd-border-width-2);
|
||||||
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
|
background: var(--cpd-color-bg-canvas-default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
opacity: 100%;
|
||||||
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.translucent {
|
.avatar[data-style="translucent"] {
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
mix-blend-mode: multiply;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CSS makes us put a condition here, even though all we want to do is
|
/* CSS makes us put a condition here, even though all we want to do is
|
||||||
@@ -69,6 +125,35 @@ unconditionally select the container so we can use cqmin units */
|
|||||||
inline-size: 50cqmin;
|
inline-size: 50cqmin;
|
||||||
block-size: 50cqmin;
|
block-size: 50cqmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.waves + .avatar {
|
||||||
|
/* Make the avatar slightly smaller to accommodate sound waves, if present */
|
||||||
|
inline-size: 38cqmin;
|
||||||
|
block-size: 38cqmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave:nth-child(1) {
|
||||||
|
inline-size: calc(38cqmin + var(--cpd-space-5x) + 3 * var(--cpd-space-10x));
|
||||||
|
block-size: calc(38cqmin + var(--cpd-space-5x) + 3 * var(--cpd-space-10x));
|
||||||
|
background: var(--cpd-color-alpha-gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave:nth-child(2) {
|
||||||
|
inline-size: calc(38cqmin + var(--cpd-space-5x) + 2 * var(--cpd-space-10x));
|
||||||
|
block-size: calc(38cqmin + var(--cpd-space-5x) + 2 * var(--cpd-space-10x));
|
||||||
|
background: var(--cpd-color-alpha-gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave:nth-child(3) {
|
||||||
|
inline-size: calc(38cqmin + var(--cpd-space-5x) + var(--cpd-space-10x));
|
||||||
|
block-size: calc(38cqmin + var(--cpd-space-5x) + var(--cpd-space-10x));
|
||||||
|
background: var(--cpd-color-alpha-gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.speakingBorder {
|
||||||
|
inline-size: calc(38cqmin + var(--cpd-space-3x));
|
||||||
|
block-size: calc(38cqmin + var(--cpd-space-3x));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar > img {
|
.avatar > img {
|
||||||
@@ -121,18 +206,18 @@ unconditionally select the container so we can use cqmin units */
|
|||||||
.status {
|
.status {
|
||||||
grid-area: status;
|
grid-area: status;
|
||||||
color: var(--cpd-color-text-primary);
|
color: var(--cpd-color-text-primary);
|
||||||
display: flex;
|
|
||||||
flex-wrap: none;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-block-start: calc(var(--cpd-space-3x) - var(--fg-inset));
|
margin-block-start: calc(var(--cpd-space-3x) - var(--fg-inset));
|
||||||
margin-inline-start: calc(var(--cpd-space-4x) - var(--fg-inset));
|
margin-inline-start: calc(var(--cpd-space-4x) - var(--fg-inset));
|
||||||
}
|
|
||||||
|
|
||||||
.status svg {
|
svg {
|
||||||
color: var(--cpd-color-icon-tertiary);
|
color: var(--cpd-color-icon-tertiary);
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
margin-inline-end: 3px;
|
||||||
|
block-size: 1.2em;
|
||||||
|
inline-size: 1.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reactions {
|
.reactions {
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
type TrackReference,
|
type TrackReference,
|
||||||
type TrackReferencePlaceholder,
|
type TrackReferencePlaceholder,
|
||||||
} from "@livekit/components-core";
|
} from "@livekit/components-core";
|
||||||
import { LocalTrackPublication, Track } from "livekit-client";
|
import { type LocalTrackPublication, Track } from "livekit-client";
|
||||||
import { TrackInfo } from "@livekit/protocol";
|
|
||||||
import { type ComponentProps } from "react";
|
import { type ComponentProps } from "react";
|
||||||
|
|
||||||
import { MediaView } from "./MediaView";
|
import { MediaView } from "./MediaView";
|
||||||
@@ -28,10 +27,7 @@ describe("MediaView", () => {
|
|||||||
};
|
};
|
||||||
const trackReference: TrackReference = {
|
const trackReference: TrackReference = {
|
||||||
...trackReferencePlaceholder,
|
...trackReferencePlaceholder,
|
||||||
publication: new LocalTrackPublication(
|
publication: {} as Partial<LocalTrackPublication> as LocalTrackPublication,
|
||||||
Track.Kind.Video,
|
|
||||||
new TrackInfo({ sid: "id", name: "name" }),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseProps: ComponentProps<typeof MediaView> = {
|
const baseProps: ComponentProps<typeof MediaView> = {
|
||||||
@@ -129,30 +125,4 @@ describe("MediaView", () => {
|
|||||||
).toBe(0);
|
).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("videoEnabled", () => {
|
|
||||||
test("just video is visible", () => {
|
|
||||||
render(
|
|
||||||
<TooltipProvider>
|
|
||||||
<MediaView {...baseProps} videoEnabled={true} />
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId("video")).toBeVisible();
|
|
||||||
expect(screen.queryAllByRole("img", { name: "some name" }).length).toBe(
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("just avatar is visible", () => {
|
|
||||||
render(
|
|
||||||
<TooltipProvider>
|
|
||||||
<MediaView {...baseProps} videoEnabled={false} />
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
screen.getByRole("img", { name: "@alice:example.com" }),
|
|
||||||
).toBeVisible();
|
|
||||||
expect(screen.getByTestId("video")).not.toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
||||||
import { animated } from "@react-spring/web";
|
import { animated } from "@react-spring/web";
|
||||||
import {
|
import { type FC, type ComponentProps, type ReactNode } from "react";
|
||||||
type FC,
|
|
||||||
type ComponentProps,
|
|
||||||
type ReactNode,
|
|
||||||
type ComponentType,
|
|
||||||
type SVGAttributes,
|
|
||||||
} from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { VideoTrack } from "@livekit/components-react";
|
import { VideoTrack } from "@livekit/components-react";
|
||||||
@@ -40,14 +34,17 @@ interface Props extends ComponentProps<typeof animated.div> {
|
|||||||
video: TrackReferenceOrPlaceholder | undefined;
|
video: TrackReferenceOrPlaceholder | undefined;
|
||||||
videoFit: "cover" | "contain";
|
videoFit: "cover" | "contain";
|
||||||
mirror: boolean;
|
mirror: boolean;
|
||||||
|
soundWaves?: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
videoEnabled: boolean;
|
videoEnabled: boolean;
|
||||||
unencryptedWarning: boolean;
|
unencryptedWarning: boolean;
|
||||||
status?: { text: string; Icon: ComponentType<SVGAttributes<SVGElement>> };
|
status?: ReactNode;
|
||||||
showNameTags: boolean;
|
showNameTags: boolean;
|
||||||
nameTagLeadingIcon?: ReactNode;
|
nameTagLeadingIcon?: ReactNode;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
mxcAvatarUrl: string | undefined;
|
mxcAvatarUrl: string | undefined;
|
||||||
|
avatarStyle?: "solid" | "translucent";
|
||||||
|
background?: "solid" | "transparent";
|
||||||
focusable: boolean;
|
focusable: boolean;
|
||||||
primaryButton?: ReactNode;
|
primaryButton?: ReactNode;
|
||||||
raisedHandTime?: Date;
|
raisedHandTime?: Date;
|
||||||
@@ -70,6 +67,7 @@ export const MediaView: FC<Props> = ({
|
|||||||
video,
|
video,
|
||||||
videoFit,
|
videoFit,
|
||||||
mirror,
|
mirror,
|
||||||
|
soundWaves,
|
||||||
userId,
|
userId,
|
||||||
videoEnabled,
|
videoEnabled,
|
||||||
unencryptedWarning,
|
unencryptedWarning,
|
||||||
@@ -77,6 +75,8 @@ export const MediaView: FC<Props> = ({
|
|||||||
nameTagLeadingIcon,
|
nameTagLeadingIcon,
|
||||||
displayName,
|
displayName,
|
||||||
mxcAvatarUrl,
|
mxcAvatarUrl,
|
||||||
|
avatarStyle = "solid",
|
||||||
|
background = "solid",
|
||||||
focusable,
|
focusable,
|
||||||
primaryButton,
|
primaryButton,
|
||||||
status,
|
status,
|
||||||
@@ -94,7 +94,10 @@ export const MediaView: FC<Props> = ({
|
|||||||
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
|
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
|
||||||
const [showConnectionStats] = useSetting(showConnectionStatsSetting);
|
const [showConnectionStats] = useSetting(showConnectionStatsSetting);
|
||||||
|
|
||||||
const avatarSize = Math.round(Math.min(targetWidth, targetHeight) / 2);
|
const avatarSize = Math.round(
|
||||||
|
Math.min(targetWidth, targetHeight) *
|
||||||
|
(soundWaves === undefined ? 0.5 : 0.38),
|
||||||
|
);
|
||||||
|
|
||||||
const warnings = unencryptedWarning && (
|
const warnings = unencryptedWarning && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -121,20 +124,27 @@ export const MediaView: FC<Props> = ({
|
|||||||
style={style}
|
style={style}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-testid="videoTile"
|
data-testid="videoTile"
|
||||||
|
data-video-enabled={video && videoEnabled}
|
||||||
data-video-fit={videoFit}
|
data-video-fit={videoFit}
|
||||||
|
data-background={background}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={styles.bg}>
|
<div className={styles.bg}>
|
||||||
|
{soundWaves !== undefined && (
|
||||||
|
<div className={styles.waves} data-visible={soundWaves}>
|
||||||
|
<div className={styles.wave} />
|
||||||
|
<div className={styles.wave} />
|
||||||
|
<div className={styles.wave} />
|
||||||
|
<div className={styles.speakingBorder} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Avatar
|
<Avatar
|
||||||
id={userId}
|
id={userId}
|
||||||
name={displayName}
|
name={displayName}
|
||||||
size={avatarSize}
|
size={avatarSize}
|
||||||
src={mxcAvatarUrl}
|
src={mxcAvatarUrl}
|
||||||
className={classNames(styles.avatar, {
|
data-style={avatarStyle}
|
||||||
// When the avatar is overlaid with a status, make it translucent
|
className={styles.avatar}
|
||||||
// for readability
|
|
||||||
[styles.translucent]: status,
|
|
||||||
})}
|
|
||||||
style={{ display: video && videoEnabled ? "none" : "initial" }}
|
style={{ display: video && videoEnabled ? "none" : "initial" }}
|
||||||
/>
|
/>
|
||||||
{video?.publication !== undefined && (
|
{video?.publication !== undefined && (
|
||||||
@@ -143,7 +153,6 @@ export const MediaView: FC<Props> = ({
|
|||||||
// There's no reason for this to be focusable
|
// There's no reason for this to be focusable
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
style={{ display: video && videoEnabled ? "block" : "none" }}
|
|
||||||
data-testid="video"
|
data-testid="video"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -180,14 +189,7 @@ export const MediaView: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{status && (
|
{status && <div className={styles.status}>{status}</div>}
|
||||||
<div className={styles.status}>
|
|
||||||
<status.Icon width={16} height={16} aria-hidden />
|
|
||||||
<Text as="span" size="sm" weight="medium">
|
|
||||||
{status.text}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* TODO: Bring this back once encryption status is less broken */}
|
{/* TODO: Bring this back once encryption status is less broken */}
|
||||||
{/*encryptionStatus !== EncryptionStatus.Okay && (
|
{/*encryptionStatus !== EncryptionStatus.Okay && (
|
||||||
<div className={styles.status}>
|
<div className={styles.status}>
|
||||||
|
|||||||
41
src/tile/RingingStatus.tsx
Normal file
41
src/tile/RingingStatus.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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 { type FC } from "react";
|
||||||
|
import {
|
||||||
|
VideoCallSolidIcon,
|
||||||
|
VoiceCallSolidIcon,
|
||||||
|
EndCallIcon,
|
||||||
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
||||||
|
import { useBehavior } from "../useBehavior";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
vm: RingingMediaViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RingingStatus: FC<Props> = ({ vm }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const pickupState = useBehavior(vm.pickupState$);
|
||||||
|
const Icon =
|
||||||
|
pickupState === "ringing"
|
||||||
|
? vm.intent === "video"
|
||||||
|
? VideoCallSolidIcon
|
||||||
|
: VoiceCallSolidIcon
|
||||||
|
: EndCallIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Icon aria-hidden />
|
||||||
|
{pickupState === "ringing"
|
||||||
|
? t("video_tile.calling")
|
||||||
|
: t("video_tile.call_ended")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -23,7 +23,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
scroll-behavior: smooth; */
|
scroll-behavior: smooth; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.maximised .contents {
|
.tile[data-maximised="true"] .contents {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
--media-view-fg-inset: 10px;
|
--media-view-fg-inset: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maximised .item {
|
.tile[data-maximised="true"] .item {
|
||||||
/* Ensure that foreground elements lie within the safe area */
|
/* Ensure that foreground elements lie within the safe area */
|
||||||
--media-view-fg-inset: calc(var(--call-view-safe-area-inset-top, 0px) + 10px)
|
--media-view-fg-inset: calc(var(--call-view-safe-area-inset-top, 0px) + 10px)
|
||||||
calc(env(safe-area-inset-right) + 10px)
|
calc(env(safe-area-inset-right) + 10px)
|
||||||
@@ -191,7 +191,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maximised .indicators {
|
.tile[data-maximised="true"] .indicators {
|
||||||
inset-block-end: calc(-1 * var(--cpd-space-4x) - 2px);
|
inset-block-end: calc(-1 * var(--cpd-space-4x) - 2px);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,20 @@ test("SpotlightTile is accessible", async () => {
|
|||||||
const toggleExpanded = vi.fn();
|
const toggleExpanded = vi.fn();
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<SpotlightTile
|
<SpotlightTile
|
||||||
vm={new SpotlightTileViewModel(constant([vm1, vm2]), constant(false))}
|
vm={
|
||||||
|
new SpotlightTileViewModel(
|
||||||
|
constant([vm1, vm2]),
|
||||||
|
constant(false),
|
||||||
|
constant("solid"),
|
||||||
|
)
|
||||||
|
}
|
||||||
targetWidth={300}
|
targetWidth={300}
|
||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggleExpanded={toggleExpanded}
|
onToggleExpanded={toggleExpanded}
|
||||||
showIndicators
|
showIndicators
|
||||||
showNameTags
|
showNameTags
|
||||||
|
showRingingStatus
|
||||||
focusable={true}
|
focusable={true}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -100,13 +107,20 @@ test("Screen share volume UI is shown when screen share has audio", async () =>
|
|||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<SpotlightTile
|
<SpotlightTile
|
||||||
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
vm={
|
||||||
|
new SpotlightTileViewModel(
|
||||||
|
constant([vm]),
|
||||||
|
constant(false),
|
||||||
|
constant("solid"),
|
||||||
|
)
|
||||||
|
}
|
||||||
targetWidth={300}
|
targetWidth={300}
|
||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggleExpanded={toggleExpanded}
|
onToggleExpanded={toggleExpanded}
|
||||||
showIndicators
|
showIndicators
|
||||||
showNameTags
|
showNameTags
|
||||||
|
showRingingStatus
|
||||||
focusable
|
focusable
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
@@ -130,13 +144,20 @@ test("Screen share volume UI is hidden when screen share has no audio", async ()
|
|||||||
const toggleExpanded = vi.fn();
|
const toggleExpanded = vi.fn();
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<SpotlightTile
|
<SpotlightTile
|
||||||
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
vm={
|
||||||
|
new SpotlightTileViewModel(
|
||||||
|
constant([vm]),
|
||||||
|
constant(false),
|
||||||
|
constant("solid"),
|
||||||
|
)
|
||||||
|
}
|
||||||
targetWidth={300}
|
targetWidth={300}
|
||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggleExpanded={toggleExpanded}
|
onToggleExpanded={toggleExpanded}
|
||||||
showIndicators
|
showIndicators
|
||||||
showNameTags
|
showNameTags
|
||||||
|
showRingingStatus
|
||||||
focusable
|
focusable
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -165,13 +186,20 @@ test("SpotlightTile displays ringing media", async () => {
|
|||||||
const toggleExpanded = vi.fn();
|
const toggleExpanded = vi.fn();
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<SpotlightTile
|
<SpotlightTile
|
||||||
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
vm={
|
||||||
|
new SpotlightTileViewModel(
|
||||||
|
constant([vm]),
|
||||||
|
constant(false),
|
||||||
|
constant("solid"),
|
||||||
|
)
|
||||||
|
}
|
||||||
targetWidth={300}
|
targetWidth={300}
|
||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggleExpanded={toggleExpanded}
|
onToggleExpanded={toggleExpanded}
|
||||||
showIndicators
|
showIndicators
|
||||||
showNameTags
|
showNameTags
|
||||||
|
showRingingStatus
|
||||||
focusable={true}
|
focusable={true}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ import {
|
|||||||
VolumeOnIcon,
|
VolumeOnIcon,
|
||||||
VolumeOffSolidIcon,
|
VolumeOffSolidIcon,
|
||||||
VolumeOnSolidIcon,
|
VolumeOnSolidIcon,
|
||||||
VideoCallSolidIcon,
|
|
||||||
VoiceCallSolidIcon,
|
|
||||||
EndCallIcon,
|
|
||||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
import { animated } from "@react-spring/web";
|
import { animated } from "@react-spring/web";
|
||||||
import { type Observable, map } from "rxjs";
|
import { type Observable, map } from "rxjs";
|
||||||
@@ -34,7 +31,7 @@ import { useObservableRef } from "observable-hooks";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
||||||
import { Menu, MenuItem } from "@vector-im/compound-web";
|
import { Menu, MenuItem, Text } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import FullScreenMaximiseIcon from "../icons/FullScreenMaximise.svg?react";
|
import FullScreenMaximiseIcon from "../icons/FullScreenMaximise.svg?react";
|
||||||
import FullScreenMinimiseIcon from "../icons/FullScreenMinimise.svg?react";
|
import FullScreenMinimiseIcon from "../icons/FullScreenMinimise.svg?react";
|
||||||
@@ -56,6 +53,7 @@ import { type MediaViewModel } from "../state/media/MediaViewModel";
|
|||||||
import { Slider } from "../Slider";
|
import { Slider } from "../Slider";
|
||||||
import { platform } from "../Platform";
|
import { platform } from "../Platform";
|
||||||
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
||||||
|
import { RingingStatus } from "./RingingStatus";
|
||||||
|
|
||||||
interface SpotlightItemBaseProps {
|
interface SpotlightItemBaseProps {
|
||||||
ref?: Ref<HTMLDivElement>;
|
ref?: Ref<HTMLDivElement>;
|
||||||
@@ -67,6 +65,7 @@ interface SpotlightItemBaseProps {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
mxcAvatarUrl: string | undefined;
|
mxcAvatarUrl: string | undefined;
|
||||||
showNameTags: boolean;
|
showNameTags: boolean;
|
||||||
|
background: "solid" | "transparent";
|
||||||
focusable: boolean;
|
focusable: boolean;
|
||||||
"aria-hidden"?: boolean;
|
"aria-hidden"?: boolean;
|
||||||
}
|
}
|
||||||
@@ -80,6 +79,7 @@ interface SpotlightMemberMediaItemBaseProps extends SpotlightItemBaseProps {
|
|||||||
interface SpotlightUserMediaItemBaseProps extends SpotlightMemberMediaItemBaseProps {
|
interface SpotlightUserMediaItemBaseProps extends SpotlightMemberMediaItemBaseProps {
|
||||||
videoFit: "contain" | "cover";
|
videoFit: "contain" | "cover";
|
||||||
videoEnabled: boolean;
|
videoEnabled: boolean;
|
||||||
|
soundWaves: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpotlightLocalUserMediaItemProps extends SpotlightUserMediaItemBaseProps {
|
interface SpotlightLocalUserMediaItemProps extends SpotlightUserMediaItemBaseProps {
|
||||||
@@ -122,6 +122,7 @@ const SpotlightUserMediaItem: FC<SpotlightUserMediaItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const videoFit = useBehavior(vm.videoFit$);
|
const videoFit = useBehavior(vm.videoFit$);
|
||||||
const videoEnabled = useBehavior(vm.videoEnabled$);
|
const videoEnabled = useBehavior(vm.videoEnabled$);
|
||||||
|
const speaking = useBehavior(vm.speaking$);
|
||||||
|
|
||||||
// Whenever target bounds change, inform the viewModel
|
// Whenever target bounds change, inform the viewModel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -134,6 +135,7 @@ const SpotlightUserMediaItem: FC<SpotlightUserMediaItemProps> = ({
|
|||||||
RefAttributes<HTMLDivElement> = {
|
RefAttributes<HTMLDivElement> = {
|
||||||
videoFit,
|
videoFit,
|
||||||
videoEnabled,
|
videoEnabled,
|
||||||
|
soundWaves: props.background === "transparent" ? speaking : undefined,
|
||||||
targetWidth,
|
targetWidth,
|
||||||
targetHeight,
|
targetHeight,
|
||||||
...props,
|
...props,
|
||||||
@@ -204,28 +206,26 @@ const SpotlightMemberMediaItem: FC<SpotlightMemberMediaItemProps> = ({
|
|||||||
|
|
||||||
interface SpotlightRingingMediaItemProps extends SpotlightItemBaseProps {
|
interface SpotlightRingingMediaItemProps extends SpotlightItemBaseProps {
|
||||||
vm: RingingMediaViewModel;
|
vm: RingingMediaViewModel;
|
||||||
|
showStatus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightRingingMediaItem: FC<SpotlightRingingMediaItemProps> = ({
|
const SpotlightRingingMediaItem: FC<SpotlightRingingMediaItemProps> = ({
|
||||||
vm,
|
vm,
|
||||||
|
showStatus,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const pickupState = useBehavior(vm.pickupState$);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaView
|
<MediaView
|
||||||
video={undefined}
|
video={undefined}
|
||||||
unencryptedWarning={false}
|
unencryptedWarning={false}
|
||||||
status={
|
status={
|
||||||
pickupState === "ringing"
|
showStatus && (
|
||||||
? {
|
<Text as="span" size="md" weight="medium">
|
||||||
text: t("video_tile.calling"),
|
<RingingStatus vm={vm} />
|
||||||
Icon:
|
</Text>
|
||||||
vm.intent === "video" ? VideoCallSolidIcon : VoiceCallSolidIcon,
|
)
|
||||||
}
|
|
||||||
: { text: t("video_tile.call_ended"), Icon: EndCallIcon }
|
|
||||||
}
|
}
|
||||||
|
avatarStyle="translucent"
|
||||||
videoEnabled={false}
|
videoEnabled={false}
|
||||||
videoFit="cover"
|
videoFit="cover"
|
||||||
mirror={false}
|
mirror={false}
|
||||||
@@ -246,12 +246,15 @@ interface SpotlightItemProps {
|
|||||||
*/
|
*/
|
||||||
targetHeight: number;
|
targetHeight: number;
|
||||||
showNameTags: boolean;
|
showNameTags: boolean;
|
||||||
|
showRingingStatus: boolean;
|
||||||
|
background: "solid" | "transparent";
|
||||||
focusable: boolean;
|
focusable: boolean;
|
||||||
intersectionObserver$: Observable<IntersectionObserver>;
|
intersectionObserver$: Observable<IntersectionObserver>;
|
||||||
/**
|
/**
|
||||||
* Whether this item should act as a scroll snapping point.
|
* Whether this item should act as a scroll snapping point.
|
||||||
*/
|
*/
|
||||||
snap: boolean;
|
snap: boolean;
|
||||||
|
className?: string;
|
||||||
"aria-hidden"?: boolean;
|
"aria-hidden"?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,9 +264,12 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
|
|||||||
targetWidth,
|
targetWidth,
|
||||||
targetHeight,
|
targetHeight,
|
||||||
showNameTags,
|
showNameTags,
|
||||||
|
showRingingStatus,
|
||||||
|
background,
|
||||||
focusable,
|
focusable,
|
||||||
intersectionObserver$,
|
intersectionObserver$,
|
||||||
snap,
|
snap,
|
||||||
|
className,
|
||||||
"aria-hidden": ariaHidden,
|
"aria-hidden": ariaHidden,
|
||||||
}) => {
|
}) => {
|
||||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -290,19 +296,24 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
|
|||||||
const baseProps: SpotlightItemBaseProps & RefAttributes<HTMLDivElement> = {
|
const baseProps: SpotlightItemBaseProps & RefAttributes<HTMLDivElement> = {
|
||||||
ref,
|
ref,
|
||||||
"data-id": vm.id,
|
"data-id": vm.id,
|
||||||
className: classNames(styles.item, { [styles.snap]: snap }),
|
className: classNames(className, styles.item, { [styles.snap]: snap }),
|
||||||
targetWidth,
|
targetWidth,
|
||||||
targetHeight,
|
targetHeight,
|
||||||
userId: vm.userId,
|
userId: vm.userId,
|
||||||
displayName,
|
displayName,
|
||||||
mxcAvatarUrl,
|
mxcAvatarUrl,
|
||||||
showNameTags,
|
showNameTags,
|
||||||
|
background,
|
||||||
focusable,
|
focusable,
|
||||||
"aria-hidden": ariaHidden,
|
"aria-hidden": ariaHidden,
|
||||||
};
|
};
|
||||||
|
|
||||||
return vm.type === "ringing" ? (
|
return vm.type === "ringing" ? (
|
||||||
<SpotlightRingingMediaItem vm={vm} {...baseProps} />
|
<SpotlightRingingMediaItem
|
||||||
|
vm={vm}
|
||||||
|
showStatus={showRingingStatus}
|
||||||
|
{...baseProps}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SpotlightMemberMediaItem vm={vm} {...baseProps} />
|
<SpotlightMemberMediaItem vm={vm} {...baseProps} />
|
||||||
);
|
);
|
||||||
@@ -386,8 +397,13 @@ interface Props {
|
|||||||
targetHeight: number;
|
targetHeight: number;
|
||||||
showIndicators: boolean;
|
showIndicators: boolean;
|
||||||
showNameTags: boolean;
|
showNameTags: boolean;
|
||||||
|
showRingingStatus: boolean;
|
||||||
focusable: boolean;
|
focusable: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/**
|
||||||
|
* CSS class of the individual spotlight items.
|
||||||
|
*/
|
||||||
|
itemClassName?: string;
|
||||||
style?: ComponentProps<typeof animated.div>["style"];
|
style?: ComponentProps<typeof animated.div>["style"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,14 +416,17 @@ export const SpotlightTile: FC<Props> = ({
|
|||||||
targetHeight,
|
targetHeight,
|
||||||
showIndicators,
|
showIndicators,
|
||||||
showNameTags,
|
showNameTags,
|
||||||
|
showRingingStatus,
|
||||||
focusable = true,
|
focusable = true,
|
||||||
className,
|
className,
|
||||||
|
itemClassName,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
|
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const maximised = useBehavior(vm.maximised$);
|
const maximised = useBehavior(vm.maximised$);
|
||||||
|
const background = useBehavior(vm.background$);
|
||||||
const media = useBehavior(vm.media$);
|
const media = useBehavior(vm.media$);
|
||||||
const [visibleId, setVisibleId] = useState<string | undefined>(media[0]?.id);
|
const [visibleId, setVisibleId] = useState<string | undefined>(media[0]?.id);
|
||||||
const latestMedia = useLatest(media);
|
const latestMedia = useLatest(media);
|
||||||
@@ -488,9 +507,8 @@ export const SpotlightTile: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<animated.div
|
<animated.div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(className, styles.tile, {
|
className={classNames(className, styles.tile)}
|
||||||
[styles.maximised]: maximised,
|
data-maximised={maximised}
|
||||||
})}
|
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{canGoBack && (
|
{canGoBack && (
|
||||||
@@ -510,7 +528,9 @@ export const SpotlightTile: FC<Props> = ({
|
|||||||
vm={vm}
|
vm={vm}
|
||||||
targetWidth={targetWidth}
|
targetWidth={targetWidth}
|
||||||
targetHeight={targetHeight}
|
targetHeight={targetHeight}
|
||||||
|
showRingingStatus={showRingingStatus}
|
||||||
showNameTags={showNameTags}
|
showNameTags={showNameTags}
|
||||||
|
background={background}
|
||||||
focusable={focusable}
|
focusable={focusable}
|
||||||
intersectionObserver$={intersectionObserver$}
|
intersectionObserver$={intersectionObserver$}
|
||||||
// This is how we get the container to scroll to the right media
|
// This is how we get the container to scroll to the right media
|
||||||
@@ -518,6 +538,7 @@ export const SpotlightTile: FC<Props> = ({
|
|||||||
// remove all scroll snap points except for just the one media
|
// remove all scroll snap points except for just the one media
|
||||||
// that we want to bring into view
|
// that we want to bring into view
|
||||||
snap={scrollToId === null || scrollToId === vm.id}
|
snap={scrollToId === null || scrollToId === vm.id}
|
||||||
|
className={itemClassName}
|
||||||
aria-hidden={(scrollToId ?? visibleId) !== vm.id}
|
aria-hidden={(scrollToId ?? visibleId) !== vm.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, describe, it } from "vitest";
|
import { expect, describe, it } from "vitest";
|
||||||
import { render } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
import { TileAvatar } from "./TileAvatar";
|
import { TileAvatar } from "./TileAvatar";
|
||||||
|
|
||||||
describe("TileAvatar", () => {
|
describe("TileAvatar", () => {
|
||||||
it("should show loading spinner when loading", () => {
|
it("should show loading spinner when loading", () => {
|
||||||
const { container } = render(
|
render(
|
||||||
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={true} />,
|
<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", () => {
|
it("should not show loading spinner when not loading", () => {
|
||||||
const { container } = render(
|
render(
|
||||||
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={false} />,
|
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={false} />,
|
||||||
);
|
);
|
||||||
expect(container.querySelector(".loading")).not.toBeInTheDocument();
|
expect(screen.queryByLabelText("Loading…")).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user