mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-23 20:59:21 +00:00
Compare commits
238 Commits
johannes/i
...
performanc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d400323d0 | ||
|
|
608695fc95 | ||
|
|
a08a577e18 | ||
|
|
55b2d2b130 | ||
|
|
5b84b2c04d | ||
|
|
3af0744324 | ||
|
|
07567ed31d | ||
|
|
d6e097476e | ||
|
|
540eb306f4 | ||
|
|
f62428ba0d | ||
|
|
d579169d54 | ||
|
|
8daaa649eb | ||
|
|
2d5a4eefec | ||
|
|
d550430c8a | ||
|
|
78724acfcf | ||
|
|
20c7112812 | ||
|
|
a7c996f3ea | ||
|
|
4fbcbb4849 | ||
|
|
74102ac6c2 | ||
|
|
11a6fa9b45 | ||
|
|
507a6f29a5 | ||
|
|
e725bc5de3 | ||
|
|
c15d4c50a1 | ||
|
|
e1bc4a096b | ||
|
|
b524de9298 | ||
|
|
bfb5fb922f | ||
|
|
3f4d55343a | ||
|
|
73d8d2c2c3 | ||
|
|
fcfe73c50d | ||
|
|
5564015a8b | ||
|
|
62eeb10d38 | ||
|
|
2de7fdde10 | ||
|
|
5a96d1769e | ||
|
|
a443a55bec | ||
|
|
9b2d9fdd1f | ||
|
|
05ad9b2475 | ||
|
|
7962d02be5 | ||
|
|
5c35288162 | ||
|
|
aa6732da19 | ||
|
|
65ac11e952 | ||
|
|
0b21cff020 | ||
|
|
062e0a00fb | ||
|
|
53ae78c609 | ||
|
|
b046c4b569 | ||
|
|
46cb89b232 | ||
|
|
e20abb19fb | ||
|
|
bf76f47357 | ||
|
|
9c2d3d067d | ||
|
|
ae7ede3201 | ||
|
|
e5ff431b7a | ||
|
|
66a676f2f5 | ||
|
|
d33dedd038 | ||
|
|
1f2837f86a | ||
|
|
927cf68110 | ||
|
|
cd72ad0008 | ||
|
|
2d2e85e340 | ||
|
|
4da1b9a02a | ||
|
|
73758f7f77 | ||
|
|
c433c36687 | ||
|
|
3e9d7b7cf7 | ||
|
|
2a56d2afb8 | ||
|
|
bff41ad66a | ||
|
|
1d088911e1 | ||
|
|
f23eb96555 | ||
|
|
0d4e7defb6 | ||
|
|
023aec22ca | ||
|
|
bf4cee9898 | ||
|
|
147ff131f4 | ||
|
|
3e5edd03a6 | ||
|
|
0afefa4889 | ||
|
|
49a762832b | ||
|
|
3cf5cab1d2 | ||
|
|
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 | ||
|
|
1811b36205 | ||
|
|
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 | ||
|
|
89c148a530 | ||
|
|
e11c04ac87 | ||
|
|
9b070052a0 | ||
|
|
bb3d2cd95c | ||
|
|
d417bd8b90 | ||
|
|
f72a593eb4 | ||
|
|
f6cd06fc01 | ||
|
|
e77d143ce1 | ||
|
|
256219c4bf | ||
|
|
6af72f4c90 | ||
|
|
3a598ffe43 | ||
|
|
05797097c6 | ||
|
|
8b0f5054dd | ||
|
|
1c760f3ec4 | ||
|
|
699f383b57 | ||
|
|
6009988d3c | ||
|
|
8cda9777a5 | ||
|
|
049c432fc3 | ||
|
|
3ea89e8275 | ||
|
|
19fd7f42b1 | ||
|
|
0410be33cd | ||
|
|
002f9b0cea | ||
|
|
669f4264c7 | ||
|
|
dc6511b99e | ||
|
|
032968ea2d | ||
|
|
b2f02b0ff6 | ||
|
|
b31825d05b | ||
|
|
c4fe0223ab | ||
|
|
6ce79d8a52 | ||
|
|
b68abb7df4 | ||
|
|
1281ed31df | ||
|
|
e7377fb104 | ||
|
|
edb4dfa9ed | ||
|
|
7db13e1fc0 | ||
|
|
b9d769364d | ||
|
|
164fb99ace | ||
|
|
db26db266d | ||
|
|
fc3461dd54 | ||
|
|
8c8c9eef24 | ||
|
|
df7451c2cf | ||
|
|
9537c7657c | ||
|
|
6c105e971e | ||
|
|
cbbe6a812e | ||
|
|
0a572a9528 | ||
|
|
3ef3ebe897 | ||
|
|
0c284ed75b | ||
|
|
838d2f3c5d | ||
|
|
2a24fbcee1 | ||
|
|
be8a76868b | ||
|
|
d1ea63587b | ||
|
|
96a9115ee7 | ||
|
|
2ac6cdeb46 | ||
|
|
659da15cc2 | ||
|
|
9b2e1279bc | ||
|
|
3af160bb02 | ||
|
|
73ccc5f483 | ||
|
|
3a824dfff0 | ||
|
|
2b05c415c1 | ||
|
|
7ffa585f0d | ||
|
|
0690387b70 | ||
|
|
ba1957ef52 | ||
|
|
abf9022fd9 | ||
|
|
7fa3febf55 | ||
|
|
c1f528e120 | ||
|
|
9a2fc883b6 | ||
|
|
664a10c724 | ||
|
|
308a042076 | ||
|
|
b554ce4a87 | ||
|
|
b86909af52 | ||
|
|
d6635b7818 | ||
|
|
a6fc710d8c | ||
|
|
5dfd1648c5 | ||
|
|
1d5ac67324 | ||
|
|
2faab3280a | ||
|
|
2a3271b8b3 | ||
|
|
f45aa85aef |
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",
|
||||
},
|
||||
},
|
||||
};
|
||||
253
.github/labels.yml
vendored
Normal file
253
.github/labels.yml
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
- name: "A-1:1"
|
||||
description: "Calls between two people"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Big-Grid"
|
||||
description: "The freedom layout system used for >12 participants"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Developer-Experience"
|
||||
description: "Workflow of developing: building, linting, debugging, profiling, etc."
|
||||
color: "c5def5"
|
||||
- name: "A-E2EE"
|
||||
description: "End-to-end encryption"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Embedded"
|
||||
description: "Using the app embedded within other Matrix clients (as a widget)"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Feedback-Reporting"
|
||||
description: "Reporting process for bugs, debug logs (rageshakes), suggestions"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Freedom"
|
||||
description: "Freedom layout, where participants can be rearranged and resized"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Handset"
|
||||
description: "Audio playback through the earpiece of a phone. Also known as 'earpiece mode' or 'handset mode'."
|
||||
color: "bfd4f2"
|
||||
- name: "A-Huddle"
|
||||
description: "Ad-hoc calls in a room notifying others"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Lobby"
|
||||
description: "The page before joining a call"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Login"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Matrix2.0"
|
||||
description: "Issues relating to the Matrix 2.0 / MSC4143 work, such as sticky events and multi-sfu"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Media-Devices"
|
||||
color: "BFD4F2"
|
||||
- name: "A-Media-Quality"
|
||||
description: "Distortions or glitches in audio/video"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Meeting"
|
||||
description: "Scheduled call on the calendar"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Mobile"
|
||||
description: "Using the app on a mobile device"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Moderation"
|
||||
description: "Access to calls and powers within calls"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Performance"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Reactions"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Registration"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Screen-Sharing"
|
||||
color: "bfd4f2"
|
||||
- name: "A-SDK"
|
||||
description: "SDK for building MatrixRTC + LiveKit widgets"
|
||||
color: "c5def5"
|
||||
- name: "A-Settings"
|
||||
color: "bfd4f2"
|
||||
- name: "A-SFU"
|
||||
description: "Routing calls through a selective forwarding unit"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Signaling"
|
||||
description: "Call signaling"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Simulcast"
|
||||
description: "Automatic selection of variable video resolutions"
|
||||
color: "bfd4f2"
|
||||
- name: "A-SPA"
|
||||
description: "Standalone application accessed via call links"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Spatial-Audio"
|
||||
description: "Directional audio based on where a speaker appears on screen"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Speech-Enhancement"
|
||||
description: "Techniques to enhance the intelligibility of speech in calls"
|
||||
color: "c5def5"
|
||||
- name: "A-Split-Grid"
|
||||
description: "The freedom layout system used for ≤12 participants"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Spotlight"
|
||||
description: "Spotlight layout, where the active speaker is foregrounded"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Telemetry-Posthog"
|
||||
description: "Share opt in usage data for optimizing the app via posthog"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Testing"
|
||||
description: "Integration tests, unit tests, etc."
|
||||
color: "bfd4f2"
|
||||
- name: "A-Video-Rooms"
|
||||
description: "Rooms reserved exclusively for calling"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Walkie-Talkie"
|
||||
description: "Walkie-talkie / PTT (push-to-talk) mode"
|
||||
color: "bfd4f2"
|
||||
- name: "A11y"
|
||||
description: "Accessibility"
|
||||
color: "4ADEC0"
|
||||
- name: "backport-candidate"
|
||||
description: "Something that is a candidate for backport to a particular release branch"
|
||||
color: "0B8D85"
|
||||
- name: "customer-retainer"
|
||||
color: "F44A5F"
|
||||
- name: "dependencies"
|
||||
description: "Pull requests that update a dependency file"
|
||||
color: "0366d6"
|
||||
- name: "development build"
|
||||
description: "runs yarn build process in development mode"
|
||||
color: "1d76db"
|
||||
- name: "Discord"
|
||||
description: "Use case familiar to Discord users"
|
||||
color: "3670d2"
|
||||
- name: "docker build"
|
||||
description: "Creates a docker image for this PR"
|
||||
color: "0e8a16"
|
||||
- name: "EPIC"
|
||||
color: "5319E7"
|
||||
- name: "good first issue"
|
||||
description: "Good for newcomers"
|
||||
color: "7057ff"
|
||||
- name: "Help Wanted"
|
||||
description: "Community contributions are welcome!"
|
||||
color: "159818"
|
||||
- name: "I18n"
|
||||
description: "Internationalisation"
|
||||
color: "d4c5f9"
|
||||
- name: "O-Frequent"
|
||||
description: "Affects or can be seen by most users regularly or impacts most users' first experience"
|
||||
color: "0052CC"
|
||||
- name: "O-Occasional"
|
||||
description: "Affects or can be seen by some users regularly or most users rarely"
|
||||
color: "1D76DB"
|
||||
- name: "O-Uncommon"
|
||||
description: "Most users are unlikely to come across this or unexpected workflow"
|
||||
color: "C5DEF5"
|
||||
- name: "p1"
|
||||
description: "Must fix/implement before this is usable as a product"
|
||||
color: "D93F0B"
|
||||
- name: "p2"
|
||||
description: "Should fix/implement, but not at the expense of p1s"
|
||||
color: "FBCA04"
|
||||
- name: "p3"
|
||||
description: "Could fix/implement when time allows"
|
||||
color: "0E8A16"
|
||||
- name: "PR-Breaking-Change"
|
||||
description: "A Pull request that changes EC in a way that is incompatible to the previous version."
|
||||
color: "D93F0B"
|
||||
- name: "PR-Bug-Fix"
|
||||
description: "Release note category. A PR that fixes a bug."
|
||||
color: "C2E0C6"
|
||||
- name: "PR-Developer-Experience"
|
||||
description: "Release note category. A PR that does not change EC but improves working with the repository."
|
||||
color: "C2E0C6"
|
||||
- name: "PR-Documentation"
|
||||
description: "Release note category. A PR that improves the documentation."
|
||||
color: "C2E0C6"
|
||||
- name: "PR-Feature"
|
||||
description: "Release note category. A PR that introduces a new user facing feature."
|
||||
color: "C2E0C6"
|
||||
- name: "PR-Improvement"
|
||||
description: "Release note category. A PR that improves EC's performance or stability."
|
||||
color: "C2E0C6"
|
||||
- name: "PR-Task"
|
||||
description: "Release note category. A PR that is hidden from release note."
|
||||
color: "C2E0C6"
|
||||
- name: "Privacy"
|
||||
color: "f41192"
|
||||
- name: "Roadmap"
|
||||
color: "57457E"
|
||||
- name: "S-Critical"
|
||||
description: "Prevents work, causes data loss and/or has no workaround"
|
||||
color: "bd0026"
|
||||
- name: "S-Major"
|
||||
description: "Severely degrades major functionality or product features, with no satisfactory workaround"
|
||||
color: "fc4e2a"
|
||||
- name: "S-Minor"
|
||||
description: "Impairs non-critical functionality or suitable workarounds exist"
|
||||
color: "feb24c"
|
||||
- name: "S-Tolerable"
|
||||
description: "Low/no impact on users"
|
||||
color: "ffeda0"
|
||||
- name: "Security"
|
||||
color: "b3e5fc"
|
||||
- name: "storybook build"
|
||||
description: "Build and deploy the storybook frontend to netlify."
|
||||
color: "45cd61"
|
||||
- name: "T-Defect"
|
||||
description: "Something isn't working: bugs, crashes, hangs, vulnerabilities, or other reported problems"
|
||||
color: "98e6ae"
|
||||
- name: "T-Enhancement"
|
||||
description: "New features, changes in functionality, performance boosts, user-facing improvements"
|
||||
color: "98e6ae"
|
||||
- name: "T-Other"
|
||||
description: "Questions, user support, anything else"
|
||||
color: "98e6ae"
|
||||
- name: "T-Task"
|
||||
description: "Refactoring, enabling or disabling functionality, other engineering tasks"
|
||||
color: "98e6ae"
|
||||
- name: "X-Blocked"
|
||||
description: "Cannot be merged due to external dependencies"
|
||||
color: "ff7979"
|
||||
- name: "X-Cannot-Reproduce"
|
||||
description: "Needs reproduction steps"
|
||||
color: "ff7979"
|
||||
- name: "X-Needs-Design"
|
||||
description: "May require input from the design team"
|
||||
color: "ff7979"
|
||||
- name: "X-Needs-Info"
|
||||
description: "This issue is blocked awaiting information from the reporter"
|
||||
color: "ff7979"
|
||||
- name: "X-Needs-Investigation"
|
||||
color: "ff7979"
|
||||
- name: "X-Needs-Product"
|
||||
description: "More input needed from the Product team"
|
||||
color: "ff7979"
|
||||
- name: "X-Regression"
|
||||
color: "ff7979"
|
||||
- name: "X-Release-Blocker"
|
||||
color: "ff7979"
|
||||
- name: "X-Spec-Changes"
|
||||
description: "May require spec changes"
|
||||
color: "ff7979"
|
||||
- name: "X-Won't-Fix"
|
||||
description: "This will not be worked on"
|
||||
color: "ff7979"
|
||||
- name: "Z-Community-Testing"
|
||||
description: "Issues found during the community testing sessions"
|
||||
color: "efefef"
|
||||
- name: "Z-Could"
|
||||
color: "ededed"
|
||||
- name: "Z-Design"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test"
|
||||
color: "aaaaaa"
|
||||
- name: "Z-Media-Failure"
|
||||
description: "Someone's audio or video isn't coming through"
|
||||
color: "ededed"
|
||||
- name: "Z-Must"
|
||||
color: "ededed"
|
||||
- name: "Z-Platform-Specific"
|
||||
color: "ededed"
|
||||
- name: "Z-Power-Users"
|
||||
color: "ededed"
|
||||
- name: "Z-ProductPolish"
|
||||
color: "aaaaaa"
|
||||
- name: "Z-Should"
|
||||
color: "ededed"
|
||||
- name: "Z-Splitbrain"
|
||||
description: "Someone who should be on the call isn't showing up"
|
||||
color: "ededed"
|
||||
10
.github/workflows/lint.yaml
vendored
10
.github/workflows/lint.yaml
vendored
@@ -2,7 +2,7 @@ name: Lint, format & type check
|
||||
on:
|
||||
pull_request: {}
|
||||
jobs:
|
||||
prettier:
|
||||
lint:
|
||||
name: Lint, format & type check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -20,12 +20,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
# ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present)
|
||||
run: "pnpm install --frozen-lockfile --ignore-pnpmfile"
|
||||
- name: Prettier
|
||||
run: "pnpm run prettier:check"
|
||||
- name: Formatting
|
||||
run: "pnpm run format:check"
|
||||
- name: i18n
|
||||
run: "pnpm run i18n:check"
|
||||
- name: ESLint
|
||||
run: "pnpm run lint:eslint"
|
||||
- name: Lint
|
||||
run: "pnpm run lint:oxlint"
|
||||
- name: Type check
|
||||
run: "pnpm run lint:types"
|
||||
- name: Dead code analysis
|
||||
|
||||
23
.github/workflows/sync-labels.yml
vendored
Normal file
23
.github/workflows/sync-labels.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Sync labels
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
push:
|
||||
branches:
|
||||
- livekit
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
- .github/workflows/sync-labels.yml
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
|
||||
with:
|
||||
LABELS: |
|
||||
.github/labels.yml
|
||||
DELETE: true
|
||||
WET: true
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
34
.github/workflows/test.yaml
vendored
34
.github/workflows/test.yaml
vendored
@@ -7,10 +7,6 @@ jobs:
|
||||
vitest:
|
||||
name: Run unit tests
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
# Make sure to grab the latest version of the Playwright image
|
||||
# https://playwright.dev/docs/docker#pull-the-image
|
||||
image: mcr.microsoft.com/playwright:v1.60.0-noble
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
@@ -26,6 +22,18 @@ jobs:
|
||||
- name: Install dependencies
|
||||
# ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present)
|
||||
run: "pnpm install --frozen-lockfile --ignore-pnpmfile"
|
||||
- name: Get Playwright version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[0].devDependencies["@playwright/test"].version')" >> $GITHUB_ENV
|
||||
- name: Cache Playwright binaries
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
- name: Install Playwright binaries
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm playwright install --with-deps
|
||||
- name: Vitest
|
||||
run: "pnpm run test:coverage"
|
||||
- name: Upload to codecov
|
||||
@@ -52,12 +60,22 @@ jobs:
|
||||
- name: Install dependencies
|
||||
# ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present)
|
||||
run: pnpm install --frozen-lockfile --ignore-pnpmfile
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
- name: Get Playwright version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[0].devDependencies["@playwright/test"].version')" >> $GITHUB_ENV
|
||||
- name: Cache Playwright binaries
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
- name: Install Playwright binaries
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm playwright install --with-deps
|
||||
- name: Run backend components
|
||||
run: |
|
||||
docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml pull
|
||||
docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up -d
|
||||
docker compose -f docker-compose-dev.yml -f docker-compose-playwright.yml pull
|
||||
docker compose -f docker-compose-dev.yml -f docker-compose-playwright.yml up -d
|
||||
docker ps
|
||||
- name: Run Playwright tests
|
||||
env:
|
||||
|
||||
11
.github/workflows/translations-download.yaml
vendored
11
.github/workflows/translations-download.yaml
vendored
@@ -40,8 +40,13 @@ jobs:
|
||||
- name: Fix the owner of the downloaded files
|
||||
run: "sudo chown runner:docker -R locales"
|
||||
|
||||
- name: Prettier
|
||||
run: pnpm prettier:format
|
||||
# Localazy doesn't write file contents in the same order that i18next-cli's
|
||||
# extractor uses. We re-run the extractor after downloading to fix the order.
|
||||
- name: i18n
|
||||
run: pnpm i18n
|
||||
|
||||
- name: Formatting
|
||||
run: pnpm format
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
@@ -53,7 +58,7 @@ jobs:
|
||||
title: Localazy Download
|
||||
commit-message: Translations updates
|
||||
labels: |
|
||||
T-Task
|
||||
PR-Task
|
||||
|
||||
- name: Enable automerge
|
||||
run: gh pr merge --merge --auto "$PR_NUMBER"
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -15,9 +15,9 @@ jobs:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
|
||||
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 @@
|
||||
{}
|
||||
14
README.md
14
README.md
@@ -212,7 +212,7 @@ See also:
|
||||
|
||||
### Backend
|
||||
|
||||
A docker compose file `dev-backend-docker-compose.yml` is provided to start the
|
||||
A docker compose file `docker-compose-dev.yml` is provided to start the
|
||||
whole stack of components which is required for a local development environment
|
||||
including federation:
|
||||
|
||||
@@ -239,7 +239,7 @@ the backend components:
|
||||
```sh
|
||||
pnpm backend
|
||||
# or for podman-compose:
|
||||
# podman-compose -f dev-backend-docker-compose.yml up
|
||||
# podman-compose -f docker-compose-dev.yml up
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
@@ -359,6 +359,16 @@ Usage and other technical details about the project can be found here:
|
||||
|
||||
[**Docs**](./docs/README.md)
|
||||
|
||||
## GitHub Labels
|
||||
|
||||
GitHub labels in this repository are maintained in the [`labels.yml`](.github/labels.yml) file and
|
||||
automatically synced to GitHub using the [`sync-labels` workflow](.github/workflows/sync-labels.yml).
|
||||
We do this so that we can reuse the labels between repositories.
|
||||
|
||||
> [!WARNING]
|
||||
> Do not manually edit labels in the GitHub UI. Any manual changes will be overridden by the
|
||||
> workflow on its next invocation.
|
||||
|
||||
## 📝 Copyright & License
|
||||
|
||||
Copyright 2021-2025 New Vector Ltd
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
node: "current",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
runtime: "automatic",
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: ["babel-plugin-transform-vite-meta-env"],
|
||||
};
|
||||
@@ -12,7 +12,7 @@
|
||||
"feature_use_device_session_member_events": true
|
||||
},
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
"matrix_rtc_mode": "legacy",
|
||||
"matrix_rtc_mode": "compatibility",
|
||||
"matrix_rtc_session": {
|
||||
"wait_for_key_rotation_ms": 3000,
|
||||
"membership_event_expiry_ms": 180000000,
|
||||
|
||||
@@ -47,7 +47,7 @@ services:
|
||||
- ecbackend
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.10.1
|
||||
image: livekit/livekit-server:v1.13.4
|
||||
pull_policy: always
|
||||
hostname: livekit-sfu
|
||||
command: --dev --config /etc/livekit.yaml
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
- ecbackend
|
||||
|
||||
livekit-1:
|
||||
image: livekit/livekit-server:v1.10.1
|
||||
image: livekit/livekit-server:v1.13.4
|
||||
pull_policy: always
|
||||
hostname: livekit-sfu-1
|
||||
command: --dev --config /etc/livekit.yaml
|
||||
@@ -156,7 +156,7 @@ services:
|
||||
nginx:
|
||||
# see backend/dev_tls_setup for how to generate the tls certs
|
||||
hostname: synapse.m.localhost
|
||||
image: nginx:latest
|
||||
image: nginx:latest@sha256:4ae259ae64fbedb67918c07d167fdcb0e05855a1615480ca445bea485e7d65ff
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z
|
||||
19
docker-compose-playwright.yml
Normal file
19
docker-compose-playwright.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
# This file contains overrides to docker-compose-dev.yml and should
|
||||
# only be used in combination with that file.
|
||||
services:
|
||||
synapse:
|
||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||
image: ghcr.io/element-hq/synapse:latest@sha256:da325af40104051923899e5f5a2f1d537e6e3ccf2f0f38285689ae9bbfdb190a
|
||||
volumes:
|
||||
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z
|
||||
synapse-1:
|
||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||
image: ghcr.io/element-hq/synapse:latest@sha256:da325af40104051923899e5f5a2f1d537e6e3ccf2f0f38285689ae9bbfdb190a
|
||||
volumes:
|
||||
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||
element-web:
|
||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||
image: ghcr.io/element-hq/element-web:develop@sha256:0183c0a7f4292252349ddcff2de92a83a4571b283da49cc54adb9efbf3d20695
|
||||
element-web-1:
|
||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||
image: ghcr.io/element-hq/element-web:develop@sha256:0183c0a7f4292252349ddcff2de92a83a4571b283da49cc54adb9efbf3d20695
|
||||
@@ -7,6 +7,9 @@ A few aspects of Element Call's interface can be controlled through a global API
|
||||
- `controls.canEnterPip(): boolean` Determines whether it's possible to enter picture-in-picture mode.
|
||||
- `controls.enablePip(): void` Puts the call interface into picture-in-picture mode. Throws if not in a call.
|
||||
- `controls.disablePip(): void` Takes the call interface out of picture-in-picture mode, restoring it to its natural display mode. Throws if not in a call.
|
||||
- `controls.onPipMediaOrientationUpdate: ((orientation: "landscape"|"portrait") => void) | undefined` Callback called whenever the PiP media orientation changes.
|
||||
The client should track this value to already initiate the pip in the right orientation.
|
||||
It should update the orientation of the current Pip window when called.
|
||||
|
||||
## Audio devices
|
||||
|
||||
|
||||
@@ -86,10 +86,11 @@ to implement
|
||||
In the context of MatrixRTC, we suggest using a single hostname for backend
|
||||
communication by implementing endpoint routing within a reverse proxy setup. For
|
||||
the example above, this results in:
|
||||
| Service | Endpoint | Example |
|
||||
| -------- | ------- | ------- |
|
||||
|
||||
| Service | Endpoint | Example |
|
||||
| --------------------------------------------------------------------------------- | -------------- | ------------------------------------ |
|
||||
| [Livekit SFU](https://github.com/livekit/livekit) WebSocket signalling connection | `/livekit/sfu` | `matrix-rtc.example.com/livekit/sfu` |
|
||||
| [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
|
||||
| [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
|
||||
|
||||
Using Nginx, you can achieve this by:
|
||||
|
||||
|
||||
@@ -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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
||||
### Widget-only parameters
|
||||
|
||||
@@ -9,4 +9,4 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
|
||||
|
||||
[plugins]
|
||||
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"
|
||||
}
|
||||
20
knip.ts
20
knip.ts
@@ -11,31 +11,35 @@ export default {
|
||||
vite: {
|
||||
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: [
|
||||
// This is deprecated, so Knip doesn't actually recognize it as a globally
|
||||
// installed binary. TODO We should switch to Compose v2:
|
||||
// https://docs.docker.com/compose/migrate/
|
||||
"docker-compose",
|
||||
// This is a shell built-in.
|
||||
"printf",
|
||||
],
|
||||
ignoreFiles: [
|
||||
"scripts/.pnpmfile.cjs",
|
||||
// Deliberately added prior to any component or business logic
|
||||
// implementation
|
||||
"src/state/ServiceInterruptionsViewModel.ts",
|
||||
],
|
||||
ignoreFiles: ["scripts/.pnpmfile.cjs"],
|
||||
ignoreDependencies: [
|
||||
// Used in CSS
|
||||
"normalize.css",
|
||||
// Used for its global type declarations
|
||||
"@types/grecaptcha",
|
||||
// Because we use matrix-js-sdk as a Git dependency rather than consuming
|
||||
// the proper release artifacts, and also import directly from src/, we're
|
||||
// forced to re-install some of the types that it depends on even though
|
||||
// these look unused to Knip
|
||||
"@types/content-type",
|
||||
"@types/sdp-transform",
|
||||
"@types/uuid",
|
||||
// We obviously use this, but if the package has been linked with pnpm link,
|
||||
// then Knip will flag it as a false positive
|
||||
// https://github.com/webpro-nl/knip/issues/766
|
||||
"@vector-im/compound-web",
|
||||
"matrix-widget-api",
|
||||
// Used by oxlint
|
||||
"eslint-plugin-element-call",
|
||||
"eslint-plugin-storybook",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
} satisfies KnipConfig;
|
||||
|
||||
@@ -102,11 +102,15 @@
|
||||
"connection_lost_description": "Hovor byl přerušen.",
|
||||
"e2ee_unsupported": "Nekompatibilní prohlížeč",
|
||||
"e2ee_unsupported_description": "Váš webový prohlížeč nepodporuje šifrované hovory. Mezi podporované prohlížeče patří Chrome, Safari a Firefox 117+.",
|
||||
"failed_to_start_livekit": "Nepodařilo se navázat připojení k Livekitu",
|
||||
"generic": "Něco se pokazilo.",
|
||||
"generic_description": "Odeslání protokolů ladění nám pomůže vystopovat problém.",
|
||||
"insufficient_capacity": "Nedostatečná kapacita",
|
||||
"insufficient_capacity_description": "Server dosáhl své maximální kapacity a v tuto chvíli se nemůžete připojit k hovoru. Zkuste to později nebo se obraťte na správce serveru, pokud problém přetrvává.",
|
||||
"matrix_rtc_transport_missing": "Server není nakonfigurován pro práci s {{brand}}. Obraťte se na správce serveru (Doména: {{domain}}, Kód chyby: {{ errorCode }}).",
|
||||
"membership_manager": "Chyba Správce členství",
|
||||
"membership_manager_description": "Správce členství musel být ukončen. To je způsobeno mnoha po sobě jdoucími neúspěšnými síťovými požadavky.",
|
||||
"no_matrix_2_authorization_service": "Autorizační služba vašeho mediálního serveru (SFU) je zastaralá.",
|
||||
"open_elsewhere": "Otevřeno na jiné kartě",
|
||||
"open_elsewhere_description": "{{brand}} byl otevřen v jiné záložce. Pokud to nezní správně, zkuste stránku znovu načíst.",
|
||||
"room_creation_restricted": "Nepodařilo se vytvořit hovor",
|
||||
@@ -141,6 +145,7 @@
|
||||
},
|
||||
"layout_grid_label": "Mřížka",
|
||||
"layout_spotlight_label": "Soustředěný mód",
|
||||
"layout_switch_label": "Rozvržení",
|
||||
"lobby": {
|
||||
"ask_to_join": "Žádost o připojení k hovoru",
|
||||
"join_as_guest": "Připojte se jako host",
|
||||
@@ -241,11 +246,14 @@
|
||||
"version": "{{productName}}verze: {{version}}",
|
||||
"video_tile": {
|
||||
"always_show": "Vždy zobrazit",
|
||||
"call_ended": "Hovor ukončen",
|
||||
"calling": "Volání…",
|
||||
"camera_starting": "Načítání videa...",
|
||||
"collapse": "Sbalit",
|
||||
"expand": "Rozbalit",
|
||||
"mute_for_me": "Pro mě ztlumit",
|
||||
"muted_for_me": "Pro mě ztlumené",
|
||||
"screen_share_volume": "Hlasitost sdílení obrazovky",
|
||||
"volume": "Hlasitost",
|
||||
"waiting_for_media": "Čekání na média..."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"peer_connection_timeout_description": "Connection to the media server timed out. Try switching to a different network or disabling your VPN. If the problem persists, see our <0>troubleshooting guide</0> or contact your server administrator.",
|
||||
"room_creation_restricted": "Failed to create call",
|
||||
"room_creation_restricted_description": "Call creation might be restricted to authorized users only. Try again later, or contact your server admin if the problem persists.",
|
||||
"sticky_events_required": "Homeserver does not support Matrix 2.0 calls",
|
||||
"sticky_events_required_description": "This deployment is configured to use Matrix 2.0 call mode, but the homeserver does not advertise support for sticky events (MSC4354). Ask your server admin to upgrade, or switch the deployment to a compatible mode.",
|
||||
"unexpected_ec_error": "An unexpected error occurred (<0>Error Code:</0> <1>{{ errorCode }}</1>). Please contact your server admin."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -203,6 +205,7 @@
|
||||
"blur_not_supported_by_browser": "(Background blur is not supported by this device.)",
|
||||
"developer_tab_title": "Developer",
|
||||
"devices": {
|
||||
"activating": "Activating…",
|
||||
"camera": "Camera",
|
||||
"camera_numbered": "Camera {{n}}",
|
||||
"change_device_button": "Change audio device",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"user_menu": "Kasutajamenüü"
|
||||
},
|
||||
"action": {
|
||||
"blur_background": "Hägusta tausta",
|
||||
"close": "Sulge",
|
||||
"copy_link": "Kopeeri link",
|
||||
"edit": "Muuda",
|
||||
@@ -72,8 +73,20 @@
|
||||
"environment_variables": "Keskkonnamuutujad",
|
||||
"hostname": "Hosti nimi: {{hostname}}",
|
||||
"livekit_server_info": "LiveKiti serveri teave",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"livekit_sfu": "LiveKiti meediaedastusserver (SFU): {{url}}",
|
||||
"matrixRTCMode": {
|
||||
"Comptibility": {
|
||||
"description": "Ühildub koduserveritega, mis ei toeta määratud kestusega sündmuseid (kuid kõik teised EC kliendid on v0.17.0 või hilisemad)",
|
||||
"label": "Ühilduvus: olekusündmused ja mitu meediaedastusserverit (SFU)"
|
||||
},
|
||||
"Legacy": {
|
||||
"description": "Ühildub EC vanemate versioonidega, millel puudub mitme meediaedastusserveri (SFU) tugi",
|
||||
"label": "Vana lahendus: oleku üritused ja vanim meediaedastusserver (SFU)"
|
||||
},
|
||||
"Matrix_2_0": {
|
||||
"description": "Ühildub ainult määratud kestusega sündmuseid toetavate koduserveritega ja kõigi EC-klientidega alates versioonist 0.17.0",
|
||||
"label": "Matrix 2.0: määratud kestusega sündmused ja mitu meediaedastusserverit (SFU)"
|
||||
},
|
||||
"title": "MatrixRTC režiim"
|
||||
},
|
||||
"matrix_id": "Matrixi kasutajatunnus: {{id}}",
|
||||
@@ -95,13 +108,20 @@
|
||||
"generic_description": "Silumis- ja vealogide saatmine võib aidata meid vea põhjuseni jõuda.",
|
||||
"insufficient_capacity": "Mittepiisav jõudlus",
|
||||
"insufficient_capacity_description": "Serveri jõudluse ülempiir on hetkel ületatud ja sa ei saa hetkel selle kõnega liituda. Proovi hiljem uuesti või kui probleem kestab kauem, siis võta ühendust serveri haldajaga.",
|
||||
"livekit_connection_error": "Livekiti serveriga ühendamine ei õnnestunud",
|
||||
"livekit_connection_error_description": "Livekiti serveriga ühendamisel tekkis viga (<1>Põhjus: </1> <2>{{ reason }}</2>).",
|
||||
"matrix_rtc_transport_missing": "See server pole seadistatud töötama rakendusega {{brand}}. Palun võta ühendust serveri halduriga (domeen: {{domain}}, veakood: {{ errorCode }}).",
|
||||
"membership_manager": "Viga liikmelisuse haldamisel",
|
||||
"membership_manager_description": "Liikmelisuse haldur pidi oma töö lõpetama. Selle põhjuseks olid paljud järjestikused ebaõnnestunud võrgupäringud.",
|
||||
"no_matrix_2_authorization_service": "Sinu meediaedastusserveri (SFU) autoriseerimisteenus on aegunud.",
|
||||
"open_elsewhere": "Avatud teisel vahekaardil",
|
||||
"open_elsewhere_description": "{{brand}} on avatud teisel vahekaardil. Kui see ei tundu olema õige, proovi selle lehe uuesti laadimist.",
|
||||
"peer_connection_timeout": "Ühendus aegus",
|
||||
"peer_connection_timeout_description": "Meediaserveriga ühenduspäring aegus. Proovi kasutada muud võrku või keelata VPN-i kasutamine. Kui probleem püsib, vaata meie <0> veaotsingu juhendit</0> või võta ühendust oma serveri peakasutajaga.",
|
||||
"room_creation_restricted": "Kõne loomine ei õnnestunud",
|
||||
"room_creation_restricted_description": "Kõne loomine võib olla lubatud ainult volitatud kasutajatele. Proovi hiljem uuesti või probleemi püsimisel võta ühendust oma serveri haldajaga.",
|
||||
"sticky_events_required": "Koduserver ei toeta Matrix 2.0 kõnesid",
|
||||
"sticky_events_required_description": "See server on seadistatud kasutama Matrix 2.0 kõnerežiimi, kuid koduserver ei teata, et tal selleks vajalike sündmuste (MSC4354) tugi. Palu serveri peakasutajal see uuendada või kasuta koduserveris ühilduvat režiimi.",
|
||||
"unexpected_ec_error": "Tekkis ootamatu viga (<0>Veakood:</0> <1>{{ errorCode }}</1>). Palun võta ühendust serveri haldajaga."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -132,6 +152,7 @@
|
||||
},
|
||||
"layout_grid_label": "Ruudustik",
|
||||
"layout_spotlight_label": "Rambivalgus",
|
||||
"layout_switch_label": "Paigutus",
|
||||
"lobby": {
|
||||
"ask_to_join": "Küsi võimalust liituda kõnega",
|
||||
"join_as_guest": "Liitu külalisena",
|
||||
@@ -184,6 +205,7 @@
|
||||
"blur_not_supported_by_browser": "(Tausta hägustamine pole selles seadmes toetatud.)",
|
||||
"developer_tab_title": "Arendaja",
|
||||
"devices": {
|
||||
"activating": "Aktiveerin…",
|
||||
"camera": "Kaamera",
|
||||
"camera_numbered": "Kaamera {{n}}",
|
||||
"change_device_button": "Muuda heliseadet",
|
||||
@@ -223,6 +245,7 @@
|
||||
"stop_video_button_label": "Peata videovoog",
|
||||
"submitting": "Saadan…",
|
||||
"switch_camera": "Vaheta kaamerat",
|
||||
"technical_details": "Tehnilised üksikasjad",
|
||||
"unauthenticated_view_body": "Sa pole veel registreerunud? <2>Loo kasutajakonto</2>",
|
||||
"unauthenticated_view_login_button": "Logi oma kontosse sisse",
|
||||
"unauthenticated_view_ssla_caption": "Klõpsides „Jätka“ nõustud sa meie <2>Tarkvara ja teenuste litsentseerimise lepinguga (Software and Services License Agreement - SSLA)</2>",
|
||||
@@ -230,11 +253,14 @@
|
||||
"version": "{{productName}}, versioon: {{version}}",
|
||||
"video_tile": {
|
||||
"always_show": "Näita alati",
|
||||
"call_ended": "Kõne lõppes",
|
||||
"calling": "Helistan…",
|
||||
"camera_starting": "Video on laadimisel...",
|
||||
"collapse": "Näita vähem",
|
||||
"expand": "Näita rohkem",
|
||||
"mute_for_me": "Summuta minu jaoks",
|
||||
"muted_for_me": "Minule summutatud",
|
||||
"screen_share_volume": "Helivaljus ekraanijagaamisel",
|
||||
"volume": "Helivaljus",
|
||||
"waiting_for_media": "Ootame kuni meedia on olemas..."
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"user_menu": "Käyttäjävalikko"
|
||||
},
|
||||
"action": {
|
||||
"blur_background": "Sumenna tausta",
|
||||
"close": "Sulje",
|
||||
"copy_link": "Kopioi linkki",
|
||||
"edit": "Muokkaa",
|
||||
@@ -107,14 +108,20 @@
|
||||
"generic_description": "Vianmäärityslokien lähettäminen auttaa meitä jäljittämään ongelman.",
|
||||
"insufficient_capacity": "Riittämätön kapasiteetti",
|
||||
"insufficient_capacity_description": "Palvelin on saavuttanut maksimikapasiteettinsa, etkä voi liittyä puheluun tällä hetkellä. Yritä myöhemmin uudelleen tai ota yhteyttä palvelimen ylläpitäjään, jos ongelma jatkuu.",
|
||||
"livekit_connection_error": "Yhteyden muodostaminen Livekit-palvelimeen epäonnistui",
|
||||
"livekit_connection_error_description": "Livekit-palvelimeen yhteyden muodostamisessa tapahtui virhe (<1>Syy: </1> <2>{{ reason }}</2>).",
|
||||
"matrix_rtc_transport_missing": "Palvelinta ei ole määritetty toimimaan {{brand}} -sovelluksen kanssa. Ota yhteyttä palvelimen ylläpitäjään (Verkkotunnus: {{domain}}, Virhekoodi: {{ errorCode }}).",
|
||||
"membership_manager": "Jäsenyydenhallinnan virhe",
|
||||
"membership_manager_description": "Jäsenyyshallinta jouduttiin sulkemaan. Tämä johtui useista peräkkäisistä epäonnistuneista verkkopyynnöistä.",
|
||||
"no_matrix_2_authorization_service": "Mediapalvelimesi (SFU) valtuutuspalvelu on vanhentunut.",
|
||||
"open_elsewhere": "Avattu toisessa välilehdessä",
|
||||
"open_elsewhere_description": "{{brand}} on avattu toisessa välilehdessä. Jos tämä ei kuulosta oikealta, yritä ladata sivu uudelleen.",
|
||||
"peer_connection_timeout": "Yhteyden aikakatkaisu",
|
||||
"peer_connection_timeout_description": "Yhteys mediapalvelimeen aikakatkaistiin. Kokeile vaihtaa verkkoa tai poistaa VPN käytöstä. Jos ongelma jatkuu, katso <0>vianetsintäoppaamme</0> tai ota yhteyttä palvelimesi ylläpitäjään.",
|
||||
"room_creation_restricted": "Puhelun luominen epäonnistui",
|
||||
"room_creation_restricted_description": "Puheluiden luominen saattaa olla rajoitettu vain valtuutetuille käyttäjille. Yritä myöhemmin uudelleen tai ota yhteyttä palvelimen ylläpitäjään, jos ongelma jatkuu.",
|
||||
"sticky_events_required": "Kotipalvelin ei tue Matrix 2.0 -puheluita",
|
||||
"sticky_events_required_description": "Tämä asennus on määritetty käyttämään Matrix 2.0 -puhelutilaa, mutta kotipalvelin ei mainosta tukea tarttuville tapahtumille (MSC4354). Pyydä palvelimen järjestelmänvalvojaa päivittämään tai vaihtamaan asennus yhteensopivaan tilaan.",
|
||||
"unexpected_ec_error": "Tapahtui odottamaton virhe (<0>Virhekoodi:</0> <1>{{ errorCode }}</1>). Ota yhteyttä palvelimen ylläpitäjään."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -145,6 +152,7 @@
|
||||
},
|
||||
"layout_grid_label": "Ruudukko",
|
||||
"layout_spotlight_label": "Valokeila",
|
||||
"layout_switch_label": "Asettelu",
|
||||
"lobby": {
|
||||
"ask_to_join": "Pyydä liittymistä puheluun",
|
||||
"join_as_guest": "Liity vieraana",
|
||||
@@ -197,6 +205,7 @@
|
||||
"blur_not_supported_by_browser": "(Tämä laite ei tue taustan sumennusta.)",
|
||||
"developer_tab_title": "Kehittäjä",
|
||||
"devices": {
|
||||
"activating": "Aktivoidaan…",
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Vaihda äänilaite",
|
||||
@@ -236,6 +245,7 @@
|
||||
"stop_video_button_label": "Lopeta video",
|
||||
"submitting": "Lähetetään…",
|
||||
"switch_camera": "Vaihda kameraa",
|
||||
"technical_details": "Tekniset tiedot",
|
||||
"unauthenticated_view_body": "Etkö ole vielä rekisteröitynyt? <2>Luo tili</2>",
|
||||
"unauthenticated_view_login_button": "Kirjaudu tilillesi",
|
||||
"unauthenticated_view_ssla_caption": "Klikkaamalla \"Siirry\" hyväksyt <2>ohjelmisto- ja palvelulisenssisopimuksen (SSLA)</2>",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"user_menu": "Menu użytkownika"
|
||||
},
|
||||
"action": {
|
||||
"blur_background": "Rozmycie tła",
|
||||
"close": "Zamknij",
|
||||
"copy_link": "Kopiuj link",
|
||||
"edit": "Edytuj",
|
||||
@@ -21,7 +22,7 @@
|
||||
"submit": "Wyślij",
|
||||
"upload_file": "Prześlij plik"
|
||||
},
|
||||
"analytics_notice": "Uczestnicząc w tej becie, upoważniasz nas do zbierania anonimowych danych, które wykorzystamy do ulepszenia produktu. Dowiedz się więcej na temat danych, które zbieramy w naszej <2>Polityce prywatności</2> i <5>Polityce ciasteczek</5>.",
|
||||
"analytics_notice": "Biorąc udział w wersji beta, wyrażasz zgodę na gromadzenie anonimowych danych, które wykorzystujemy w celu ulepszenia naszego produktu. Więcej informacji na temat danych, które śledzimy, znajdziesz w naszej <2>Polityce prywatności</2> dla i <6>Polityce ciasteczek</6>.",
|
||||
"call_ended_view": {
|
||||
"create_account_button": "Utwórz konto",
|
||||
"create_account_prompt": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
|
||||
@@ -41,7 +42,7 @@
|
||||
"display_name": "Nazwa wyświetlana",
|
||||
"encrypted": "Szyfrowane",
|
||||
"home": "Strona domowa",
|
||||
"loading": "Ładowanie…",
|
||||
"loading": "Wczytywanie…",
|
||||
"next": "Dalej",
|
||||
"options": "Opcje",
|
||||
"password": "Hasło",
|
||||
@@ -58,6 +59,14 @@
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Pokaż opcję słuchawki iPhone na wszystkich platformach",
|
||||
"crypto_version": "Wersja krypto: {{version}}",
|
||||
"custom_livekit_url": {
|
||||
"current_url": "Aktualnie ustawione na: ",
|
||||
"from_config": "Aktualnie nie nadpisano ustawienia. Używany jest adres URL z dobrego źródła lub konfiguracji.",
|
||||
"label": "Własny Livekit-url",
|
||||
"reset": "Zresetuj nadpisanie",
|
||||
"save": "Zapisz",
|
||||
"saving": "Zapisuję..."
|
||||
},
|
||||
"debug_tile_layout_label": "Układ kafelków Debug",
|
||||
"device_id": "ID urządzenia: {{id}}",
|
||||
"duplicate_tiles_label": "Liczba dodatkowych kopii kafelków na uczestnika",
|
||||
@@ -65,6 +74,21 @@
|
||||
"hostname": "Nazwa hosta: {{hostname}}",
|
||||
"livekit_server_info": "Informacje serwera LiveKit",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrixRTCMode": {
|
||||
"Comptibility": {
|
||||
"description": "Kompatybilny z serwerami domowymi, które nie obsługują sticky events (ale wszyscy inni klienci EC mają wersję v0.17.0 lub nowszą)",
|
||||
"label": "Kompatybilność: state events & multi SFU"
|
||||
},
|
||||
"Legacy": {
|
||||
"description": "Kompatybilny ze starszymi wersjami EC, które nie wspierają multi SFU",
|
||||
"label": "Przestarzałe: state events & oldest membership SFU"
|
||||
},
|
||||
"Matrix_2_0": {
|
||||
"description": "Kompatybilny tylko z serwerami domowymi obsługującymi sticky events i wszystkimi klientami EC w wersji v0.17.0 lub nowszej",
|
||||
"label": "Matrix 2.0: sticky events & multi SFU"
|
||||
},
|
||||
"title": "Tryb MatrixRTC"
|
||||
},
|
||||
"matrix_id": "ID Matrix: {{id}}",
|
||||
"mute_all_audio": "Wycisz cały dźwięk (uczestnicy, reakcje, dźwięki dołączenia)",
|
||||
"show_connection_stats": "Pokaż statystyki połączenia",
|
||||
@@ -74,25 +98,36 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Połączenie nie jest obsługiwane",
|
||||
"call_not_found": "Nie znaleziono połączenia",
|
||||
"call_not_found_description": "<0>Wygląda na to, że ten link nie należy do żadnego istniejącego połączenia. Sprawdź, czy posiadasz prawidłowy link lub <1>utwórz nowy</1>.</0>",
|
||||
"call_not_found_description": "<0>Wygląda na to, że ten link nie należy do żadnego istniejącego połączenia. Sprawdź, czy masz prawidłowy link lub <2> utwórz nowy</2>.</0>",
|
||||
"connection_lost": "Utracono połączenie",
|
||||
"connection_lost_description": "Zostałeś rozłączony z rozmowy.",
|
||||
"e2ee_unsupported": "Niekompatybilna przeglądarka",
|
||||
"e2ee_unsupported_description": "Twoja przeglądarka nie wspiera szyfrowanych połączeń. Obsługiwane przeglądarki obejmują Chrome, Safari i Firefox 117+.",
|
||||
"failed_to_start_livekit": "Nie udało się uruchomić połączenia Livekit",
|
||||
"generic": "Coś poszło nie tak",
|
||||
"generic_description": "Wysłanie dziennika debug, pozwoli nam namierzyć problem.",
|
||||
"insufficient_capacity": "Za mało miejsca",
|
||||
"insufficient_capacity_description": "Serwer osiągnął maksymalną pojemność, przez co nie możesz dołączyć do połączenia. Spróbuj ponownie później lub skontaktuj się z administratorem serwera, jeśli problem nie zniknie.",
|
||||
"livekit_connection_error": "Nie udało się połączyć z serwerem Livekit",
|
||||
"livekit_connection_error_description": "Wystąpił błąd podczas łączenia się z serwerem Livekit (<1>Przyczyna:</1> <2>{{ reason }}</2>).",
|
||||
"matrix_rtc_transport_missing": "Serwer nie jest skonfigurowany do pracy z {{brand}}. Prosimy o kontakt z administratorem serwera (Domena: {{domain}}, Kod błędu: {{ errorCode }}).",
|
||||
"membership_manager": "Błąd menedżera członkostwa",
|
||||
"membership_manager_description": "Menedżer członkostwa został zamknięty przez zbyt wiele powtarzających się błędów połączenia.",
|
||||
"no_matrix_2_authorization_service": "Usługa autoryzacji dla serwera multimediów (SFU) jest nieaktualna.",
|
||||
"open_elsewhere": "Otwarto w innej karcie",
|
||||
"open_elsewhere_description": "{{brand}} został otwarty w innej karcie. Jeśli tak nie jest, spróbuj odświeżyć stronę.",
|
||||
"peer_connection_timeout": "Upłynął limit czasu połączenia",
|
||||
"peer_connection_timeout_description": "Upłynął czas oczekiwania na połączenie z serwerem multimediów. Spróbuj połączyć się z inną siecią lub wyłączyć VPN. Jeśli problem nadal występuje, zapoznaj się z naszym <0>poradnikiem rozwiązywania problemów</0> lub skontaktuj się z administratorem serwera.",
|
||||
"room_creation_restricted": "Nie udało się utworzyć połączenia",
|
||||
"room_creation_restricted_description": "Tworzenie połączeń mogło zostać ograniczone tylko do autoryzowanych użytkowników. Spróbuj ponownie później lub skontaktuj się z administratorem serwera, jeśli problem nie minie.",
|
||||
"sticky_events_required": "Serwer domowy nie wspiera połączeń Matrix 2.0",
|
||||
"sticky_events_required_description": "Wybrana konfiguracja została ustawiona na korzystanie z połączeń Matrix 2.0, lecz serwer domowy nie wspiera ogłaszania sticky events (MSC4354). Poproś administratora serwera o aktualizację lub zmień konfigurację.",
|
||||
"unexpected_ec_error": "Wystąpił nieoczekiwany błąd (<0>Kod błędu:</0> <1>{{ errorCode }}</1>). Skontaktuj się z administratorem serwera."
|
||||
},
|
||||
"group_call_loader": {
|
||||
"banned_body": "Zostałeś zbanowany z pokoju.",
|
||||
"banned_heading": "Zbanowany",
|
||||
"call_ended_body": "Zostałeś usunięty z rozmowy.",
|
||||
"call_ended_body": "Zostałeś usunięty z połączenia.",
|
||||
"call_ended_heading": "Połączenie zakończone",
|
||||
"knock_reject_body": "Członkowie pokoju odrzucili Twoją prośbę o dołączenie.",
|
||||
"knock_reject_heading": "Nie możesz dołączyć",
|
||||
@@ -112,17 +147,18 @@
|
||||
},
|
||||
"join_existing_call_modal": {
|
||||
"join_button": "Tak, dołącz do połączenia",
|
||||
"text": "Te połączenie już istnieje, czy chcesz do niego dołączyć?",
|
||||
"text": "Połączenie już istnieje, czy chcesz do niej dołączyć?",
|
||||
"title": "Dołączyć do istniejącego połączenia?"
|
||||
},
|
||||
"layout_grid_label": "Siatka",
|
||||
"layout_spotlight_label": "Centrum uwagi",
|
||||
"layout_switch_label": "Układ",
|
||||
"lobby": {
|
||||
"ask_to_join": "Poproś o dołączenie do rozmowy",
|
||||
"ask_to_join": "Poproś o dołączenie do połączenia",
|
||||
"join_as_guest": "Dołącz jako gość",
|
||||
"join_button": "Dołącz do połączenia",
|
||||
"leave_button": "Wróć do ostatnie",
|
||||
"waiting_for_invite": "Żądanie wysłane"
|
||||
"waiting_for_invite": "Prośba wysłana! Oczekiwanie na zatwierdzenie…"
|
||||
},
|
||||
"log_in": "Zaloguj",
|
||||
"logging_in": "Logowanie…",
|
||||
@@ -162,14 +198,15 @@
|
||||
"screenshare_button_label": "Udostępnij ekran",
|
||||
"settings": {
|
||||
"audio_tab": {
|
||||
"effect_volume_description": "Dostosuj głośność, z jaką odtwarzane są efekty reakcji i podniesionej ręki",
|
||||
"effect_volume_label": "Głośność efektu dźwiękowego"
|
||||
"effect_volume_description": "Dostosuj głośność efektów reakcji i podniesienia ręki.",
|
||||
"effect_volume_label": "Głośność efektów dźwiękowych"
|
||||
},
|
||||
"background_blur_header": "Tło",
|
||||
"background_blur_label": "Rozmycie tła wideo",
|
||||
"blur_not_supported_by_browser": "(Rozmycie tła nie jest wspierane przez to urządzenie.)",
|
||||
"developer_tab_title": "Programista",
|
||||
"devices": {
|
||||
"activating": "Aktywuję…",
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Zmień urządzenie audio",
|
||||
@@ -192,7 +229,7 @@
|
||||
"preferences_tab": {
|
||||
"developer_mode_label": "Tryb programisty",
|
||||
"developer_mode_label_description": "Włącz tryb programisty i wyświetl kartę ustawień programisty.",
|
||||
"introduction": "Tutaj możesz skonfigurować dodatkowe opcje dla lepszych doświadczeń.",
|
||||
"introduction": "Tutaj możesz skonfigurować dodatkowe opcje dla lepszego komfortu użytkowania.",
|
||||
"reactions_play_sound_description": "Odtwórz efekt dźwiękowy, gdy ktoś wyślę reakcję w trakcie połączenia.",
|
||||
"reactions_play_sound_label": "Odtwórz dźwięki reakcji",
|
||||
"reactions_show_description": "Odtwórz animację, gdy ktoś wyślę reakcję.",
|
||||
@@ -201,26 +238,31 @@
|
||||
"show_hand_raised_timer_label": "Pokaż czas po podniesieniu ręki"
|
||||
}
|
||||
},
|
||||
"star_rating_input_label_one": "{{count}} gwiazdki",
|
||||
"star_rating_input_label_other": "{{count}} gwiazdki",
|
||||
"star_rating_input_label_one": "{{count}} gwiazdka",
|
||||
"star_rating_input_label_few": "{{count}} gwiazdki",
|
||||
"star_rating_input_label_many": "{{count}} gwiazdek",
|
||||
"start_new_call": "Rozpocznij nowe połączenie",
|
||||
"start_video_button_label": "Rozpocznij wideo",
|
||||
"stop_screenshare_button_label": "Udostępnianie ekranu",
|
||||
"stop_video_button_label": "Zakończ wideo",
|
||||
"submitting": "Wysyłanie…",
|
||||
"switch_camera": "Przełącz kamerę",
|
||||
"technical_details": "Szczegóły techniczne",
|
||||
"unauthenticated_view_body": "Nie masz konta? <2>Utwórz je</2>",
|
||||
"unauthenticated_view_login_button": "Zaloguj się do swojego konta",
|
||||
"unauthenticated_view_login_button": "Zaloguj się na swoje konto",
|
||||
"unauthenticated_view_ssla_caption": "Klikając \"Przejdź\", wyrażasz zgodę na naszą <2>Umowę licencyjną oprogramowania i usług (SSLA)</2>",
|
||||
"unmute_microphone_button_label": "Odcisz mikrofon",
|
||||
"version": "Wersja: {{version}}",
|
||||
"unmute_microphone_button_label": "Wyłącz wyciszenie mikrofonu",
|
||||
"version": "{{productName}} wersja: {{version}}",
|
||||
"video_tile": {
|
||||
"always_show": "Zawsze pokazuj",
|
||||
"call_ended": "Połączenie zakończone",
|
||||
"calling": "Łączenie...",
|
||||
"camera_starting": "Wczytywanie wideo...",
|
||||
"collapse": "Zwiń",
|
||||
"expand": "Rozwiń",
|
||||
"mute_for_me": "Wycisz dla mnie",
|
||||
"muted_for_me": "Wyciszone dla mnie",
|
||||
"screen_share_volume": "Głośność udostępniania ekranu",
|
||||
"volume": "Głośność",
|
||||
"waiting_for_media": "Oczekiwanie na media..."
|
||||
}
|
||||
|
||||
70
package.json
70
package.json
@@ -17,11 +17,11 @@
|
||||
"build:sdk": "pnpm build:full --config vite-sdk.config.js",
|
||||
"build:sdk:production": "pnpm build:sdk",
|
||||
"serve": "vite preview",
|
||||
"prettier:check": "prettier -c .",
|
||||
"prettier:format": "prettier -w .",
|
||||
"lint": "pnpm lint:types && pnpm lint:eslint && pnpm lint:knip",
|
||||
"lint:eslint": "eslint --max-warnings 0 src playwright",
|
||||
"lint:eslint-fix": "eslint --max-warnings 0 src playwright --fix",
|
||||
"format": "oxfmt",
|
||||
"format:check": "oxfmt --check; rc=$?; [[ $rc -ne 0 ]] && printf '\\033[46;30m INFO \\033[0m To fix, run: pnpm format\\n' >&2; exit $rc",
|
||||
"lint": "pnpm lint:types && pnpm lint:oxlint && pnpm lint:knip",
|
||||
"lint:oxlint": "oxlint src playwright",
|
||||
"lint:oxlint-fix": "oxlint --fix src playwright",
|
||||
"lint:knip": "knip",
|
||||
"lint:types": "tsc",
|
||||
"i18n": "npx i18next-cli extract",
|
||||
@@ -30,8 +30,8 @@
|
||||
"test:storybook": "vitest --project=storybook",
|
||||
"test:unit": "vitest --project=unit",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"backend": "docker-compose -f dev-backend-docker-compose.yml up",
|
||||
"backend-playwright": "docker-compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up",
|
||||
"backend": "docker-compose -f docker-compose-dev.yml up",
|
||||
"backend-playwright": "docker-compose -f docker-compose-dev.yml -f docker-compose-playwright.yml up",
|
||||
"test:playwright": "playwright test",
|
||||
"test:playwright:open": "pnpm test:playwright --ui",
|
||||
"links:on": "cp scripts/.pnpmfile.cjs .pnpmfile.cjs & pnpm install",
|
||||
@@ -40,10 +40,6 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
"@babel/preset-env": "^7.29.5",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@codecov/vite-plugin": "^1.3.0",
|
||||
"@fontsource/inconsolata": "^5.1.0",
|
||||
"@fontsource/inter": "^5.1.0",
|
||||
@@ -51,7 +47,6 @@
|
||||
"@formatjs/intl-segmenter": "^11.7.3",
|
||||
"@livekit/components-core": "^0.12.0",
|
||||
"@livekit/components-react": "^2.0.0",
|
||||
"@livekit/protocol": "^1.42.2",
|
||||
"@livekit/track-processors": "^0.7.1",
|
||||
"@mediapipe/tasks-vision": "^0.10.18",
|
||||
"@playwright/test": "^1.60.0",
|
||||
@@ -59,17 +54,17 @@
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-visually-hidden": "^1.0.3",
|
||||
"@react-spring/web": "^10.0.0",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"@sentry/vite-plugin": "^3.0.0",
|
||||
"@storybook/addon-docs": "^10.3.6",
|
||||
"@storybook/addon-vitest": "^10.3.6",
|
||||
"@storybook/react-vite": "^10.3.6",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@testing-library/dom": "^10.1.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
@@ -79,34 +74,20 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"@typescript-eslint/utils": "^8.61.0",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vector-im/compound-design-tokens": "^10.0.0",
|
||||
"@vector-im/compound-web": "^9.3.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitest/browser-playwright": "^4.1.5",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@vitest/ui": "4.1.7",
|
||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||
"@vitest/ui": "4.1.10",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-deprecate": "^0.9.0",
|
||||
"eslint-plugin-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-element-call": "link:eslint",
|
||||
"eslint-plugin-storybook": "^10.3.6",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fetch-mock": "11.1.5",
|
||||
"global-jsdom": "^26.0.0",
|
||||
"i18next": "^25.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-cli": "^1.61.0",
|
||||
@@ -115,16 +96,18 @@
|
||||
"livekit-client": "^2.18.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"loglevel": "^1.9.1",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"node-stdlib-browser": "^1.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
"oxfmt": "^0.61.0",
|
||||
"oxlint": "^1.70.0",
|
||||
"oxlint-tsgolint": "^0.25.0",
|
||||
"pako": "^2.0.4",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"posthog-js": "1.374.0",
|
||||
"prettier": "^3.0.0",
|
||||
"posthog-js": "1.408.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "19",
|
||||
"react-dom": "19",
|
||||
@@ -135,7 +118,6 @@
|
||||
"sass": "^1.42.1",
|
||||
"storybook": "^10.3.6",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"uuid": "^14.0.0",
|
||||
"vaul": "^1.0.0",
|
||||
@@ -149,17 +131,5 @@
|
||||
"vitest": "^4.1.5",
|
||||
"vitest-axe": "^1.0.0-pre.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@livekit/components-core>rxjs": "^7.8.1",
|
||||
"@livekit/track-processors>@mediapipe/tasks-vision": "^0.10.18",
|
||||
"minimatch": "^10.2.3",
|
||||
"tar": "^7.5.11",
|
||||
"glob": "^10.5.0",
|
||||
"qs": "^6.14.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"esbuild": "^0.28.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
"packageManager": "pnpm@11.6.0+sha512.9a36518224080c6fe5165afdcfe79bfa118c29be703f3f462b1e32efe1e98e47e8750b148e08286250aad4113cc7993ca413c4e2cd447752708c2ee5751bc95f"
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
services:
|
||||
synapse:
|
||||
volumes:
|
||||
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z
|
||||
synapse-1:
|
||||
volumes:
|
||||
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||
@@ -1,2 +0,0 @@
|
||||
include:
|
||||
- dev-backend-docker-compose.yml
|
||||
@@ -120,6 +120,11 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
.getByRole("heading", { name: "Welcome Room" }),
|
||||
).toBeVisible();
|
||||
} else if (callType === "dm") {
|
||||
await TestHelpers.closeReleaseAnnouncement(
|
||||
ewPage1,
|
||||
"Introducing Sections",
|
||||
);
|
||||
|
||||
await ewPage1
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
|
||||
@@ -88,15 +88,18 @@ test("When creator left, avoid reconnect to the same SFU", async ({
|
||||
await guestCPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||
|
||||
await guestCPage.waitForTimeout(1000);
|
||||
|
||||
if (wsConnectionCount === 2) {
|
||||
console.warn("wsConnectionCount is 2, expecting 1 after join");
|
||||
}
|
||||
const wsConnectionCountBeforeLeave = wsConnectionCount;
|
||||
// ========
|
||||
// the creator leaves the call
|
||||
await creatorPage.getByTestId("incall_leave").click();
|
||||
|
||||
// https://github.com/element-hq/element-call/issues/3344
|
||||
// The app used to request a new jwt token then to reconnect to the SFU
|
||||
expect(wsConnectionCount).toBe(1);
|
||||
expect(wsConnectionCount).toBe(wsConnectionCountBeforeLeave);
|
||||
// Wait a bit to be sure that if there was a reconnect, it would have happened by now
|
||||
await guestCPage.waitForTimeout(6000);
|
||||
expect(wsConnectionCount).toBe(1);
|
||||
expect(wsConnectionCount).toBe(wsConnectionCountBeforeLeave);
|
||||
});
|
||||
|
||||
@@ -125,21 +125,10 @@ async function expectVideoTilesCount(page: Page, count: number): Promise<void> {
|
||||
});
|
||||
|
||||
// There should be `count` video elements, visible and autoplaying
|
||||
await expect(page.locator("video")).toHaveCount(count);
|
||||
|
||||
await expect(async () => {
|
||||
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,
|
||||
});
|
||||
await expect(page.locator("video").filter({ visible: true })).toHaveCount(
|
||||
count,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
}
|
||||
|
||||
export const SpaHelpers = {
|
||||
|
||||
@@ -36,6 +36,7 @@ widgetTest("Footer interaction in PiP", async ({ addUser, browserName }) => {
|
||||
await valere.page.waitForTimeout(600);
|
||||
|
||||
// Switch to the other room, the call should go to PIP
|
||||
await TestHelpers.expandRoomList(valere.page);
|
||||
await TestHelpers.switchToRoomNamed(valere.page, "OtherRoom");
|
||||
|
||||
// We should see the PIP overlay
|
||||
|
||||
@@ -53,6 +53,7 @@ widgetTest("Put call in PIP", async ({ addUser, browserName }) => {
|
||||
});
|
||||
|
||||
// Switch to the other room, the call should go to PIP
|
||||
await TestHelpers.expandRoomList(valere.page);
|
||||
await TestHelpers.switchToRoomNamed(valere.page, "DoubleTask");
|
||||
|
||||
// We should see the PIP overlay
|
||||
|
||||
@@ -73,9 +73,6 @@ export class TestHelpers {
|
||||
audioOnly: boolean = false,
|
||||
isDM: boolean = false,
|
||||
): Promise<void> {
|
||||
// XXX This using the notification toast to join the room.
|
||||
// Not the button in the header
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
const label = isDM
|
||||
? audioOnly
|
||||
@@ -85,7 +82,9 @@ export class TestHelpers {
|
||||
await expect(page.getByText(label)).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await page.getByRole("button", { name: "Join" }).click({
|
||||
// XXX This using the notification toast to join the room.
|
||||
// Not the buttons in the header or timeline
|
||||
await page.getByRole("alert").getByRole("button", { name: "Join" }).click({
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
@@ -160,6 +159,7 @@ export class TestHelpers {
|
||||
const expectedToasts = [
|
||||
{ title: "Failed to load service worker", button: "OK" },
|
||||
{ title: "Back up your chats", button: "Dismiss" },
|
||||
{ title: "Turn on key storage", button: "Dismiss" },
|
||||
{ title: "Element does not support this browser", button: "Dismiss" },
|
||||
];
|
||||
|
||||
@@ -189,11 +189,27 @@ export class TestHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
public static async closeReleaseAnnouncement(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await page
|
||||
.getByRole("dialog", { name })
|
||||
.getByRole("button", { name: "OK" })
|
||||
.click({ timeout: 2000 });
|
||||
} catch {
|
||||
// Announcement not shown; nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
public static async createRoom(
|
||||
name: string,
|
||||
page: Page,
|
||||
andInvite: string[] = [],
|
||||
): Promise<void> {
|
||||
await TestHelpers.closeReleaseAnnouncement(page, "Introducing Sections");
|
||||
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
@@ -330,6 +346,12 @@ export class TestHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
public static async expandRoomList(page: Page): Promise<void> {
|
||||
await page
|
||||
.getByRole("separator", { name: "Click or drag to expand" })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a room in the room list by its name.
|
||||
* @param page - The EW page
|
||||
@@ -368,25 +390,9 @@ export class TestHelpers {
|
||||
frame: FrameLocator,
|
||||
count: number,
|
||||
): Promise<void> {
|
||||
// XXX we need to be better at our HTML markup and accessibility, it would make
|
||||
// this kind of stuff way easier to test if we could look out for aria attributes.
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
return await frame
|
||||
.locator("video")
|
||||
.evaluateAll(
|
||||
(videos: Element[]) =>
|
||||
videos.filter(
|
||||
(v: Element) =>
|
||||
window.getComputedStyle(v).display === "block",
|
||||
).length,
|
||||
);
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
.toBe(count);
|
||||
await expect(frame.locator("video").filter({ visible: true })).toHaveCount(
|
||||
count,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,17 +31,18 @@ widgetTest(
|
||||
const brooksFrame = brooks.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
|
||||
// We should show a ringing tile, let's check for that
|
||||
await expect(
|
||||
brooksFrame
|
||||
.getByTestId("videoTile")
|
||||
.filter({ has: brooksFrame.getByText(whistler.displayName) })
|
||||
.filter({ has: brooksFrame.getByText("Calling…") }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(whistler.page.getByText("Incoming voice call")).toBeVisible();
|
||||
await whistler.page.getByRole("button", { name: "Join" }).click();
|
||||
await whistler.page
|
||||
.getByRole("alert")
|
||||
.getByRole("button", { name: "Join" })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
@@ -83,23 +84,22 @@ widgetTest(
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
|
||||
// So first we need to confirm that it is hidden when in the call.
|
||||
await expect(
|
||||
whistler.page.locator(".mx_BasicMessageComposer"),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
brooks.page.locator(".mx_BasicMessageComposer"),
|
||||
).not.toBeVisible();
|
||||
|
||||
// ASSERT hanging up on one side ends the call for both
|
||||
await brooksFrame.getByRole("button", { name: "End call" }).click();
|
||||
|
||||
// The widget should be closed on both sides and the timeline should be back on screen
|
||||
// We confirm that we started in Pip mode (voice call) and check that we still see the composer.
|
||||
await expect(
|
||||
whistler.page.locator(".mx_BasicMessageComposer"),
|
||||
).toBeVisible();
|
||||
await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible();
|
||||
|
||||
// ASSERT hanging up on one side ends the call for both
|
||||
await brooksFrame.getByRole("button", { name: "End call" }).click();
|
||||
|
||||
// The widget should be closed on both sides
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -132,7 +132,10 @@ widgetTest(
|
||||
).toBeVisible();
|
||||
|
||||
await expect(whistler.page.getByText("Incoming video call")).toBeVisible();
|
||||
await whistler.page.getByRole("button", { name: "Join" }).click();
|
||||
await whistler.page
|
||||
.getByRole("alert")
|
||||
.getByRole("button", { name: "Join" })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
|
||||
8794
pnpm-lock.yaml
generated
8794
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,58 @@
|
||||
# dependencies where we use branches and hashes in the package.json. But that also use a pre/post install script.
|
||||
onlyBuiltDependencies:
|
||||
- "matrix-js-sdk"
|
||||
minimumReleaseAgeExclude:
|
||||
- "@vector-im/compound-design-tokens"
|
||||
- "@vector-im/compound-web"
|
||||
- matrix-js-sdk
|
||||
- matrix-widget-api
|
||||
allowBuilds:
|
||||
"@parcel/watcher": true
|
||||
"@sentry/cli": true
|
||||
"@swc/core": true
|
||||
"core-js": true
|
||||
"esbuild": true
|
||||
"matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7": true
|
||||
"protobufjs": true
|
||||
overrides:
|
||||
# We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but
|
||||
# LiveKit has pinned it to a very specific version which is now holding us back from updating.
|
||||
# See livekit/components-js#1101 for a request for a proper solution.
|
||||
"@livekit/components-core>rxjs": "^7.8.1"
|
||||
# Dedupe Mediapipe dependencies.
|
||||
"@livekit/track-processors>@mediapipe/tasks-vision": "^0.10.18"
|
||||
# Security fix: https://security-tracker.debian.org/tracker/CVE-2026-31802
|
||||
"tar": "^7.5.11"
|
||||
# Security fixes:
|
||||
# - https://github.com/advisories/GHSA-7r86-cg39-jmmj
|
||||
# - https://github.com/advisories/GHSA-23c5-xmqv-rm74
|
||||
"minimatch": "^10.2.3"
|
||||
# Security fix: https://github.com/element-hq/element-call/security/dependabot/109
|
||||
"glob": "^10.5.0"
|
||||
# Security fixes:
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/110
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/122
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/179
|
||||
"qs": "^6.15.2"
|
||||
# Security fixes:
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/106
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/203
|
||||
"js-yaml": "^4.2.0"
|
||||
# Storybook declares support for 0.27.0 only but empirically works fine with 0.28.0.
|
||||
"esbuild": "^0.28.0"
|
||||
# Security fixes:
|
||||
# - https://github.com/nodejs/undici/releases/tag/v6.24.0
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/201
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/199
|
||||
"undici": "^6.27.0"
|
||||
# Security fix: https://github.com/advisories/GHSA-rf6f-7fwh-wjgh
|
||||
"flatted": "^3.4.2"
|
||||
# Security fix: https://github.com/element-hq/element-call/security/dependabot/184
|
||||
"ws": "^8.21.0"
|
||||
# Security fixes:
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/191
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/190
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/192
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/194
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/198
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/196
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/195
|
||||
# - https://github.com/element-hq/element-call/security/dependabot/193
|
||||
"dompurify": "^3.4.11"
|
||||
|
||||
@@ -61,10 +61,19 @@
|
||||
{
|
||||
"groupName": "Pnpm",
|
||||
"matchDepNames": ["pnpm"]
|
||||
},
|
||||
{
|
||||
"description": "Update the element-web image digest daily, in its own PR",
|
||||
"groupName": "Element Web",
|
||||
"matchPackageNames": ["ghcr.io/element-hq/element-web"],
|
||||
"matchUpdateTypes": ["digest"],
|
||||
"schedule": ["* 0-3 * * *"],
|
||||
"minimumReleaseAge": null,
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"semanticCommits": "disabled",
|
||||
"ignoreDeps": ["eslint-plugin-matrix-org"],
|
||||
"ignoreDeps": [],
|
||||
"vulnerabilityAlerts": {
|
||||
"schedule": ["at any time"],
|
||||
"prHourlyLimit": 0,
|
||||
|
||||
@@ -15,9 +15,8 @@ import { scan } from "rxjs";
|
||||
import { type WidgetHelpers } from "../src/widget";
|
||||
import { type LivekitRoomItem } from "../src/state/CallViewModel/CallViewModel";
|
||||
|
||||
export const logger = rootLogger.getChild("[MatrixRTCSdk]");
|
||||
|
||||
export const tryMakeSticky = (widget: WidgetHelpers): void => {
|
||||
const logger = rootLogger.getChild("[MatrixRTCSdk]");
|
||||
logger.info("try making sticky MatrixRTCSdk");
|
||||
void widget.api
|
||||
.setAlwaysOnScreen(true)
|
||||
|
||||
14
sdk/main.ts
14
sdk/main.ts
@@ -52,7 +52,8 @@ import { getUrlParams } from "../src/UrlParams";
|
||||
import { MuteStates } from "../src/state/MuteStates";
|
||||
import { MediaDevices } from "../src/state/MediaDevices";
|
||||
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 {
|
||||
ElementWidgetActions,
|
||||
widget as _widget,
|
||||
@@ -76,9 +77,9 @@ interface MatrixRTCSdk {
|
||||
stop: () => void;
|
||||
data$: Observable<{ rtcBackendIdentity: string; data: string }>;
|
||||
/**
|
||||
* flattened list of members
|
||||
* flattened list of remote members
|
||||
*/
|
||||
members$: Behavior<
|
||||
remoteMembers$: Behavior<
|
||||
{
|
||||
connection: Connection | null;
|
||||
membership: CallMembership;
|
||||
@@ -86,7 +87,7 @@ interface MatrixRTCSdk {
|
||||
}[]
|
||||
>;
|
||||
/**
|
||||
* flattened local members
|
||||
* flattened local member
|
||||
*/
|
||||
localMember$: Behavior<{
|
||||
connection: Connection | null;
|
||||
@@ -104,6 +105,7 @@ export async function createMatrixRTCSdk(
|
||||
id: string = "",
|
||||
sticky: boolean = false,
|
||||
): Promise<MatrixRTCSdk> {
|
||||
const logger = rootLogger.getChild("[MatrixRTCSdk]");
|
||||
const scope = new ObservableScope();
|
||||
|
||||
// widget client
|
||||
@@ -338,8 +340,8 @@ export async function createMatrixRTCSdk(
|
||||
),
|
||||
),
|
||||
connected$: callViewModel.connected$,
|
||||
members$: scope.behavior(
|
||||
callViewModel.matrixLivekitMembers$.pipe(
|
||||
remoteMembers$: scope.behavior(
|
||||
callViewModel.remoteMatrixLivekitMembers$.pipe(
|
||||
switchMap((members) => {
|
||||
const listOfMemberObservables = members.map((member) =>
|
||||
combineLatest([
|
||||
|
||||
18
src/App.tsx
18
src/App.tsx
@@ -30,7 +30,7 @@ import { useTheme } from "./useTheme";
|
||||
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
||||
import { type AppViewModel } from "./state/AppViewModel";
|
||||
import { MediaDevicesContext } from "./MediaDevicesContext";
|
||||
import { getUrlParams, HeaderStyle } from "./UrlParams";
|
||||
import { getUrlParams, HeaderStyle, useUrlParams } from "./UrlParams";
|
||||
import { AppBar } from "./AppBar";
|
||||
|
||||
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
|
||||
@@ -41,19 +41,17 @@ interface SimpleProviderProps {
|
||||
|
||||
const BackgroundProvider: FC<SimpleProviderProps> = ({ children }) => {
|
||||
const { pathname } = useLocation();
|
||||
const { background } = useUrlParams();
|
||||
|
||||
useEffect(() => {
|
||||
let backgroundImage = "";
|
||||
if (!["/login", "/register"].includes(pathname) && !widget) {
|
||||
backgroundImage = "var(--background-gradient)";
|
||||
}
|
||||
document
|
||||
.getElementsByTagName("body")[0]
|
||||
.setAttribute("data-background", background);
|
||||
}, [pathname, background]);
|
||||
|
||||
document.getElementsByTagName("body")[0].style.backgroundImage =
|
||||
backgroundImage;
|
||||
}, [pathname]);
|
||||
|
||||
return <>{children}</>;
|
||||
return children;
|
||||
};
|
||||
|
||||
const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
||||
useTheme();
|
||||
return children;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
.bar {
|
||||
flex-shrink: 0;
|
||||
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 */
|
||||
@@ -9,8 +15,7 @@
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
/* Extend the gradient beyond the bottom of the header for readability */
|
||||
inset-block: -24px;
|
||||
z-index: var(--call-view-header-footer-layer);
|
||||
inset-block: 0 -16px;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
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 {
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
inset-inline: 0;
|
||||
inset-block-start: 0;
|
||||
block-size: 64px;
|
||||
z-index: var(--call-view-header-footer-layer);
|
||||
display: grid;
|
||||
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 {
|
||||
fill: var(--cpd-color-icon-primary);
|
||||
.bar:has(.subtitle) > header {
|
||||
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;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
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.
|
||||
*/
|
||||
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
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", () => {
|
||||
it("renders", () => {
|
||||
const { container } = render(
|
||||
<TooltipProvider>
|
||||
<AppBar>
|
||||
<p>This is the content.</p>
|
||||
</AppBar>
|
||||
</TooltipProvider>,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
it("renders", () => snapshotAppBar(content));
|
||||
|
||||
it("renders with title and subtitle", () => {
|
||||
const TestComponent: FC = () => {
|
||||
useAppBarTitle("Title");
|
||||
useAppBarSubtitle("Subtitle");
|
||||
return content;
|
||||
};
|
||||
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 {
|
||||
createContext,
|
||||
type FC,
|
||||
type MouseEvent,
|
||||
type ReactNode,
|
||||
use,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
createContext,
|
||||
type FC,
|
||||
type MouseEvent,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Heading, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import { CollapseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import classNames from "classnames";
|
||||
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 { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { Header, LeftNav, RightNav } from "./Header";
|
||||
import { platform } from "./Platform";
|
||||
import styles from "./AppBar.module.css";
|
||||
|
||||
interface AppBarContext {
|
||||
setTitle: (value: string) => void;
|
||||
setSubtitle: (value: ReactNode) => void;
|
||||
setSecondaryButton: (value: ReactNode) => void;
|
||||
setPrimaryButtonIconKind: (value: "back" | "minimise") => void;
|
||||
setHidden: (value: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -49,44 +55,76 @@ export const AppBar: FC<Props> = ({ children }) => {
|
||||
}, []);
|
||||
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [subtitle, setSubtitle] = useState<ReactNode>(undefined);
|
||||
const [hidden, setHidden] = useState<boolean>(false);
|
||||
const [secondaryButton, setSecondaryButton] = useState<ReactNode | null>(
|
||||
null,
|
||||
);
|
||||
const [primaryButtonIcon, setPrimaryButtonIconKind] = useState<
|
||||
"back" | "minimise"
|
||||
>("minimise");
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
style={{ display: hidden ? "none" : "block" }}
|
||||
className={styles.bar}
|
||||
>
|
||||
<Header
|
||||
// App bar is mainly seen in the call view, which has its own
|
||||
// 'reconnecting' toast
|
||||
disconnectedBanner={false}
|
||||
>
|
||||
<LeftNav>
|
||||
<Tooltip label={t("common.back")}>
|
||||
<IconButton size="24px" onClick={onBackClick}>
|
||||
{/* Wrap the header in a div due to annoying z-index issues with the
|
||||
gradient background */}
|
||||
<div className={classNames(styles.bar, { [styles.hidden]: hidden })}>
|
||||
<header>
|
||||
<Tooltip label={t("common.back")}>
|
||||
<IconButton
|
||||
className={styles.primaryButton}
|
||||
// We render the back button (PrimaryButtonIcon) the same size as the native os.
|
||||
// We render the minimise icon (default) smaller as per designs.
|
||||
size={primaryButtonIcon === "back" ? "32px" : "24px"}
|
||||
onClick={onBackClick}
|
||||
>
|
||||
{primaryButtonIcon === "back" ? (
|
||||
<BackIcon aria-hidden />
|
||||
) : (
|
||||
<CollapseIcon aria-hidden />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</LeftNav>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{title && (
|
||||
<Heading
|
||||
className={styles.title}
|
||||
type="body"
|
||||
size="lg"
|
||||
weight={platform === "android" ? "medium" : "semibold"}
|
||||
size={platform === "ios" ? "md" : "lg"}
|
||||
weight={platform === "ios" ? "semibold" : "medium"}
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
)}
|
||||
<RightNav>{secondaryButton}</RightNav>
|
||||
</Header>
|
||||
{subtitle && (
|
||||
<Text
|
||||
className={styles.subtitle}
|
||||
as="span"
|
||||
size={platform === "ios" ? "sm" : "lg"}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
<div className={styles.secondaryButton}>{secondaryButton}</div>
|
||||
</header>
|
||||
</div>
|
||||
<AppBarContext value={context}>{children}</AppBarContext>
|
||||
</>
|
||||
@@ -107,6 +145,36 @@ export function useAppBarTitle(title: string): void {
|
||||
}, [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
|
||||
* 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 {
|
||||
error: Error | unknown;
|
||||
error: unknown;
|
||||
widget: WidgetHelpers | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,15 @@ import {
|
||||
HeaderStyle,
|
||||
getUrlParams,
|
||||
} from "../src/UrlParams";
|
||||
import { mockConfig } from "./utils/test";
|
||||
|
||||
const ROOM_NAME = "roomNameHere";
|
||||
const ROOM_ID = "!d45f138fsd";
|
||||
const ORIGIN = "https://call.element.io";
|
||||
const HOMESERVER = "localhost";
|
||||
|
||||
mockConfig();
|
||||
|
||||
describe("UrlParams", () => {
|
||||
describe("handles URL with /room/", () => {
|
||||
it("and nothing else", () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Config } from "./config/Config";
|
||||
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "./e2ee/e2eeType";
|
||||
import { platform } from "./Platform";
|
||||
import { redact } from "./utils/redact";
|
||||
|
||||
interface RoomIdentifier {
|
||||
roomAlias: string | null;
|
||||
@@ -44,6 +45,11 @@ export enum HeaderStyle {
|
||||
AppBar = "app_bar",
|
||||
}
|
||||
|
||||
export enum BackgroundStyle {
|
||||
Solid = "solid",
|
||||
Gradient = "gradient",
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -144,6 +150,10 @@ export interface UrlProperties {
|
||||
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
||||
*/
|
||||
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"),
|
||||
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
||||
theme: parser.getParam("theme"),
|
||||
background:
|
||||
parser.getEnumParam("background", BackgroundStyle) ??
|
||||
BackgroundStyle.Gradient,
|
||||
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
||||
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
||||
posthogApiHost: parser.getParam("posthogApiHost"),
|
||||
@@ -494,7 +507,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
"intent:",
|
||||
intent,
|
||||
"\nproperties:",
|
||||
properties,
|
||||
redact(properties, "password"),
|
||||
"configuration:",
|
||||
configuration,
|
||||
);
|
||||
|
||||
@@ -3,44 +3,90 @@
|
||||
exports[`AppBar > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="bar"
|
||||
style="display: block;"
|
||||
class="_bar_221541"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
<header>
|
||||
<button
|
||||
aria-labelledby="_r_0_"
|
||||
class="_icon-button_1215g_8 _primaryButton_221541"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_0_"
|
||||
class="_icon-button_1215g_8"
|
||||
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%;"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<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
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`the content is rendered when the modal is open 1`] = `
|
||||
<div
|
||||
aria-labelledby="radix-_r_4_"
|
||||
class="overlay animate modal dialog _glass_sepwu_8"
|
||||
class="_overlay_2f5303 _animate_2f5303 _modal_dbeffe _dialog_dbeffe _glass_sepwu_8"
|
||||
data-state="open"
|
||||
id="radix-_r_3_"
|
||||
role="dialog"
|
||||
@@ -11,10 +11,10 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_dbeffe"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
class="_header_dbeffe"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
@@ -24,7 +24,7 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
class="_body_dbeffe"
|
||||
>
|
||||
<p>
|
||||
This is the content.
|
||||
@@ -37,7 +37,7 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
<div
|
||||
aria-labelledby="radix-_r_a_"
|
||||
class="overlay modal drawer"
|
||||
class="_overlay_2f5303 _modal_dbeffe _drawer_dbeffe"
|
||||
data-state="open"
|
||||
data-vaul-animate="true"
|
||||
data-vaul-custom-container="false"
|
||||
@@ -51,13 +51,13 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_dbeffe"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
class="_header_dbeffe"
|
||||
>
|
||||
<div
|
||||
class="handle"
|
||||
class="_handle_dbeffe"
|
||||
/>
|
||||
<h2
|
||||
id="radix-_r_a_"
|
||||
@@ -67,7 +67,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
class="_body_dbeffe"
|
||||
>
|
||||
<p>
|
||||
This is the content.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`QrCode > renders 1`] = `
|
||||
<div
|
||||
class="qrCode bar"
|
||||
class="_qrCode_458264 bar"
|
||||
>
|
||||
<img
|
||||
alt="QR Code"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`Toast > renders 1`] = `
|
||||
<button
|
||||
aria-labelledby="radix-_r_4_"
|
||||
class="overlay animate toast"
|
||||
class="_overlay_2f5303 _animate_2f5303 _toast_15a045"
|
||||
data-state="open"
|
||||
id="radix-_r_3_"
|
||||
role="dialog"
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
.endCall > svg {
|
||||
color: var(--stopgap-color-on-solid-accent);
|
||||
}
|
||||
|
||||
.rotate > svg {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@@ -135,21 +135,12 @@ interface EndCallButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
size?: "md" | "lg";
|
||||
}
|
||||
|
||||
export const EndCallButton: FC<EndCallButtonProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
export const EndCallButton: FC<EndCallButtonProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip label={t("hangup_button_label")}>
|
||||
<CpdButton
|
||||
className={classNames(className, styles.endCall)}
|
||||
iconOnly
|
||||
Icon={EndCallIcon}
|
||||
destructive
|
||||
{...props}
|
||||
/>
|
||||
<CpdButton iconOnly Icon={EndCallIcon} destructive {...props} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -173,7 +164,7 @@ export const LoudspeakerButton: FC<LoudspeakerButtonProps> = ({
|
||||
iconOnly
|
||||
Icon={loudspeakerModeEnabled ? VolumeOnSolidIcon : VolumeOffSolidIcon}
|
||||
{...props}
|
||||
kind={loudspeakerModeEnabled ? "primary" : "secondary"}
|
||||
kind={loudspeakerModeEnabled ? "secondary" : "primary"}
|
||||
aria-checked={loudspeakerModeEnabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby="_r_1j_"
|
||||
class="_button_1nw83_8 raisedButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _raisedButton_fb25ab _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
|
||||
@@ -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-block: 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 {
|
||||
|
||||
@@ -16,10 +16,12 @@ import inCallViewStyles from "../room/InCallView.module.css";
|
||||
import { useStaticViewModel } from "../state/ViewModel";
|
||||
import { ReactionsSenderContext } from "../reactions/useReactionsSender";
|
||||
import { type ReactionOption } from "../reactions";
|
||||
import { type GridMode } from "../state/CallViewModel/CallViewModel";
|
||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||
import { MediaDevices } from "../state/MediaDevices";
|
||||
import { globalScope } from "../state/ObservableScope";
|
||||
import { constant } from "../state/Behavior";
|
||||
import { type LayoutMode } from "../state/LayoutSwitchViewModel";
|
||||
|
||||
// consts for tests
|
||||
const reactionIdentifier = "@user:example.com:DEVICE";
|
||||
const reactionData = {
|
||||
@@ -32,6 +34,7 @@ const mediaDevices = new MediaDevices(globalScope);
|
||||
/**
|
||||
* A wrapper component that is used for:
|
||||
* - exposing the snapshot via props so the storybook documents the snapshot properties (basically unpack them form the vm)
|
||||
* - constructing the layout switch view model
|
||||
* - Add additional react context
|
||||
* The paraeters are all params from the FooterSnapshot,
|
||||
* the Snapshot of the vm, the wrapper will create a mocked vm from it and pass it to the CallFooter.
|
||||
@@ -40,11 +43,18 @@ const mediaDevices = new MediaDevices(globalScope);
|
||||
*/
|
||||
function CallFooterStoryWrapper({
|
||||
children,
|
||||
layout,
|
||||
setLayout,
|
||||
...vmSnapshot
|
||||
}: FooterSnapshot & {
|
||||
}: Omit<FooterSnapshot, "layoutSwitchVm"> & {
|
||||
children?: false | JSX.Element | JSX.Element[] | undefined;
|
||||
layout: LayoutMode | null;
|
||||
setLayout: (value: LayoutMode) => void;
|
||||
}): ReactNode {
|
||||
const vm = useStaticViewModel(vmSnapshot);
|
||||
const vm = useStaticViewModel({
|
||||
...vmSnapshot,
|
||||
layoutSwitchVm: layout && { layout$: constant(layout), setLayout },
|
||||
});
|
||||
return (
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<div className={inCallViewStyles.inRoom}>
|
||||
@@ -62,28 +72,50 @@ function CallFooterStoryWrapper({
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
component: CallFooterStoryWrapper,
|
||||
} satisfies Meta<typeof CallFooterStoryWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const fnArgType = {
|
||||
control: { type: "select" as const },
|
||||
options: ["MockedCallback", "undefined"],
|
||||
mapping: { MockedCallback: fn(), undefined: undefined },
|
||||
};
|
||||
|
||||
const meta = {
|
||||
component: CallFooterStoryWrapper,
|
||||
argTypes: {
|
||||
layout: {
|
||||
control: "radio",
|
||||
options: ["grid", "spotlight"] satisfies LayoutMode[],
|
||||
},
|
||||
audioOutputSwitcher: {
|
||||
control: "select",
|
||||
options: ["NoOutputCallback", "speaker", "earpiece"],
|
||||
table: { defaultValue: { summary: "NoOutputCallback" } },
|
||||
mapping: {
|
||||
NoOutputCallback: undefined,
|
||||
// This is inverersed (speaker<->earpice) because the switcher object stores the target output, not the current one.
|
||||
speaker: { targetOutput: "earpiece", switch: fn() },
|
||||
earpiece: { targetOutput: "speaker", switch: fn() },
|
||||
},
|
||||
},
|
||||
toggleScreenSharing: fnArgType,
|
||||
openSettings: fnArgType,
|
||||
toggleAudio: fnArgType,
|
||||
toggleVideo: fnArgType,
|
||||
hangup: fnArgType,
|
||||
},
|
||||
} satisfies Meta<typeof CallFooterStoryWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
showLogo: false,
|
||||
layoutMode: "grid",
|
||||
layout: "grid",
|
||||
setLayout: fn(),
|
||||
audioEnabled: true,
|
||||
audioBusy: false,
|
||||
videoEnabled: true,
|
||||
videoBusy: false,
|
||||
setLayoutMode: fn(),
|
||||
openSettings: fn(),
|
||||
toggleAudio: fn(),
|
||||
toggleVideo: fn(),
|
||||
@@ -111,29 +143,6 @@ export const Default: Story = {
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
argTypes: {
|
||||
layoutMode: {
|
||||
control: "radio",
|
||||
options: ["grid", "spotlight"] satisfies GridMode[],
|
||||
},
|
||||
audioOutputSwitcher: {
|
||||
control: "select",
|
||||
options: ["NoOutputCallback", "speaker", "earpiece"],
|
||||
table: { defaultValue: { summary: "NoOutputCallback" } },
|
||||
mapping: {
|
||||
NoOutputCallback: undefined,
|
||||
// This is inverersed (speaker<->earpice) because the switcher object stores the target output, not the current one.
|
||||
speaker: { targetOutput: "earpiece", switch: fn() },
|
||||
earpiece: { targetOutput: "speaker", switch: fn() },
|
||||
},
|
||||
},
|
||||
toggleScreenSharing: fnArgType,
|
||||
setLayoutMode: fnArgType,
|
||||
openSettings: fnArgType,
|
||||
toggleAudio: fnArgType,
|
||||
toggleVideo: fnArgType,
|
||||
hangup: fnArgType,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAudioAndVideoOptions: Story = {
|
||||
@@ -194,7 +203,7 @@ export const AudioVideoEnabled: Story = {
|
||||
|
||||
const spotlightRadio = canvas.getByRole("radio", { name: "Spotlight" });
|
||||
await userEvent.click(spotlightRadio);
|
||||
await expect(args.setLayoutMode).toHaveBeenCalledWith("spotlight");
|
||||
await expect(args.setLayout).toHaveBeenCalledWith("spotlight");
|
||||
|
||||
const micButtonMute = canvas.getByRole("switch", {
|
||||
name: "Mute microphone",
|
||||
@@ -225,14 +234,14 @@ export const SpotlightMode: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
layoutMode: "spotlight",
|
||||
layout: "spotlight",
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const spotlightRadio = canvas.getByRole("radio", { name: "Grid" });
|
||||
await userEvent.click(spotlightRadio);
|
||||
await expect(args.setLayoutMode).toHaveBeenCalledWith("grid");
|
||||
await expect(args.setLayout).toHaveBeenCalledWith("grid");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -264,7 +273,7 @@ export const Pip: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
buttonSize: "md",
|
||||
layoutMode: undefined,
|
||||
layout: null,
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -348,7 +357,7 @@ export const Lobby: Story = {
|
||||
...Default.args,
|
||||
showLogo: false,
|
||||
openSettings: undefined,
|
||||
setLayoutMode: undefined,
|
||||
layout: null,
|
||||
toggleScreenSharing: undefined,
|
||||
},
|
||||
parameters: {
|
||||
@@ -362,7 +371,7 @@ export const LobbyMobile: Story = {
|
||||
...Default.args,
|
||||
showLogo: false,
|
||||
|
||||
setLayoutMode: undefined,
|
||||
layout: null,
|
||||
toggleScreenSharing: undefined,
|
||||
},
|
||||
globals: {
|
||||
@@ -379,7 +388,7 @@ export const LobbyRecentButton: Story = {
|
||||
...Default.args,
|
||||
children: <Link>Back To Recents</Link>,
|
||||
showLogo: false,
|
||||
setLayoutMode: undefined,
|
||||
layout: null,
|
||||
toggleScreenSharing: undefined,
|
||||
},
|
||||
parameters: {
|
||||
@@ -393,7 +402,7 @@ export const LobbyRecentButtonMobile: Story = {
|
||||
...Default.args,
|
||||
children: <Link>Back To Recents</Link>,
|
||||
showLogo: false,
|
||||
setLayoutMode: undefined,
|
||||
layout: null,
|
||||
toggleScreenSharing: undefined,
|
||||
},
|
||||
globals: {
|
||||
|
||||
@@ -7,12 +7,6 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { type FC, type JSX, type Ref, useMemo } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
SpotlightIcon,
|
||||
GridIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { Switch } from "@vector-im/compound-web";
|
||||
import { t } from "i18next";
|
||||
|
||||
import LogoMark from "../icons/LogoMark.svg?react";
|
||||
import LogoType from "../icons/LogoType.svg?react";
|
||||
@@ -28,13 +22,14 @@ import {
|
||||
type ReactionData,
|
||||
} from "../button";
|
||||
import styles from "./CallFooter.module.css";
|
||||
import { type GridMode } from "../state/CallViewModel/CallViewModel";
|
||||
import {
|
||||
MediaMuteAndSwitchButton,
|
||||
type MenuOptions,
|
||||
} from "./MediaMuteAndSwitchButton";
|
||||
import { type ViewModel } from "../state/ViewModel";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
import { type LayoutSwitchViewModel } from "../state/LayoutSwitchViewModel";
|
||||
import { LayoutSwitch } from "../room/LayoutSwitch";
|
||||
|
||||
export interface AudioOutputSwitcher {
|
||||
targetOutput: string;
|
||||
@@ -61,8 +56,6 @@ export interface FooterActions {
|
||||
/** Also controls if the videoMute button is disabled */
|
||||
toggleVideo: (() => void) | undefined;
|
||||
toggleBlur: (() => void) | undefined;
|
||||
/** Also controls if the layout button is visible */
|
||||
setLayoutMode: ((mode: GridMode) => void) | undefined;
|
||||
toggleScreenSharing: (() => void) | undefined;
|
||||
/** Also controls if the settings button is visible */
|
||||
openSettings: (() => void) | undefined;
|
||||
@@ -87,7 +80,8 @@ export interface FooterState {
|
||||
buttonSize: "md" | "lg";
|
||||
showLogo: boolean;
|
||||
|
||||
layoutMode: GridMode | undefined;
|
||||
/** Also controls if the layout switch is visible */
|
||||
layoutSwitchVm: LayoutSwitchViewModel | null;
|
||||
|
||||
sharingScreen: boolean;
|
||||
|
||||
@@ -112,16 +106,21 @@ export interface FooterState {
|
||||
}
|
||||
|
||||
export interface FooterProps {
|
||||
className?: string;
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
children?: JSX.Element | JSX.Element[] | false;
|
||||
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 showFooter = useBehavior(vm.showFooter$);
|
||||
const hideControls = useBehavior(vm.hideControls$);
|
||||
const layoutMode = useBehavior(vm.layoutMode$);
|
||||
const setLayoutMode = useBehavior(vm.setLayoutMode$);
|
||||
const layoutSwitchVm = useBehavior(vm.layoutSwitchVm$);
|
||||
const openSettings = useBehavior(vm.openSettings$);
|
||||
const audioEnabled = useBehavior(vm.audioEnabled$);
|
||||
const audioBusy = useBehavior(vm.audioBusy$);
|
||||
@@ -292,7 +291,7 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
<div
|
||||
ref={ref}
|
||||
data-testid="footer-container"
|
||||
className={classNames(styles.footer, {
|
||||
className={classNames(className, styles.footer, {
|
||||
[styles.overlay]: asOverlay,
|
||||
[styles.hidden]: !showFooter,
|
||||
})}
|
||||
@@ -311,20 +310,8 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
{(showLogo || debugTileLayout) && logoDebugContainer}
|
||||
</div>
|
||||
{!hideControls && <div className={styles.buttons}>{buttons}</div>}
|
||||
{!hideControls && setLayoutMode && layoutMode && (
|
||||
<Switch<"spotlight", "grid">
|
||||
name="layoutMode"
|
||||
aria-label={t("layout_switch_label")}
|
||||
leftLabel={t("layout_spotlight_label")}
|
||||
leftValue="spotlight"
|
||||
leftIcon={SpotlightIcon}
|
||||
rightLabel={t("layout_grid_label")}
|
||||
rightValue="grid"
|
||||
rightIcon={GridIcon}
|
||||
className={styles.layout}
|
||||
value={layoutMode}
|
||||
onChange={setLayoutMode}
|
||||
/>
|
||||
{!hideControls && layoutSwitchVm && (
|
||||
<LayoutSwitch vm={layoutSwitchVm} className={styles.layout} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -173,9 +173,7 @@ export function createCallFooterViewModel(
|
||||
callModel.setSettingsOpen$,
|
||||
]).pipe(
|
||||
map(([isPip, showHeader, setSettingsOpen]) =>
|
||||
!isPip &&
|
||||
!(headerStyle === HeaderStyle.AppBar && showHeader) &&
|
||||
showControls
|
||||
!isPip && headerStyle !== HeaderStyle.AppBar && showControls
|
||||
? (): void => setSettingsOpen(true)
|
||||
: undefined,
|
||||
),
|
||||
@@ -184,14 +182,7 @@ export function createCallFooterViewModel(
|
||||
|
||||
showLogo$: scope.behavior(isPip$.pipe(map((isPip) => showLogo && !isPip))),
|
||||
|
||||
layoutMode$: callModel.gridMode$,
|
||||
setLayoutMode$: scope.behavior(
|
||||
isPip$.pipe(
|
||||
map((isPip) =>
|
||||
!isPip && showControls ? callModel.setGridMode : undefined,
|
||||
),
|
||||
),
|
||||
),
|
||||
layoutSwitchVm$: callModel.layoutSwitchVm$,
|
||||
|
||||
sharingScreen$: callModel.sharingScreen$,
|
||||
toggleScreenSharing$: constant(callModel.toggleScreenSharing ?? undefined),
|
||||
@@ -249,20 +240,18 @@ export function createLobbyFooterViewModel(
|
||||
hideControls: false,
|
||||
asOverlay: false,
|
||||
buttonSize: "lg",
|
||||
showLayoutSwitcher: false,
|
||||
openSettings,
|
||||
hangup,
|
||||
debugTileLayout: false,
|
||||
showFooter: true,
|
||||
toggleAudio: undefined,
|
||||
toggleVideo: undefined,
|
||||
setLayoutMode: undefined,
|
||||
toggleScreenSharing: undefined,
|
||||
audioEnabled: undefined,
|
||||
audioBusy: false,
|
||||
videoEnabled: undefined,
|
||||
videoBusy: false,
|
||||
layoutMode: undefined,
|
||||
layoutSwitchVm: null,
|
||||
sharingScreen: false,
|
||||
audioOutputSwitcher: undefined,
|
||||
reactionIdentifier: undefined,
|
||||
|
||||
@@ -180,12 +180,12 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Microphone" }));
|
||||
screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 1" });
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 2" });
|
||||
await user.keyboard("[Escape]");
|
||||
await user.click(screen.getByRole("button", { name: "Camera" }));
|
||||
screen.getByRole("menuitem", { name: "Camera 1" });
|
||||
screen.getByRole("menuitem", { name: "Camera 2" });
|
||||
screen.getByRole("menuitemradio", { name: "Camera 1" });
|
||||
screen.getByRole("menuitemradio", { name: "Camera 2" });
|
||||
});
|
||||
|
||||
test("calls select callback on menu click", async () => {
|
||||
@@ -206,7 +206,9 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
await user.click(
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 2" }),
|
||||
);
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith("mic2");
|
||||
});
|
||||
@@ -228,7 +230,9 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 1" }));
|
||||
await user.click(
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 1" }),
|
||||
);
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -264,18 +268,24 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
const { getByRole } = renderComponent(<Wrapper />);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
await user.click(
|
||||
screen.getByRole("menuitemradio", { name: "Microphone 2" }),
|
||||
);
|
||||
|
||||
expect(onSelectPressed).toHaveBeenCalled();
|
||||
expect(onOptionUpdated).not.toHaveBeenCalled();
|
||||
// After clicking, plannedSelection="mic2" but selectedOption is still "mic1",
|
||||
// so a spinner should appear on the mic2 item
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
expect(mic2Item.querySelector(".rotate")).toBeTruthy();
|
||||
// so mic2 should be in an activating state
|
||||
screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 2 Activating…",
|
||||
checked: false,
|
||||
});
|
||||
|
||||
// The currently-selected mic1 item should not have a spinner
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
expect(mic1Item.querySelector(".rotate")).toBeNull();
|
||||
// The currently-selected mic1 item should not be activating
|
||||
screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 1",
|
||||
checked: true,
|
||||
});
|
||||
await act(async () => {
|
||||
// resolve the promise that acutally updates the select option.
|
||||
resolve();
|
||||
@@ -284,7 +294,7 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
|
||||
expect(onOptionUpdated).toHaveBeenCalled();
|
||||
// Spinner should now be gone since the selection has caught up
|
||||
const mic2ItemAfter = screen.getByRole("menuitem", {
|
||||
const mic2ItemAfter = screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 2",
|
||||
});
|
||||
expect(mic2ItemAfter.querySelector(".rotate")).toBeNull();
|
||||
@@ -336,11 +346,15 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
|
||||
// The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
const mic1Item = screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 2",
|
||||
});
|
||||
expect(mic1Item.querySelectorAll("svg").length).toBe(2);
|
||||
|
||||
// The unselected item (mic1) only renders its IconOptions SVG
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
const mic2Item = screen.getByRole("menuitemradio", {
|
||||
name: "Microphone 1",
|
||||
});
|
||||
expect(mic2Item.querySelectorAll("svg").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,6 +191,7 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.itemIcon}
|
||||
aria-hidden
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -201,10 +202,23 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
onSelect?.(id);
|
||||
}}
|
||||
key={id}
|
||||
role="menuitemradio"
|
||||
aria-checked={selectedOption === id}
|
||||
>
|
||||
{selectedOption === id && <CheckIcon width={24} height={24} />}
|
||||
{selectedOption === id && (
|
||||
<CheckIcon
|
||||
width={24}
|
||||
height={24}
|
||||
aria-hidden // A label would be redundant to aria-checked above
|
||||
/>
|
||||
)}
|
||||
{selectedOption !== id && plannedSelection === id && (
|
||||
<SpinnerIcon width={24} height={24} className={styles.rotate} />
|
||||
<SpinnerIcon
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.rotate}
|
||||
aria-label={t("settings.devices.activating")}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_e649de"
|
||||
>
|
||||
<button
|
||||
aria-busy="false"
|
||||
@@ -35,7 +35,7 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Microphone"
|
||||
class="_button_1nw83_8 menuButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _menuButton_e649de _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
data-state="closed"
|
||||
|
||||
@@ -183,12 +183,6 @@ export interface ConfigOptions {
|
||||
// Overrides members from ConfigOptions that are always provided by the
|
||||
// default config and are therefore non-optional.
|
||||
export interface ResolvedConfigOptions extends ConfigOptions {
|
||||
default_server_config: {
|
||||
["m.homeserver"]: {
|
||||
base_url: string;
|
||||
server_name: string;
|
||||
};
|
||||
};
|
||||
sync_disconnect_grace_period_ms: number;
|
||||
ssla: string;
|
||||
matrix_rtc_session: {
|
||||
@@ -202,12 +196,6 @@ export interface ResolvedConfigOptions extends ConfigOptions {
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||
default_server_config: {
|
||||
["m.homeserver"]: {
|
||||
base_url: "http://localhost:8008",
|
||||
server_name: "localhost",
|
||||
},
|
||||
},
|
||||
features: {
|
||||
feature_use_device_session_member_events: true,
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Controls {
|
||||
canEnterPip(): boolean;
|
||||
enablePip(): void;
|
||||
disablePip(): void;
|
||||
onPipMediaOrientationUpdate?: (orientation: "landscape" | "portrait") => void;
|
||||
|
||||
setAvailableAudioDevices(devices: OutputDevice[]): void;
|
||||
setAudioDevice(id: string): void;
|
||||
|
||||
@@ -10,15 +10,15 @@ import {
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} 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";
|
||||
const logger = rootLogger.getChild("[MatrixKeyProvider]");
|
||||
|
||||
export class MatrixKeyProvider extends BaseKeyProvider {
|
||||
private rtcSession?: MatrixRTCSession;
|
||||
|
||||
private logger: Logger;
|
||||
public constructor() {
|
||||
super({ ratchetWindowSize: 10, keyringSize: 256 });
|
||||
this.logger = rootLogger.getChild("[MatrixKeyProvider]");
|
||||
}
|
||||
|
||||
public setRTCSession(rtcSession: MatrixRTCSession): void {
|
||||
@@ -60,12 +60,12 @@ export class MatrixKeyProvider extends BaseKeyProvider {
|
||||
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}`,
|
||||
);
|
||||
},
|
||||
(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}`,
|
||||
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 |
BIN
src/graphics/video-placeholder.gif
Normal file
BIN
src/graphics/video-placeholder.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 B |
@@ -10,28 +10,28 @@ import { type ReactNode, useCallback, useMemo } from "react";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { type OneOnOneLandscapeLayout as OneOnOneLandscapeLayoutModel } from "../state/layout-types.ts";
|
||||
import { type OneOnOneDesktopLayout as OneOnOneDesktopLayoutModel } from "../state/layout-types.ts";
|
||||
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
||||
import styles from "./OneOnOneLandscapeLayout.module.css";
|
||||
import styles from "./OneOnOneDesktopLayout.module.css";
|
||||
import { type DragCallback, useUpdateLayout } from "./Grid";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
|
||||
/**
|
||||
* An implementation of the "one-on-one" layout for landscape screens, in which
|
||||
* An implementation of the "one-on-one" layout for desktop platforms, in which
|
||||
* the remote participant is shown at maximum size, overlaid by a small view of
|
||||
* the local participant.
|
||||
*/
|
||||
export const makeOneOnOneLandscapeLayout: CallLayout<
|
||||
OneOnOneLandscapeLayoutModel
|
||||
export const makeOneOnOneDesktopLayout: CallLayout<
|
||||
OneOnOneDesktopLayoutModel
|
||||
> = ({ minBounds$ }) => ({
|
||||
foreground: "fixed",
|
||||
|
||||
fixed: function OneOnOneLandscapeLayoutFixed({ ref }): ReactNode {
|
||||
fixed: function OneOnOneDesktopLayoutFixed({ ref }): ReactNode {
|
||||
useUpdateLayout();
|
||||
return <div ref={ref} />;
|
||||
},
|
||||
|
||||
scrolling: function OneOnOneLandscapeLayoutScrolling({
|
||||
scrolling: function OneOnOneDesktopLayoutScrolling({
|
||||
ref,
|
||||
model,
|
||||
Slot,
|
||||
@@ -19,14 +19,28 @@ Please see LICENSE in the repository root for full details.
|
||||
inset: var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
/* Give the PiP a landscape aspect ratio */
|
||||
.pip[data-size="sm"] {
|
||||
inline-size: 88px;
|
||||
block-size: 132px;
|
||||
inline-size: 132px;
|
||||
block-size: 88px;
|
||||
}
|
||||
|
||||
.pip[data-size="lg"] {
|
||||
inline-size: 140px;
|
||||
block-size: 210px;
|
||||
inline-size: 210px;
|
||||
block-size: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
/* Give the PiP a portrait aspect ratio */
|
||||
.pip[data-size="sm"] {
|
||||
inline-size: 88px;
|
||||
block-size: 132px;
|
||||
}
|
||||
|
||||
.pip[data-size="lg"] {
|
||||
inline-size: 140px;
|
||||
block-size: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
.pip[data-block-alignment="start"] {
|
||||
@@ -9,23 +9,23 @@ Please see LICENSE in the repository root for full details.
|
||||
import { type ReactNode, useCallback } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { type OneOnOnePortraitLayout as OneOnOnePortraitLayoutModel } from "../state/layout-types.ts";
|
||||
import { type OneOnOneMobileLayout as OneOnOneMobileLayoutModel } from "../state/layout-types.ts";
|
||||
import { type CallLayout } from "./CallLayout";
|
||||
import styles from "./OneOnOnePortraitLayout.module.css";
|
||||
import styles from "./OneOnOneMobileLayout.module.css";
|
||||
import { type DragCallback, useUpdateLayout } from "./Grid";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
|
||||
/**
|
||||
* An implementation of the "one-on-one" layout for portrait screens, in which
|
||||
* An implementation of the "one-on-one" layout for mobile platforms, in which
|
||||
* the remote participant is shown at maximum size, overlaid by a small view of
|
||||
* the local participant.
|
||||
*/
|
||||
export const makeOneOnOnePortraitLayout: CallLayout<
|
||||
OneOnOnePortraitLayoutModel
|
||||
export const makeOneOnOneMobileLayout: CallLayout<
|
||||
OneOnOneMobileLayoutModel
|
||||
> = () => ({
|
||||
foreground: "scrolling",
|
||||
|
||||
fixed: function OneOnOnePortraitLayoutFixed({ ref, model, Slot }): ReactNode {
|
||||
fixed: function OneOnOneMobileLayoutFixed({ ref, model, Slot }): ReactNode {
|
||||
useUpdateLayout();
|
||||
return (
|
||||
<div ref={ref} className={styles.layer}>
|
||||
@@ -38,7 +38,7 @@ export const makeOneOnOnePortraitLayout: CallLayout<
|
||||
);
|
||||
},
|
||||
|
||||
scrolling: function OneOnOnePortraitLayoutScrolling({
|
||||
scrolling: function OneOnOneMobileLayoutScrolling({
|
||||
ref,
|
||||
model,
|
||||
Slot,
|
||||
@@ -15,8 +15,7 @@ Please see LICENSE in the repository root for full details.
|
||||
@import url("@fontsource/inconsolata/700.css");
|
||||
|
||||
@import url("normalize.css/normalize.css") layer(normalize);
|
||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css")
|
||||
layer(compound);
|
||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
|
||||
@import url("@vector-im/compound-web/dist/style.css") layer(compound.components);
|
||||
|
||||
:root {
|
||||
@@ -28,12 +27,6 @@ layer(compound);
|
||||
--font-size-title: calc(24px * 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);
|
||||
/* 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
|
||||
@@ -55,7 +48,6 @@ layer(compound);
|
||||
--small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
|
||||
--big-drop-shadow: 0px 0px 24px 0px #1b1d221a;
|
||||
--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-header-footer-layer: 2;
|
||||
@@ -74,9 +66,6 @@ layer(compound);
|
||||
|
||||
body {
|
||||
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-scheme: dark;
|
||||
margin: 0;
|
||||
@@ -85,6 +74,24 @@ body {
|
||||
-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
|
||||
we use this for mobile pip webviews */
|
||||
.no-scroll-body {
|
||||
|
||||
@@ -173,14 +173,6 @@ Please see LICENSE in the repository root for full details.
|
||||
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 {
|
||||
background: var(--cpd-color-text-action-accent);
|
||||
border-color: var(--cpd-color-text-action-accent);
|
||||
|
||||
@@ -99,7 +99,7 @@ function renderTestComponent(
|
||||
),
|
||||
} as unknown as Room;
|
||||
|
||||
if (explicitTracks?.length ?? 0 > 0) {
|
||||
if ((explicitTracks?.length ?? 0) > 0) {
|
||||
tracks = explicitTracks!.map(({ participantId, source, kind }) => {
|
||||
const participant =
|
||||
liveKitParticipants.find((p) => p.identity === participantId) ??
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
AudioTrack,
|
||||
type AudioTrackProps,
|
||||
} 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 { useReactiveState } from "../useReactiveState";
|
||||
@@ -40,7 +40,6 @@ export interface MatrixAudioRendererProps {
|
||||
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.
|
||||
*
|
||||
@@ -60,6 +59,7 @@ export function LivekitRoomAudioRenderer({
|
||||
validIdentities,
|
||||
muted,
|
||||
}: MatrixAudioRendererProps): ReactNode {
|
||||
const logger = rootLogger.getChild("[MatrixAudioRenderer]");
|
||||
const tracks = useTracks(
|
||||
[
|
||||
Track.Source.Microphone,
|
||||
@@ -80,7 +80,7 @@ export function LivekitRoomAudioRenderer({
|
||||
if (!isValid) {
|
||||
// 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.
|
||||
prefixedLogger.warn(
|
||||
logger.warn(
|
||||
`Audio track ${ref.participant.identity} from ${url} has no matching matrix call member`,
|
||||
`current members: ${validIdentities.join()}`,
|
||||
`track will not get rendered`,
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import {
|
||||
ProcessorWrapper,
|
||||
supportsBackgroundProcessors,
|
||||
supportsBackgroundProcessors as supportsBackgroundProcessorsLivekitSdk,
|
||||
type BackgroundOptions,
|
||||
} from "@livekit/track-processors";
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer";
|
||||
import { type Behavior } from "../state/Behavior";
|
||||
import { type ObservableScope } from "../state/ObservableScope";
|
||||
import { platform } from "../Platform";
|
||||
|
||||
//TODO-MULTI-SFU: This is not yet fully there.
|
||||
// it is a combination of exposing observable and react hooks.
|
||||
@@ -106,6 +107,10 @@ interface Props {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
function supportsBackgroundProcessors(): boolean {
|
||||
return supportsBackgroundProcessorsLivekitSdk() && platform === "desktop";
|
||||
}
|
||||
|
||||
export const ProcessorProvider: FC<Props> = ({ children }) => {
|
||||
// The setting the user wants to have
|
||||
const [blurActivated] = useSetting(backgroundBlurSettings);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.reactionIndicatorWidget {
|
||||
display: flex;
|
||||
background-color: #00000030;
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
box-shadow: 0 0 var(--cpd-space-2x) #00000040;
|
||||
background: "ffffff40";
|
||||
color: var(--cpd-color-text-on-solid-primary);
|
||||
background: var(--cpd-color-icon-secondary-alpha);
|
||||
backdrop-filter: blur(10px);
|
||||
outline: var(--cpd-border-width-1) solid var(--cpd-color-alpha-gray-400);
|
||||
outline-offset: calc(-1 * var(--cpd-border-width-1));
|
||||
@@ -33,7 +33,6 @@
|
||||
|
||||
.reaction {
|
||||
margin: var(--cpd-space-1x);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
exports[`RaisedHandIndicator > renders a smaller indicator when miniature is specified 1`] = `
|
||||
<div
|
||||
class="reactionIndicatorWidget"
|
||||
class="_reactionIndicatorWidget_abd277"
|
||||
>
|
||||
<div
|
||||
class="reaction"
|
||||
class="_reaction_abd277"
|
||||
>
|
||||
<span
|
||||
aria-label="Reaction"
|
||||
@@ -22,10 +22,10 @@ exports[`RaisedHandIndicator > renders a smaller indicator when miniature is spe
|
||||
|
||||
exports[`RaisedHandIndicator > renders an indicator when a hand has been raised 1`] = `
|
||||
<div
|
||||
class="reactionIndicatorWidget reactionIndicatorWidgetLarge"
|
||||
class="_reactionIndicatorWidget_abd277 _reactionIndicatorWidgetLarge_abd277"
|
||||
>
|
||||
<div
|
||||
class="reaction reactionLarge"
|
||||
class="_reaction_abd277 _reactionLarge_abd277"
|
||||
>
|
||||
<span
|
||||
aria-label="Reaction"
|
||||
@@ -42,10 +42,10 @@ exports[`RaisedHandIndicator > renders an indicator when a hand has been raised
|
||||
|
||||
exports[`RaisedHandIndicator > renders an indicator when a hand has been raised with the expected time 1`] = `
|
||||
<div
|
||||
class="reactionIndicatorWidget reactionIndicatorWidgetLarge"
|
||||
class="_reactionIndicatorWidget_abd277 _reactionIndicatorWidgetLarge_abd277"
|
||||
>
|
||||
<div
|
||||
class="reaction reactionLarge"
|
||||
class="_reaction_abd277 _reactionLarge_abd277"
|
||||
>
|
||||
<span
|
||||
aria-label="Reaction"
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
LivekitConnectionError,
|
||||
MatrixRTCTransportMissingError,
|
||||
PeerConnectionTimeoutError,
|
||||
StickyEventsRequiredError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
import { mockConfig } from "../utils/test.ts";
|
||||
@@ -59,6 +60,12 @@ test.each([
|
||||
expectedDescription:
|
||||
"The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.",
|
||||
},
|
||||
{
|
||||
error: new StickyEventsRequiredError(),
|
||||
expectedTitle: "Homeserver does not support Matrix 2.0 calls",
|
||||
expectedDescription:
|
||||
"This deployment is configured to use Matrix 2.0 call mode, but the homeserver does not advertise support for sticky events (MSC4354). Ask your server admin to upgrade, or switch the deployment to a compatible mode.",
|
||||
},
|
||||
])(
|
||||
"should report correct error for $expectedTitle",
|
||||
async ({ error, expectedTitle, expectedDescription }) => {
|
||||
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
vitest,
|
||||
} from "vitest";
|
||||
import { render, waitFor, screen, act } from "@testing-library/react";
|
||||
import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk";
|
||||
import {
|
||||
type MatrixClient,
|
||||
JoinRule,
|
||||
type RoomState,
|
||||
UnsupportedStickyEventsEndpointError,
|
||||
} from "matrix-js-sdk";
|
||||
import {
|
||||
MatrixRTCSessionEvent,
|
||||
type MatrixRTCSession,
|
||||
@@ -46,6 +51,7 @@ import {
|
||||
MockRTCSession,
|
||||
} from "../utils/test";
|
||||
import { GroupCallView } from "./GroupCallView";
|
||||
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary";
|
||||
import { ElementWidgetActions, type WidgetHelpers } from "../widget";
|
||||
import { LazyEventEmitter } from "../LazyEventEmitter";
|
||||
import { MatrixRTCTransportMissingError } from "../utils/errors";
|
||||
@@ -130,6 +136,9 @@ beforeEach(() => {
|
||||
function createGroupCallView(
|
||||
widget: WidgetHelpers | null,
|
||||
joined = true,
|
||||
options: {
|
||||
withErrorBoundary?: boolean;
|
||||
} = {},
|
||||
): {
|
||||
rtcSession: MatrixRTCSession;
|
||||
getByText: ReturnType<typeof render>["getByText"];
|
||||
@@ -166,24 +175,36 @@ function createGroupCallView(
|
||||
video: { enabled: false },
|
||||
// TODO-MULTI-SFU: This cast isn't valid, it's likely the cause of some current test failures
|
||||
} as unknown as MuteStates;
|
||||
const groupCallView = (
|
||||
<GroupCallView
|
||||
client={client}
|
||||
isPasswordlessUser={false}
|
||||
confineToRoom={false}
|
||||
preload={false}
|
||||
skipLobby={false}
|
||||
rtcSession={rtcSession.asMockedSession()}
|
||||
muteStates={muteState}
|
||||
widget={widget}
|
||||
// TODO-MULTI-SFU: Make joined and setJoined work
|
||||
joined={true}
|
||||
setJoined={function (value: boolean): void {}}
|
||||
/>
|
||||
);
|
||||
const { getByText } = render(
|
||||
<BrowserRouter>
|
||||
<TooltipProvider>
|
||||
<MediaDevicesContext value={mockMediaDevices({})}>
|
||||
<ProcessorProvider>
|
||||
<GroupCallView
|
||||
client={client}
|
||||
isPasswordlessUser={false}
|
||||
confineToRoom={false}
|
||||
preload={false}
|
||||
skipLobby={false}
|
||||
rtcSession={rtcSession.asMockedSession()}
|
||||
muteStates={muteState}
|
||||
widget={widget}
|
||||
// TODO-MULTI-SFU: Make joined and setJoined work
|
||||
joined={true}
|
||||
setJoined={function (value: boolean): void {}}
|
||||
/>
|
||||
{options.withErrorBoundary ? (
|
||||
<GroupCallErrorBoundary
|
||||
recoveryActionHandler={vi.fn()}
|
||||
widget={null}
|
||||
>
|
||||
{groupCallView}
|
||||
</GroupCallErrorBoundary>
|
||||
) : (
|
||||
groupCallView
|
||||
)}
|
||||
</ProcessorProvider>
|
||||
</MediaDevicesContext>
|
||||
</TooltipProvider>
|
||||
@@ -251,7 +272,7 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn
|
||||
expect(leaveRTCSession).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("Should close widget when all other left and have time to play a sound", async () => {
|
||||
test("Should close widget when all other left and play a sound", async () => {
|
||||
const user = userEvent.setup();
|
||||
let widgetClosedCalled = false;
|
||||
const { promise: widgetClosedPromise, resolve: widgetClosedResolver } =
|
||||
@@ -289,8 +310,6 @@ test("Should close widget when all other left and have time to play a sound", as
|
||||
expect(widgetClosedCalled).toBeFalsy();
|
||||
resolvePlaySound.resolve();
|
||||
|
||||
// Expect the leave sound to be played but silent (volumeOverwrite = 0)
|
||||
// The allOthersLeft effect should already play a leave sound for the last user in the call.
|
||||
expect(playSound).toHaveBeenCalledWith("left", 0);
|
||||
await widgetClosedPromise;
|
||||
await flushPromises();
|
||||
@@ -298,37 +317,6 @@ test("Should close widget when all other left and have time to play a sound", as
|
||||
expect(widgetStopMock).toHaveBeenCalledOnce();
|
||||
}, 80000);
|
||||
|
||||
test("Should close widget when all other left", async () => {
|
||||
const user = userEvent.setup();
|
||||
const widgetClosedCalled = Promise.withResolvers<void>();
|
||||
const widgetSendMock = vi.fn().mockImplementation((action: string) => {
|
||||
if (action === ElementWidgetActions.Close) {
|
||||
widgetClosedCalled.resolve();
|
||||
}
|
||||
});
|
||||
const widgetStopMock = vi.fn().mockResolvedValue(undefined);
|
||||
const widget = {
|
||||
api: {
|
||||
setAlwaysOnScreen: vi.fn().mockResolvedValue(true),
|
||||
transport: {
|
||||
send: widgetSendMock,
|
||||
reply: vi.fn().mockResolvedValue(undefined),
|
||||
stop: widgetStopMock,
|
||||
} as unknown as ITransport,
|
||||
} as Partial<WidgetHelpers["api"]>,
|
||||
lazyActions: new LazyEventEmitter(),
|
||||
};
|
||||
|
||||
const { getByText } = createGroupCallView(widget as WidgetHelpers);
|
||||
const leaveButton = getByText("SimulateOtherLeft");
|
||||
await user.click(leaveButton);
|
||||
await flushPromises();
|
||||
|
||||
await widgetClosedCalled.promise;
|
||||
await flushPromises();
|
||||
expect(widgetStopMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("Should not close widget when auto leave due to error", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -394,6 +382,46 @@ test.skip("GroupCallView shows errors that occur during joining", async () => {
|
||||
screen.getByText("Call is not supported");
|
||||
});
|
||||
|
||||
test("translates wrapped UnsupportedStickyEventsEndpointError to the StickyEventsRequiredError screen", async () => {
|
||||
// Mirror the shape the SDK emits: the MembershipManager scheduler wraps
|
||||
// the original UnsupportedStickyEventsEndpointError in a generic Error
|
||||
// but preserves the original on `.cause`.
|
||||
const stickyError = new UnsupportedStickyEventsEndpointError(
|
||||
"Server does not support the sticky events",
|
||||
"sendStickyEvent",
|
||||
);
|
||||
const wrappedError = new Error(
|
||||
"The MembershipManager shut down because of the end condition: " +
|
||||
String(stickyError),
|
||||
{ cause: stickyError },
|
||||
);
|
||||
|
||||
const { rtcSession } = createGroupCallView(null, true, {
|
||||
withErrorBoundary: true,
|
||||
});
|
||||
|
||||
await act(() =>
|
||||
rtcSession.emit(MatrixRTCSessionEvent.MembershipManagerError, wrappedError),
|
||||
);
|
||||
|
||||
await screen.findByText("Homeserver does not support Matrix 2.0 calls");
|
||||
});
|
||||
|
||||
test("falls back to ConnectionLostError for unrecognised membership manager errors", async () => {
|
||||
const { rtcSession } = createGroupCallView(null, true, {
|
||||
withErrorBoundary: true,
|
||||
});
|
||||
|
||||
await act(() =>
|
||||
rtcSession.emit(
|
||||
MatrixRTCSessionEvent.MembershipManagerError,
|
||||
new Error("something else broke"),
|
||||
),
|
||||
);
|
||||
|
||||
await screen.findByText("Connection lost");
|
||||
});
|
||||
|
||||
test("user can reconnect after a membership manager error", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rtcSession } = createGroupCallView(null, true);
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type MatrixClient, JoinRule, type Room } from "matrix-js-sdk";
|
||||
import {
|
||||
type MatrixClient,
|
||||
JoinRule,
|
||||
type Room,
|
||||
UnsupportedStickyEventsEndpointError,
|
||||
} from "matrix-js-sdk";
|
||||
import {
|
||||
Room as LivekitRoom,
|
||||
isE2EESupported as isE2EESupportedBrowser,
|
||||
@@ -67,6 +72,7 @@ import {
|
||||
ConnectionLostError,
|
||||
E2EENotSupportedError,
|
||||
ElementCallError,
|
||||
StickyEventsRequiredError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
|
||||
@@ -162,8 +168,22 @@ export const GroupCallView: FC<Props> = ({
|
||||
useTypedEventEmitter(
|
||||
rtcSession,
|
||||
MatrixRTCSessionEvent.MembershipManagerError,
|
||||
(error) => setExternalError(new ConnectionLostError()),
|
||||
(error) => {
|
||||
// When matrix_rtc_mode=matrix_2_0 is in effect but the homeserver does
|
||||
// not advertise MSC4354 (sticky events), the SDK throws an
|
||||
// `UnsupportedStickyEventsEndpointError`. The MembershipManager
|
||||
// scheduler wraps it and exposes the original via `.cause`.
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.cause instanceof UnsupportedStickyEventsEndpointError
|
||||
) {
|
||||
setExternalError(new StickyEventsRequiredError());
|
||||
} else {
|
||||
setExternalError(new ConnectionLostError());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Sanity check the room object
|
||||
if (client.getRoom(rtcSession.room.roomId) !== rtcSession.room)
|
||||
@@ -533,11 +553,9 @@ export const GroupCallView: FC<Props> = ({
|
||||
});
|
||||
}
|
||||
}}
|
||||
onError={
|
||||
(/**error*/) => {
|
||||
if (rtcSession.isJoined()) onLeft("error");
|
||||
}
|
||||
}
|
||||
onError={(_error) => {
|
||||
if (rtcSession.isJoined()) onLeft("error");
|
||||
}}
|
||||
>
|
||||
{body}
|
||||
</GroupCallErrorBoundary>
|
||||
|
||||
@@ -14,6 +14,23 @@ Please see LICENSE in the repository root for full details.
|
||||
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 {
|
||||
position: sticky;
|
||||
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
|
||||
elements behind it */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fixedGrid > :not(:first-child),
|
||||
.scrollingGrid > :not(:first-child) {
|
||||
pointer-events: initial;
|
||||
> :not(:first-child) {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.tile {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tile {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
}
|
||||
|
||||
.tile.maximised {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
@@ -15,12 +14,7 @@ import {
|
||||
type MockedFunction,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import {
|
||||
render,
|
||||
type RenderResult,
|
||||
getByRole,
|
||||
screen,
|
||||
} from "@testing-library/react";
|
||||
import { render, type RenderResult } from "@testing-library/react";
|
||||
import { type LocalParticipant } from "livekit-client";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
@@ -50,7 +44,6 @@ import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
||||
import { constant } from "../state/Behavior";
|
||||
import { AppBar } from "../AppBar";
|
||||
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", () => {
|
||||
it("is visible and can be clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -44,14 +44,13 @@ import {
|
||||
createCallViewModel$,
|
||||
} from "../state/CallViewModel/CallViewModel.ts";
|
||||
import { Grid, type TileProps } from "../grid/Grid";
|
||||
import { useInitial } from "../useInitial";
|
||||
import { SpotlightTile } from "../tile/SpotlightTile";
|
||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { makeGridLayout } from "../grid/GridLayout";
|
||||
import { type CallLayoutOutputs } from "../grid/CallLayout";
|
||||
import { makeOneOnOneLandscapeLayout } from "../grid/OneOnOneLandscapeLayout";
|
||||
import { makeOneOnOnePortraitLayout } from "../grid/OneOnOnePortraitLayout";
|
||||
import { makeOneOnOneDesktopLayout } from "../grid/OneOnOneDesktopLayout";
|
||||
import { makeOneOnOneMobileLayout } from "../grid/OneOnOneMobileLayout";
|
||||
import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout";
|
||||
import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout";
|
||||
import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout";
|
||||
@@ -69,22 +68,24 @@ import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
|
||||
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
||||
import { useMediaDevices } from "../MediaDevicesContext.ts";
|
||||
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
||||
import { useAppBarHidden, useAppBarSecondaryButton } from "../AppBar.tsx";
|
||||
import {
|
||||
useAppBarHidden,
|
||||
useAppBarSecondaryButton,
|
||||
useAppBarSubtitle,
|
||||
} from "../AppBar.tsx";
|
||||
import { useBehavior } from "../useBehavior.ts";
|
||||
import { constant } from "../state/Behavior.ts";
|
||||
import { Toast } from "../Toast.tsx";
|
||||
import overlayStyles from "../Overlay.module.css";
|
||||
import { 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 { type Layout } from "../state/layout-types.ts";
|
||||
import { ObservableScope } from "../state/ObservableScope.ts";
|
||||
import { useLatest } from "../useLatest.ts";
|
||||
import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx";
|
||||
import { SettingsIconButton } from "../button/Button.tsx";
|
||||
import { createCallFooterViewModel } from "../components/CallFooterViewModel.tsx";
|
||||
import { type ViewModel } from "../state/ViewModel.ts";
|
||||
import { RingingStatus } from "../tile/RingingStatus.tsx";
|
||||
import { RingingAudioRenderer } from "./RingingAudioRenderer.tsx";
|
||||
|
||||
declare module "react" {
|
||||
interface CSSProperties {
|
||||
@@ -93,8 +94,6 @@ declare module "react" {
|
||||
}
|
||||
}
|
||||
|
||||
const logger = rootLogger.getChild("[InCallView]");
|
||||
|
||||
export interface ActiveCallProps extends Omit<
|
||||
InCallViewProps,
|
||||
"vm" | "livekitRoom" | "connState" | "footerVm"
|
||||
@@ -115,7 +114,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
const mediaDevices = useMediaDevices();
|
||||
const trackProcessorState$ = useTrackProcessorObservable$();
|
||||
useEffect(() => {
|
||||
logger.info("START CALL VIEW SCOPE");
|
||||
rootLogger.info("START CALL VIEW SCOPE");
|
||||
const scope = new ObservableScope();
|
||||
const reactionsReader = new ReactionsReader(scope, props.rtcSession);
|
||||
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
|
||||
@@ -217,6 +216,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
muteStates,
|
||||
onShareClick,
|
||||
}) => {
|
||||
const logger = rootLogger.getChild("[InCallView]");
|
||||
const { t } = useTranslation();
|
||||
const { sendReaction, toggleRaisedHand } = useReactionsSender();
|
||||
|
||||
@@ -240,20 +240,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const { showControls, header: headerStyle } = useUrlParams();
|
||||
|
||||
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 toggleVideo = useBehavior(muteStates.video.toggle$);
|
||||
const setAudioEnabled = useBehavior(muteStates.audio.setEnabled$);
|
||||
@@ -266,12 +252,13 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
() => void toggleRaisedHand(),
|
||||
);
|
||||
|
||||
const ringing = useBehavior(vm.ringing$);
|
||||
const ringingVm = useBehavior(vm.ringingVm$);
|
||||
const audioParticipants = useBehavior(vm.livekitRoomItems$);
|
||||
const participantCount = useBehavior(vm.participantCount$);
|
||||
const reconnecting = useBehavior(vm.reconnecting$);
|
||||
const layout = useBehavior(vm.layout$);
|
||||
const edgeToEdge = useBehavior(vm.edgeToEdge$);
|
||||
const overflowing = useBehavior(vm.overflowing$);
|
||||
const showNameTags = useBehavior(vm.showNameTags$);
|
||||
const showHeader = useBehavior(vm.showHeader$);
|
||||
const settingsOpen = useBehavior(vm.settingsOpen$);
|
||||
@@ -286,22 +273,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
throw fatalCallError;
|
||||
}
|
||||
|
||||
// While ringing, loop the ringtone
|
||||
useEffect((): void | (() => void) => {
|
||||
const audio = latestPickupPhaseAudio.current;
|
||||
if (ringing && audio) {
|
||||
const endSound = audio.playSoundLooping(
|
||||
"waiting",
|
||||
audio.soundDuration["waiting"] ?? 1,
|
||||
);
|
||||
return () => {
|
||||
void endSound().catch((e) => {
|
||||
logger.error("Failed to stop ringing sound", e);
|
||||
});
|
||||
};
|
||||
}
|
||||
}, [ringing, latestPickupPhaseAudio]);
|
||||
|
||||
// iOS Safari doesn't reliably fire `click` on plain <div>s, so we listen
|
||||
// for `pointerup` instead. Scrolls end in `pointercancel`, not `pointerup`,
|
||||
// so this still only fires for taps.
|
||||
@@ -363,6 +334,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
);
|
||||
|
||||
useAppBarHidden(!showHeader);
|
||||
useAppBarSubtitle(
|
||||
ringingVm && vm.ringingStatusLocation === "app_bar" && (
|
||||
<RingingStatus vm={ringingVm} />
|
||||
),
|
||||
);
|
||||
|
||||
let header: ReactNode = null;
|
||||
switch (headerStyle) {
|
||||
@@ -457,6 +433,12 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
);
|
||||
const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$);
|
||||
const showNameTags = useBehavior(vm.showNameTags$);
|
||||
const showRingingStatus = vm.ringingStatusLocation === "tile";
|
||||
const showOutline = useBehavior(
|
||||
model instanceof GridTileViewModel
|
||||
? model.showOutline$
|
||||
: constant(false),
|
||||
);
|
||||
|
||||
return model instanceof GridTileViewModel ? (
|
||||
<GridTile
|
||||
@@ -469,6 +451,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
style={style}
|
||||
showSpeakingIndicators={showSpeakingIndicators}
|
||||
showNameTags={showNameTags}
|
||||
showRingingStatus={showRingingStatus}
|
||||
showOutline={showOutline}
|
||||
focusable={!contentObscured}
|
||||
/>
|
||||
) : (
|
||||
@@ -481,8 +465,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetHeight={targetHeight}
|
||||
showIndicators={showSpotlightIndicators}
|
||||
showNameTags={showNameTags}
|
||||
showRingingStatus={showRingingStatus}
|
||||
focusable={!contentObscured}
|
||||
className={classNames(className, styles.tile)}
|
||||
itemClassName={styles.spotlightItem}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
@@ -497,8 +483,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
"spotlight-landscape": makeSpotlightLandscapeLayout(inputs),
|
||||
"spotlight-portrait": makeSpotlightPortraitLayout(inputs),
|
||||
"spotlight-expanded": makeSpotlightExpandedLayout(inputs),
|
||||
"one-on-one-landscape": makeOneOnOneLandscapeLayout(inputs),
|
||||
"one-on-one-portrait": makeOneOnOnePortraitLayout(inputs),
|
||||
"one-on-one-desktop": makeOneOnOneDesktopLayout(inputs),
|
||||
"one-on-one-mobile": makeOneOnOneMobileLayout(inputs),
|
||||
};
|
||||
}, [gridBoundsObservable$]);
|
||||
|
||||
@@ -507,7 +493,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
if (layout.type === "pip") {
|
||||
return (
|
||||
<SpotlightTile
|
||||
className={classNames(styles.tile, styles.maximised)}
|
||||
className={styles.tile}
|
||||
itemClassName={styles.spotlightItem}
|
||||
data-maximised
|
||||
vm={layout.spotlight}
|
||||
expanded
|
||||
onToggleExpanded={null}
|
||||
@@ -515,6 +503,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetHeight={gridBounds.height}
|
||||
showIndicators={false}
|
||||
showNameTags={showNameTags}
|
||||
showRingingStatus={vm.ringingStatusLocation === "tile"}
|
||||
focusable={!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
|
||||
const footer = footerVm !== null && (
|
||||
<CallFooter ref={footerRef} vm={footerVm} />
|
||||
<CallFooter className={styles.footer} ref={footerRef} vm={footerVm} />
|
||||
);
|
||||
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.
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={styles.inRoom}
|
||||
className={classNames(styles.inRoom, {
|
||||
[styles.overflowing]: overflowing,
|
||||
})}
|
||||
ref={containerRef}
|
||||
onPointerUp={onViewPointerUp}
|
||||
onPointerMove={onPointerMove}
|
||||
@@ -626,6 +617,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
{renderContent()}
|
||||
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<RingingAudioRenderer vm={ringingVm} muted={muteAllAudio} />
|
||||
{reconnectingToast}
|
||||
{earpieceOverlay}
|
||||
<ReactionsOverlay vm={vm} />
|
||||
|
||||
44
src/room/LayoutSwitch.tsx
Normal file
44
src/room/LayoutSwitch.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 { useId, type FC } from "react";
|
||||
import {
|
||||
SpotlightViewIcon,
|
||||
GridIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { type LayoutSwitchViewModel } from "../state/LayoutSwitchViewModel";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Switch } from "@vector-im/compound-web";
|
||||
|
||||
interface Props {
|
||||
vm: LayoutSwitchViewModel;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LayoutSwitch: FC<Props> = ({ vm, className }) => {
|
||||
const { t } = useTranslation();
|
||||
const layout = useBehavior(vm.layout$);
|
||||
const name = useId();
|
||||
|
||||
return (
|
||||
<Switch<"spotlight", "grid">
|
||||
name={name}
|
||||
aria-label={t("layout_switch_label")}
|
||||
leftLabel={t("layout_spotlight_label")}
|
||||
leftValue="spotlight"
|
||||
leftIcon={SpotlightViewIcon}
|
||||
rightLabel={t("layout_grid_label")}
|
||||
rightValue="grid"
|
||||
rightIcon={GridIcon}
|
||||
className={className}
|
||||
value={layout}
|
||||
onChange={vm.setLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,10 @@ import { BrowserRouter } from "react-router-dom";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { type MatrixClient } from "matrix-js-sdk";
|
||||
import { axe } from "vitest-axe";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ChevronLeftIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
@@ -20,6 +24,7 @@ import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import lobbyStyles from "./LobbyView.module.css";
|
||||
import headerStyles from "../Header.module.css";
|
||||
import { AppBar } from "../AppBar";
|
||||
|
||||
vi.mock("@livekit/components-react", () => ({
|
||||
usePreviewTracks: (): unknown[] => [],
|
||||
@@ -47,6 +52,13 @@ const mockClient = {
|
||||
getDeviceId: () => "DEVICE",
|
||||
} as Partial<MatrixClient> as MatrixClient;
|
||||
|
||||
const platformMock = vi.hoisted(() => vi.fn(() => "desktop"));
|
||||
vi.mock("../Platform", () => ({
|
||||
get platform(): string {
|
||||
return platformMock();
|
||||
},
|
||||
}));
|
||||
|
||||
const matrixInfo = {
|
||||
userId: "@user:example.org",
|
||||
displayName: "Test User",
|
||||
@@ -60,25 +72,32 @@ const matrixInfo = {
|
||||
|
||||
function renderLobbyView(
|
||||
props: Partial<Parameters<typeof LobbyView>[0]> = {},
|
||||
withAppBar = false,
|
||||
platform = "android",
|
||||
): ReturnType<typeof render> {
|
||||
platformMock.mockReturnValue(platform);
|
||||
const mediaDevices = mockMediaDevices({});
|
||||
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(
|
||||
<BrowserRouter>
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<TooltipProvider>
|
||||
<LobbyView
|
||||
client={mockClient}
|
||||
matrixInfo={matrixInfo}
|
||||
muteStates={muteStates}
|
||||
onEnter={() => {}}
|
||||
confineToRoom={false}
|
||||
hideHeader={false}
|
||||
participantCount={3}
|
||||
onShareClick={null}
|
||||
{...props}
|
||||
/>
|
||||
{withAppBar && <AppBar>{lobbyView}</AppBar>}
|
||||
{!withAppBar && lobbyView}
|
||||
</TooltipProvider>
|
||||
</MediaDevicesContext>
|
||||
</BrowserRouter>,
|
||||
@@ -97,9 +116,10 @@ describe("LobbyView", () => {
|
||||
|
||||
it("renders without header", () => {
|
||||
const { container } = renderLobbyView({ hideHeader: true });
|
||||
expect(
|
||||
container.getElementsByClassName(headerStyles.header).length,
|
||||
).toBeFalsy();
|
||||
const els = container.getElementsByClassName(headerStyles.header);
|
||||
for (const el of els) {
|
||||
expect(el).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders with waiting for invite state", () => {
|
||||
@@ -108,4 +128,50 @@ describe("LobbyView", () => {
|
||||
});
|
||||
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 { createLobbyFooterViewModel } from "../components/CallFooterViewModel";
|
||||
import { type ViewModel } from "../state/ViewModel";
|
||||
import { useAppBarPrimaryButtonIconKind } from "../AppBar";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@@ -85,8 +86,9 @@ export const LobbyView: FC<Props> = ({
|
||||
}, []);
|
||||
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(matrixInfo.roomName);
|
||||
|
||||
usePageTitle(matrixInfo.roomName);
|
||||
useAppBarPrimaryButtonIconKind("back");
|
||||
const audioEnabled = useBehavior(muteStates.audio.enabled$);
|
||||
const videoEnabled = useBehavior(muteStates.video.enabled$);
|
||||
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;
|
||||
};
|
||||
@@ -10,6 +10,8 @@ Please see LICENSE in the repository root for full details.
|
||||
margin-right: var(--content-inset-right);
|
||||
min-block-size: 0;
|
||||
block-size: 50vh;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-width: 100%;
|
||||
border-radius: var(--cpd-space-4x);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -66,12 +68,6 @@ video.mirror {
|
||||
);
|
||||
}
|
||||
|
||||
@media (min-aspect-ratio: 1 / 1) {
|
||||
.preview > video {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.preview {
|
||||
margin-inline: 0;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { TileAvatar } from "../tile/TileAvatar";
|
||||
import styles from "./VideoPreview.module.css";
|
||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import videoPlaceholder from "../graphics/video-placeholder.gif";
|
||||
|
||||
export type MatrixInfo = {
|
||||
userId: string;
|
||||
@@ -74,6 +75,9 @@ export const VideoPreview: FC<Props> = ({
|
||||
// There's no reason for this to be focusable
|
||||
tabIndex={-1}
|
||||
disablePictureInPicture
|
||||
// Set the placeholder to a small transparent image. (On Android web
|
||||
// views the default poster image is particularly ugly.)
|
||||
poster={videoPlaceholder}
|
||||
/>
|
||||
{(!videoEnabled || cameraIsStarting) && (
|
||||
<>
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -95,20 +95,20 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -161,17 +161,17 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -253,20 +253,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' er
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -318,17 +318,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' er
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -410,20 +410,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed'
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -475,17 +475,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed'
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreachable' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -567,20 +567,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreac
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -632,17 +632,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreac
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFound' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -724,20 +724,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFo
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -789,17 +789,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFo
|
||||
exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' error correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -881,20 +881,20 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' err
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -946,17 +946,17 @@ exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' err
|
||||
exports[`LiveKit ConnectionError variants > should link to troubleshoot guide when timeout error 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1038,20 +1038,20 @@ exports[`LiveKit ConnectionError variants > should link to troubleshoot guide wh
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1103,17 +1103,17 @@ exports[`LiveKit ConnectionError variants > should link to troubleshoot guide wh
|
||||
exports[`should have a close button in widget mode 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1195,20 +1195,20 @@ exports[`should have a close button in widget mode 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1257,17 +1257,17 @@ exports[`should have a close button in widget mode 1`] = `
|
||||
exports[`should render the error page with link back to home 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1349,20 +1349,20 @@ exports[`should render the error page with link back to home 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1411,17 +1411,17 @@ exports[`should render the error page with link back to home 1`] = `
|
||||
exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1503,20 +1503,20 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1565,17 +1565,17 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||
exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1657,20 +1657,20 @@ exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1720,20 +1720,20 @@ exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
exports[`should report correct error for 'Homeserver does not support Matrix 2.…' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1815,20 +1815,174 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16.713 6.713A.97.97 0 0 1 16 7a.97.97 0 0 1-.713-.287A.97.97 0 0 1 15 6q0-.424.287-.713A.97.97 0 0 1 16 5q.424 0 .712.287Q17 5.576 17 6t-.288.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm12 2H6v4h12zM6 12v-2h12v2zm0 2v2h12v-2zm0 6v-2h12v2z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Homeserver does not support Matrix 2.0 calls
|
||||
</h1>
|
||||
<p>
|
||||
This deployment is configured to use Matrix 2.0 call mode, but the homeserver does not advertise support for sticky events (MSC4354). Ask your server admin to upgrade, or switch the deployment to a compatible mode.
|
||||
</p>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Return to home screen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="30"
|
||||
viewBox="0 0 260 30"
|
||||
width="260"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<desc>
|
||||
Element Call (Beta)
|
||||
</desc>
|
||||
<circle
|
||||
cx="15"
|
||||
cy="15"
|
||||
fill="white"
|
||||
r="13"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15 30C23.2843 30 30 23.2843 30 15C30 6.71573 23.2843 0 15 0C6.71573 0 0 6.71573 0 15C0 23.2843 6.71573 30 15 30ZM12.2579 6.98923C12.2579 6.38376 12.7497 5.89292 13.3565 5.89292C17.4687 5.89292 20.8024 9.21967 20.8024 13.3234C20.8024 13.9289 20.3106 14.4197 19.7038 14.4197C19.0971 14.4197 18.6052 13.9289 18.6052 13.3234C18.6052 10.4306 16.2553 8.08554 13.3565 8.08554C12.7497 8.08554 12.2579 7.59471 12.2579 6.98923ZM24.1066 13.3235C24.1066 12.7181 23.6148 12.2272 23.008 12.2272C22.4013 12.2272 21.9094 12.7181 21.9094 13.3235C21.9094 16.2163 19.5595 18.5614 16.6607 18.5614C16.0539 18.5614 15.5621 19.0523 15.5621 19.6577C15.5621 20.2632 16.0539 20.754 16.6607 20.754C20.7729 20.754 24.1066 17.4273 24.1066 13.3235ZM17.7601 23.011C17.7601 23.6164 17.2682 24.1073 16.6615 24.1073C12.5492 24.1073 9.21553 20.7805 9.21553 16.6768C9.21553 16.0713 9.70739 15.5805 10.3141 15.5805C10.9209 15.5805 11.4127 16.0713 11.4127 16.6768C11.4127 19.5696 13.7627 21.9146 16.6615 21.9146C17.2682 21.9146 17.7601 22.4055 17.7601 23.011ZM5.89281 16.6769C5.89281 17.2824 6.38466 17.7732 6.9914 17.7732C7.59813 17.7732 8.08999 17.2824 8.08999 16.6769C8.08999 13.7841 10.4399 11.439 13.3388 11.439C13.9455 11.439 14.4373 10.9482 14.4373 10.3427C14.4373 9.73722 13.9455 9.24639 13.3388 9.24639C9.22647 9.24639 5.89281 12.5731 5.89281 16.6769Z"
|
||||
fill="#0DBD8B"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M53.5406 17.258H42.8052C42.932 18.3814 43.3397 19.2782 44.0282 19.9486C44.7167 20.6009 45.6227 20.927 46.746 20.927C47.4889 20.927 48.1593 20.7459 48.7572 20.3835C49.3551 20.0211 49.7809 19.5319 50.0346 18.9159H53.296C52.8611 20.3472 52.0458 21.5068 50.8499 22.3947C49.6722 23.2644 48.2771 23.6992 46.6645 23.6992C44.5627 23.6992 42.8596 23.0016 41.555 21.6065C40.2686 20.2114 39.6254 18.4448 39.6254 16.3068C39.6254 14.2231 40.2776 12.4747 41.5822 11.0614C42.8867 9.64814 44.5718 8.94151 46.6373 8.94151C48.7029 8.94151 50.3698 9.63908 51.6381 11.0342C52.9245 12.4112 53.5677 14.1506 53.5677 16.2524L53.5406 17.258ZM46.6373 11.5778C45.6227 11.5778 44.7801 11.8767 44.1098 12.4747C43.4394 13.0726 43.0226 13.8698 42.8596 14.8663H50.3607C50.2158 13.8698 49.8172 13.0726 49.1649 12.4747C48.5126 11.8767 47.6701 11.5778 46.6373 11.5778Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M55.7934 19.1605V2.9895H59.0276V19.2148C59.0276 19.9396 59.4262 20.302 60.2234 20.302L60.7941 20.2748V23.3459C60.4861 23.4003 60.16 23.4274 59.8157 23.4274C58.4206 23.4274 57.3969 23.0741 56.7446 22.3675C56.1104 21.6609 55.7934 20.5919 55.7934 19.1605Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M75.8563 17.258H65.121C65.2478 18.3814 65.6555 19.2782 66.344 19.9486C67.0325 20.6009 67.9384 20.927 69.0618 20.927C69.8047 20.927 70.4751 20.7459 71.073 20.3835C71.6709 20.0211 72.0967 19.5319 72.3503 18.9159H75.6117C75.1769 20.3472 74.3615 21.5068 73.1657 22.3947C71.988 23.2644 70.5928 23.6992 68.9803 23.6992C66.8785 23.6992 65.1753 23.0016 63.8708 21.6065C62.5843 20.2114 61.9411 18.4448 61.9411 16.3068C61.9411 14.2231 62.5934 12.4747 63.898 11.0614C65.2025 9.64814 66.8875 8.94151 68.9531 8.94151C71.0186 8.94151 72.6855 9.63908 73.9539 11.0342C75.2403 12.4112 75.8835 14.1506 75.8835 16.2524L75.8563 17.258ZM68.9531 11.5778C67.9384 11.5778 67.0959 11.8767 66.4255 12.4747C65.7551 13.0726 65.3384 13.8698 65.1753 14.8663H72.6765C72.5315 13.8698 72.1329 13.0726 71.4806 12.4747C70.8284 11.8767 69.9859 11.5778 68.9531 11.5778Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M90.448 15.274V23.3731H87.2138V14.9207C87.2138 12.7827 86.326 11.7137 84.5503 11.7137C83.59 11.7137 82.82 12.0217 82.2402 12.6377C81.6785 13.2538 81.3977 14.0963 81.3977 15.1653V23.3731H78.1635V9.26764H81.1531V11.1429C81.4973 10.5088 82.0228 9.98333 82.7294 9.5666C83.436 9.14987 84.3148 8.94151 85.3657 8.94151C87.3225 8.94151 88.7358 9.68438 89.6055 11.1701C90.8013 9.68438 92.3958 8.94151 94.3888 8.94151C96.0376 8.94151 97.3059 9.45789 98.1937 10.4907C99.0816 11.5053 99.5255 12.8461 99.5255 14.513V23.3731H96.2913V14.9207C96.2913 12.7827 95.4035 11.7137 93.6278 11.7137C92.6494 11.7137 91.8703 12.0308 91.2905 12.6649C90.7288 13.2809 90.448 14.1506 90.448 15.274Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M115.61 17.258H104.874C105.001 18.3814 105.409 19.2782 106.097 19.9486C106.786 20.6009 107.692 20.927 108.815 20.927C109.558 20.927 110.228 20.7459 110.826 20.3835C111.424 20.0211 111.85 19.5319 112.104 18.9159H115.365C114.93 20.3472 114.115 21.5068 112.919 22.3947C111.741 23.2644 110.346 23.6992 108.734 23.6992C106.632 23.6992 104.929 23.0016 103.624 21.6065C102.338 20.2114 101.694 18.4448 101.694 16.3068C101.694 14.2231 102.347 12.4747 103.651 11.0614C104.956 9.64814 106.641 8.94151 108.706 8.94151C110.772 8.94151 112.439 9.63908 113.707 11.0342C114.994 12.4112 115.637 14.1506 115.637 16.2524L115.61 17.258ZM108.706 11.5778C107.692 11.5778 106.849 11.8767 106.179 12.4747C105.508 13.0726 105.092 13.8698 104.929 14.8663H112.43C112.285 13.8698 111.886 13.0726 111.234 12.4747C110.582 11.8767 109.739 11.5778 108.706 11.5778Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M120.906 9.26764V11.1429C121.232 10.5269 121.767 10.0105 122.51 9.59378C123.271 9.15893 124.186 8.94151 125.255 8.94151C126.922 8.94151 128.208 9.44883 129.114 10.4635C130.038 11.4781 130.5 12.828 130.5 14.513V23.3731H127.266V14.9207C127.266 13.9242 127.03 13.1451 126.559 12.5834C126.106 12.0036 125.409 11.7137 124.467 11.7137C123.434 11.7137 122.619 12.0217 122.021 12.6377C121.441 13.2538 121.151 14.1053 121.151 15.1925V23.3731H117.917V9.26764H120.906Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M139.946 20.4922V23.2915C139.547 23.4003 138.985 23.4546 138.261 23.4546C135.507 23.4546 134.13 22.0685 134.13 19.2964V11.8496H131.982V9.26764H134.13V5.5986H137.364V9.26764H140V11.8496H137.364V18.9702C137.364 20.0755 137.889 20.6281 138.94 20.6281L139.946 20.4922Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M148.304 20.864C146.768 19.184 146 17.056 146 14.48C146 11.904 146.768 9.784 148.304 8.12C149.856 6.44 151.896 5.6 154.424 5.6C156.504 5.6 158.264 6.176 159.704 7.328C161.144 8.48 162.064 10.024 162.464 11.96H160.616C160.28 10.52 159.552 9.376 158.432 8.528C157.312 7.68 155.976 7.256 154.424 7.256C152.44 7.256 150.84 7.92 149.624 9.248C148.424 10.576 147.824 12.32 147.824 14.48C147.824 16.64 148.424 18.384 149.624 19.712C150.84 21.04 152.44 21.704 154.424 21.704C155.976 21.704 157.312 21.28 158.432 20.432C159.552 19.584 160.28 18.44 160.616 17H162.464C162.064 18.936 161.144 20.48 159.704 21.632C158.264 22.784 156.504 23.36 154.424 23.36C151.896 23.36 149.856 22.528 148.304 20.864Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M173.63 17.192C171.438 17.192 169.942 17.24 169.142 17.336C168.358 17.416 167.782 17.552 167.414 17.744C166.758 18.112 166.43 18.704 166.43 19.52C166.43 21.088 167.358 21.872 169.214 21.872C170.638 21.872 171.726 21.552 172.478 20.912C173.246 20.272 173.63 19.416 173.63 18.344V17.192ZM169.022 23.288C167.63 23.288 166.566 22.952 165.83 22.28C165.11 21.592 164.75 20.688 164.75 19.568C164.75 18.832 164.942 18.176 165.326 17.6C165.726 17.024 166.27 16.6 166.958 16.328C167.534 16.104 168.278 15.96 169.19 15.896C170.102 15.816 171.582 15.776 173.63 15.776V14.984C173.63 12.968 172.478 11.96 170.174 11.96C168.19 11.96 167.038 12.768 166.718 14.384H165.062C165.238 13.2 165.742 12.256 166.574 11.552C167.422 10.848 168.646 10.496 170.246 10.496C171.958 10.496 173.23 10.896 174.062 11.696C174.91 12.496 175.334 13.6 175.334 15.008V23H173.702V21.224C172.854 22.6 171.294 23.288 169.022 23.288Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M179.418 20.312V5H181.122V20.12C181.122 20.616 181.202 20.96 181.362 21.152C181.538 21.344 181.85 21.44 182.298 21.44L182.778 21.392V22.952C182.506 23 182.21 23.024 181.89 23.024C180.242 23.024 179.418 22.12 179.418 20.312Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M185.582 20.312V5H187.286V20.12C187.286 20.616 187.366 20.96 187.526 21.152C187.702 21.344 188.014 21.44 188.462 21.44L188.942 21.392V22.952C188.67 23 188.374 23.024 188.054 23.024C186.406 23.024 185.582 22.12 185.582 20.312Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M201 10C201 5.58172 204.582 2 209 2H252C256.418 2 260 5.58172 260 10V20C260 24.4183 256.418 28 252 28H209C204.582 28 201 24.4183 201 20V10Z"
|
||||
fill="#368BD6"
|
||||
/>
|
||||
<path
|
||||
d="M212.076 20H216.492C218.99 20 220.215 18.7269 220.215 17.0277C220.215 15.3764 219.043 14.407 217.882 14.3484V14.2418C218.947 13.9915 219.789 13.2457 219.789 11.9194C219.789 10.2947 218.617 9.09091 216.252 9.09091H212.076V20ZM214.052 18.3487V15.1527H216.231C217.451 15.1527 218.207 15.8984 218.207 16.8732C218.207 17.7415 217.61 18.3487 216.178 18.3487H214.052ZM214.052 13.7305V10.7209H216.05C217.211 10.7209 217.813 11.3335 217.813 12.1751C217.813 13.1339 217.035 13.7305 216.007 13.7305H214.052ZM221.934 20H229.072V18.3434H223.911V15.3658H228.662V13.7092H223.911V10.7475H229.03V9.09091H221.934V20ZM230.566 10.7475H233.938V20H235.898V10.7475H239.27V9.09091H230.566V10.7475ZM241.031 20L241.931 17.31H246.032L246.938 20H249.047L245.201 9.09091H242.762L238.921 20H241.031ZM242.464 15.7227L243.939 11.3281H244.024L245.5 15.7227H242.464Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -1872,17 +2026,17 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
exports[`should report correct error for 'Insufficient capacity' 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="page"
|
||||
class="_page_4be5c0"
|
||||
>
|
||||
<header
|
||||
class="header"
|
||||
class="_header_e4b327"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<a
|
||||
aria-label="Element Call Home"
|
||||
class="headerLogo"
|
||||
class="_headerLogo_e4b327"
|
||||
data-discover="true"
|
||||
href="/"
|
||||
>
|
||||
@@ -1964,20 +2118,20 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
class="_content_4be5c0"
|
||||
>
|
||||
<div
|
||||
class="error"
|
||||
class="_error_a69dc5"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_a69dc5"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
exports[`InCallView > rendering > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="inRoom"
|
||||
class="_inRoom_4e7ff8"
|
||||
>
|
||||
<header
|
||||
class="header header"
|
||||
class="_header_e4b327 _header_4e7ff8"
|
||||
>
|
||||
<div
|
||||
class="nav leftNav"
|
||||
class="_nav_e4b327 _leftNav_e4b327"
|
||||
>
|
||||
<div
|
||||
class="roomHeaderInfo"
|
||||
class="_roomHeaderInfo_e4b327"
|
||||
data-size="lg"
|
||||
>
|
||||
<span
|
||||
aria-label=""
|
||||
class="_avatar_va14e_8 roomAvatar _avatar-imageless_va14e_55"
|
||||
class="_avatar_va14e_8 _roomAvatar_e4b327 _avatar-imageless_va14e_55"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 56px;"
|
||||
/>
|
||||
<div
|
||||
class="nameLine"
|
||||
class="_nameLine_e4b327"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
@@ -35,7 +35,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_0_"
|
||||
class="lock"
|
||||
class="_lock_edc97d"
|
||||
data-encrypted="false"
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
@@ -50,7 +50,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="participantsLine"
|
||||
class="_participantsLine_e4b327"
|
||||
>
|
||||
<svg
|
||||
aria-label="Participants"
|
||||
@@ -80,21 +80,21 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="nav rightNav"
|
||||
class="_nav_e4b327 _rightNav_e4b327"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="scrollingGrid grid"
|
||||
class="_scrollingGrid_4e7ff8 _grid_b0d1cd"
|
||||
>
|
||||
<div
|
||||
class="layer"
|
||||
class="_layer_185815"
|
||||
>
|
||||
<div
|
||||
class="container slot"
|
||||
class="_container_185815 _slot_b0d1cd"
|
||||
data-id="1"
|
||||
>
|
||||
<div
|
||||
class="slot local slot"
|
||||
class="_slot_185815 _local_185815 _slot_b0d1cd"
|
||||
data-block-alignment="start"
|
||||
data-id="0"
|
||||
data-inline-alignment="end"
|
||||
@@ -103,22 +103,22 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="fixedGrid grid"
|
||||
class="_fixedGrid_4e7ff8 _grid_b0d1cd"
|
||||
style="inset-block-start: NaNpx;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="bg animate"
|
||||
class="_bg_2f5303 _animate_2f5303"
|
||||
data-state="closed"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="overlay"
|
||||
class="_overlay_eb6724"
|
||||
data-show="false"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 icon"
|
||||
class="_big-icon_1ssbv_8 _icon_eb6724"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
@@ -157,22 +157,22 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
Back to Speaker Mode
|
||||
</button>
|
||||
<div
|
||||
class="spacer"
|
||||
class="_spacer_eb6724"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="container"
|
||||
class="_container_8084b5"
|
||||
/>
|
||||
<div
|
||||
class="footer"
|
||||
class="_footer_4e7ff8 _footer_20b7b4"
|
||||
data-testid="footer-container"
|
||||
>
|
||||
<div
|
||||
class="settingsLogoContainer"
|
||||
class="_settingsLogoContainer_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_8_"
|
||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
||||
class="_icon-button_1215g_8 _settingsOnlyShowWide_20b7b4"
|
||||
data-kind="secondary"
|
||||
data-testid="settings-bottom-left"
|
||||
role="button"
|
||||
@@ -198,7 +198,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="logo"
|
||||
class="_logo_20b7b4"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -303,11 +303,11 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="buttons"
|
||||
class="_buttons_20b7b4"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_d_"
|
||||
class="_button_1nw83_8 settingsOnlyShowNarrow _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _settingsOnlyShowNarrow_20b7b4 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
data-testid="settings-bottom-center"
|
||||
@@ -382,7 +382,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby="_r_s_"
|
||||
class="_button_1nw83_8 raiseHand _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
class="_button_1nw83_8 _raiseHand_20b7b4 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
@@ -405,7 +405,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="_r_14_"
|
||||
class="_button_1nw83_8 endCall _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53 _destructive_1nw83_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-testid="incall_leave"
|
||||
@@ -428,12 +428,12 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</div>
|
||||
<fieldset
|
||||
aria-label="Layout"
|
||||
class="_toggle_13rnk_9 layout"
|
||||
class="_toggle_1t5ha_9 _layout_20b7b4"
|
||||
data-size="lg"
|
||||
>
|
||||
<input
|
||||
aria-labelledby="_r_19_"
|
||||
name="layoutMode"
|
||||
aria-labelledby="_r_1a_"
|
||||
name="_r_19_"
|
||||
type="radio"
|
||||
value="spotlight"
|
||||
/>
|
||||
@@ -446,13 +446,15 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 5h14v8h-5a1 1 0 0 0-1 1v5H5zm10 14v-4h4v4zM5 21h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2"
|
||||
clip-rule="evenodd"
|
||||
d="M20 6H4v12h16zM4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
aria-labelledby="_r_1e_"
|
||||
aria-labelledby="_r_1f_"
|
||||
checked=""
|
||||
name="layoutMode"
|
||||
name="_r_19_"
|
||||
type="radio"
|
||||
value="grid"
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user