Merge branch 'livekit' into header-subtitle

This commit is contained in:
Robin
2026-06-22 11:35:52 +02:00
14 changed files with 220 additions and 200 deletions

View File

@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
`;
module.exports = {
plugins: ["matrix-org", "rxjs", "jsdoc"],
plugins: ["matrix-org", "rxjs", "jsdoc", "element-call"],
extends: [
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
@@ -27,6 +27,7 @@ module.exports = {
node: true,
},
rules: {
"element-call/no-observablescope-leak": "error",
"jsdoc/no-types": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
@@ -92,6 +93,9 @@ module.exports = {
"**/test-**",
],
rules: {
// Tests often initialize an ObservableScope in an outer scope in
// beforeEach, which is not actually a problem
"element-call/no-observablescope-leak": "off",
"jsdoc/no-types": "off",
"jsdoc/empty-tags": "off",
"jsdoc/check-property-names": "off",

View File

@@ -0,0 +1,62 @@
/*
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 = ["bind", "end"];
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 ("through") scope?
scope.through.some(
({ identifier }) => identifier.name === node.name,
) &&
// Exclude calls to "safe" ObservableScope methods
node.parent?.type === "MemberExpression" &&
node.parent.object === node &&
node.parent.property.type === "Identifier" &&
!safeScopeMethods.includes(node.parent.property.name)
) {
// Verify that the variable is actually of type ObservableScope
// (expensive, so we check this last)
const services = ESLintUtils.getParserServices(context);
const type = services.getTypeAtLocation(node);
if (type.symbol?.name === "ObservableScope")
// This ObservableScope method call may be causing resource leaks.
context.report({
messageId: "scopeLeak",
loc: node.loc,
node,
});
}
},
};
},
});
export default rule;

5
eslint/index.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
rules: {
"no-observablescope-leak": require("./NoObservableScopeLeak").default,
},
};

4
eslint/package.json Normal file
View File

@@ -0,0 +1,4 @@
{
"name": "eslint-plugin-element-call",
"version": "0.0.0"
}

View File

@@ -11,7 +11,7 @@ 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:

View File

@@ -65,6 +65,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.1",
"@types/eslint": "^9.6.1",
"@types/grecaptcha": "^3.0.9",
"@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12",
@@ -76,6 +77,7 @@
"@types/sdp-transform": "^2.4.5",
"@typescript-eslint/eslint-plugin": "^8.31.0",
"@typescript-eslint/parser": "^8.31.0",
"@typescript-eslint/utils": "^8.61.0",
"@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^10.0.0",
"@vector-im/compound-web": "^9.3.0",
@@ -89,6 +91,7 @@
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-deprecate": "^0.9.0",
"eslint-plugin-element-call": "link:eslint",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^61.5.0",
"eslint-plugin-jsx-a11y": "^6.5.1",

187
pnpm-lock.yaml generated
View File

@@ -7,14 +7,14 @@ settings:
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
minimatch: ^10.2.3
glob: ^10.5.0
qs: ^6.14.1
js-yaml: ^4.1.1
esbuild: ^0.28.0
flatted: ^3.4.2
undici: ^6.24.0
flatted: ^3.4.2
importers:
@@ -95,6 +95,9 @@ importers:
'@testing-library/user-event':
specifier: ^14.5.1
version: 14.6.1(@testing-library/dom@10.4.1)
'@types/eslint':
specifier: ^9.6.1
version: 9.6.1
'@types/grecaptcha':
specifier: ^3.0.9
version: 3.0.9
@@ -128,6 +131,9 @@ importers:
'@typescript-eslint/parser':
specifier: ^8.31.0
version: 8.60.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils':
specifier: ^8.61.0
version: 8.61.0(eslint@8.57.1)(typescript@5.9.3)
'@use-gesture/react':
specifier: ^10.2.11
version: 10.3.1(react@19.2.6)
@@ -167,6 +173,9 @@ importers:
eslint-plugin-deprecate:
specifier: ^0.9.0
version: 0.9.0(eslint@8.57.1)
eslint-plugin-element-call:
specifier: link:eslint
version: link:eslint
eslint-plugin-import:
specifier: ^2.26.0
version: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)
@@ -3322,6 +3331,9 @@ packages:
'@types/dom-webcodecs@0.1.18':
resolution: {integrity: sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg==}
'@types/eslint@9.6.1':
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -3417,20 +3429,14 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.58.2':
resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.60.0':
resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.60.1':
resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==}
'@typescript-eslint/project-service@8.61.0':
resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -3445,36 +3451,26 @@ packages:
resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@typescript-eslint/scope-manager@8.58.2':
resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.60.0':
resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.60.1':
resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==}
'@typescript-eslint/scope-manager@8.61.0':
resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.61.1':
resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.58.2':
resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/tsconfig-utils@8.60.0':
resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/tsconfig-utils@8.60.1':
resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==}
'@typescript-eslint/tsconfig-utils@8.61.0':
resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -3496,10 +3492,6 @@ packages:
resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@typescript-eslint/types@8.58.2':
resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.60.0':
resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3508,6 +3500,10 @@ packages:
resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.61.0':
resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.61.1':
resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3521,20 +3517,14 @@ packages:
typescript:
optional: true
'@typescript-eslint/typescript-estree@8.58.2':
resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/typescript-estree@8.60.0':
resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/typescript-estree@8.60.1':
resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==}
'@typescript-eslint/typescript-estree@8.61.0':
resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -3551,13 +3541,6 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
'@typescript-eslint/utils@8.58.2':
resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.60.0':
resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3565,8 +3548,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.60.1':
resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==}
'@typescript-eslint/utils@8.61.0':
resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -3583,16 +3566,12 @@ packages:
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@typescript-eslint/visitor-keys@8.58.2':
resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.60.0':
resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.60.1':
resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==}
'@typescript-eslint/visitor-keys@8.61.0':
resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.61.1':
@@ -5524,7 +5503,7 @@ packages:
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7:
resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7}
resolution: {gitHosted: true, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7}
version: 41.7.0
engines: {node: '>=22.0.0'}
@@ -9641,7 +9620,7 @@ snapshots:
'@stylistic/eslint-plugin@3.1.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/utils': 8.58.2(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.61.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-visitor-keys: 4.2.1
espree: 10.4.0
@@ -9860,6 +9839,11 @@ snapshots:
'@types/dom-webcodecs@0.1.18': {}
'@types/eslint@9.6.1':
dependencies:
'@types/estree': 1.0.9
'@types/json-schema': 7.0.15
'@types/estree@1.0.8': {}
'@types/estree@1.0.9': {}
@@ -9961,28 +9945,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.58.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3)
'@typescript-eslint/types': 8.58.2
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.60.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3)
'@typescript-eslint/types': 8.60.0
'@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3)
'@typescript-eslint/types': 8.61.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.60.1(typescript@5.9.3)':
'@typescript-eslint/project-service@8.61.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3)
'@typescript-eslint/types': 8.61.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
@@ -10002,35 +9977,26 @@ snapshots:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
'@typescript-eslint/scope-manager@8.58.2':
dependencies:
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/visitor-keys': 8.58.2
'@typescript-eslint/scope-manager@8.60.0':
dependencies:
'@typescript-eslint/types': 8.60.0
'@typescript-eslint/visitor-keys': 8.60.0
'@typescript-eslint/scope-manager@8.60.1':
'@typescript-eslint/scope-manager@8.61.0':
dependencies:
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/visitor-keys': 8.60.1
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/visitor-keys': 8.61.0
'@typescript-eslint/scope-manager@8.61.1':
dependencies:
'@typescript-eslint/types': 8.61.1
'@typescript-eslint/visitor-keys': 8.61.1
'@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.61.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
@@ -10052,12 +10018,12 @@ snapshots:
'@typescript-eslint/types@5.62.0': {}
'@typescript-eslint/types@8.58.2': {}
'@typescript-eslint/types@8.60.0': {}
'@typescript-eslint/types@8.60.1': {}
'@typescript-eslint/types@8.61.0': {}
'@typescript-eslint/types@8.61.1': {}
'@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)':
@@ -10074,21 +10040,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.58.2(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3)
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/visitor-keys': 8.58.2
debug: 4.4.3
minimatch: 10.2.5
semver: 7.8.1
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.60.0(typescript@5.9.3)
@@ -10104,15 +10055,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.60.1(typescript@5.9.3)':
'@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.60.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/visitor-keys': 8.60.1
'@typescript-eslint/project-service': 8.61.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3)
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/visitor-keys': 8.61.0
debug: 4.4.3
minimatch: 10.2.5
semver: 7.8.1
semver: 7.8.4
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
@@ -10149,17 +10100,6 @@ snapshots:
- supports-color
- typescript
'@typescript-eslint/utils@8.58.2(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.60.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
@@ -10171,12 +10111,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.60.1(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/utils@8.61.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.60.1
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.61.0
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
@@ -10198,19 +10138,14 @@ snapshots:
'@typescript-eslint/types': 5.62.0
eslint-visitor-keys: 3.4.3
'@typescript-eslint/visitor-keys@8.58.2':
dependencies:
'@typescript-eslint/types': 8.58.2
eslint-visitor-keys: 5.0.1
'@typescript-eslint/visitor-keys@8.60.0':
dependencies:
'@typescript-eslint/types': 8.60.0
eslint-visitor-keys: 5.0.1
'@typescript-eslint/visitor-keys@8.60.1':
'@typescript-eslint/visitor-keys@8.61.0':
dependencies:
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/types': 8.61.0
eslint-visitor-keys: 5.0.1
'@typescript-eslint/visitor-keys@8.61.1':
@@ -11406,7 +11341,7 @@ snapshots:
eslint-plugin-storybook@10.4.1(eslint@8.57.1)(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.15)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3):
dependencies:
'@typescript-eslint/utils': 8.60.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.61.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
storybook: 10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.15)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
transitivePeerDependencies:

View File

@@ -76,9 +76,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 +86,7 @@ interface MatrixRTCSdk {
}[]
>;
/**
* flattened local members
* flattened local member
*/
localMember$: Behavior<{
connection: Connection | null;
@@ -338,8 +338,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([

View File

@@ -161,6 +161,10 @@ export function createCallNotificationLifecycle$({
recipient,
outcome$: race(timeout$, accept$, decline$).pipe(
take(1),
// Make this observable 'hot' to avoid running multiple timers. This
// is not actually a resource leak since there will be at most one
// active ring attempt at any given time.
// eslint-disable-next-line element-call/no-observablescope-leak
scope.share,
),
});

View File

@@ -125,10 +125,9 @@ import {
createConnectionManager$,
} from "./remoteMembers/ConnectionManager.ts";
import {
createMatrixLivekitMembers$,
createRemoteMatrixLivekitMembers$,
type LocalMatrixLivekitMember,
type RemoteMatrixLivekitMember,
type MatrixLivekitMember,
} from "./remoteMembers/MatrixLivekitMembers.ts";
import {
type AutoLeaveReason,
@@ -300,7 +299,7 @@ export interface CallViewModel {
/** Participants sorted by livekit room so they can be used in the audio rendering */
livekitRoomItems$: Behavior<LivekitRoomItem[]>;
/** use the layout instead, this is just for the sdk export. */
matrixLivekitMembers$: Behavior<RemoteMatrixLivekitMember[]>;
remoteMatrixLivekitMembers$: Behavior<RemoteMatrixLivekitMember[]>;
localMatrixLivekitMember$: Behavior<LocalMatrixLivekitMember | null>;
/** List of participants raising their hand */
handsRaised$: Behavior<Record<string, RaisedHandInfo>>;
@@ -527,13 +526,15 @@ export function createCallViewModel$(
ownMembershipIdentity,
});
const matrixLivekitMembers$: Behavior<Epoch<RemoteMatrixLivekitMember[]>> =
createMatrixLivekitMembers$({
scope: scope,
membershipsWithTransport$:
membershipsAndTransports.membershipsWithTransport$,
connectionManager: connectionManager,
});
const remoteMatrixLivekitMembers$: Behavior<
Epoch<RemoteMatrixLivekitMember[]>
> = createRemoteMatrixLivekitMembers$({
scope: scope,
membershipsWithTransport$:
membershipsAndTransports.membershipsWithTransport$,
connectionManager: connectionManager,
localUser: { userId, deviceId },
});
const connectOptions$ = scope.behavior(
matrixRTCMode$.pipe(
@@ -610,6 +611,13 @@ export function createCallViewModel$(
),
);
const matrixLivekitMembers$ = scope.behavior(
combineLatest(
[localMatrixLivekitMember$, remoteMatrixLivekitMembers$],
(local, remote) => [...(local === null ? [] : [local]), ...remote.value],
),
);
// ------------------------------------------------------------------------
// matrixMemberMetadataStore
@@ -639,7 +647,7 @@ export function createCallViewModel$(
connectionManager.connectionManagerData$.pipe(map((d) => d.value)),
);
const livekitRoomItems$ = scope.behavior(
matrixLivekitMembers$.pipe(
remoteMatrixLivekitMembers$.pipe(
switchMap((members) => {
const a$ = combineLatest(
members.value.map((member) =>
@@ -705,43 +713,20 @@ export function createCallViewModel$(
* List of user media (camera feeds) that we want tiles for.
*/
const userMedia$ = scope.behavior<WrappedUserMediaViewModel[]>(
combineLatest([
localMatrixLivekitMember$,
matrixLivekitMembers$,
duplicateTiles.value$,
]).pipe(
combineLatest([matrixLivekitMembers$, duplicateTiles.value$]).pipe(
// Generate a collection of user media from the list of expected (whether
// present or missing) LiveKit participants.
generateItems(
"CallViewModel userMedia$",
function* ([
localMatrixLivekitMember,
matrixLivekitMembers,
duplicateTiles,
]) {
const computeMediaId = (m: MatrixLivekitMember): string =>
`${m.userId}:${m.membership$.value.deviceId}`;
const localUserMediaId = localMatrixLivekitMember
? computeMediaId(localMatrixLivekitMember)
: undefined;
const localAsArray = localMatrixLivekitMember
? [localMatrixLivekitMember]
: [];
const remoteWithoutLocal = matrixLivekitMembers.value.filter(
(m) => computeMediaId(m) !== localUserMediaId,
);
const allMatrixLivekitMembers = [
...localAsArray,
...remoteWithoutLocal,
];
for (const matrixLivekitMember of allMatrixLivekitMembers) {
const { userId, participant, connection$, membership$ } =
matrixLivekitMember;
const rtcId = membership$.value.rtcBackendIdentity; // rtcBackendIdentity
const mediaId = computeMediaId(matrixLivekitMember);
function* ([members, duplicateTiles]) {
for (const {
userId,
participant,
connection$,
membership$,
} of members) {
const rtcId = membership$.value.rtcBackendIdentity;
const mediaId = `${userId}:${membership$.value.deviceId}`;
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
yield {
keys: [dup, mediaId, userId, participant, connection$, rtcId],
@@ -859,7 +844,7 @@ export function createCallViewModel$(
* multiple devices.
*/
const participantCount$ = scope.behavior(
matrixLivekitMembers$.pipe(map((ms) => ms.value.length)),
matrixLivekitMembers$.pipe(map((ms) => ms.length)),
);
const leaveSoundEffect$ = userMedia$.pipe(
@@ -1760,8 +1745,8 @@ export function createCallViewModel$(
setGridMode: setGridMode,
layout$: layout$,
localMatrixLivekitMember$,
matrixLivekitMembers$: scope.behavior(
matrixLivekitMembers$.pipe(
remoteMatrixLivekitMembers$: scope.behavior(
remoteMatrixLivekitMembers$.pipe(
map((members) => members.value),
tap((v) => {
const listForLogs = v

View File

@@ -15,7 +15,7 @@ import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs";
import { type IConnectionManager } from "./ConnectionManager.ts";
import {
type RemoteMatrixLivekitMember,
createMatrixLivekitMembers$,
createRemoteMatrixLivekitMembers$,
} from "./MatrixLivekitMembers.ts";
import {
Epoch,
@@ -31,6 +31,7 @@ import {
} from "../../../utils/test.ts";
import { type Connection } from "./Connection.ts";
import { constant } from "../../Behavior.ts";
import { localRtcMember } from "../../../utils/test-fixtures.ts";
let testScope: ObservableScope;
@@ -88,16 +89,17 @@ test("should signal participant not yet connected to livekit", async () => {
mockConnectionManagerData$,
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
const remoteMatrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
localUser: localRtcMember,
});
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
expect(remoteMatrixLivekitMembers$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
expect(data.length).toEqual(1);
expect(data[0].membership$.value).toBe(bobMembership);
@@ -157,16 +159,17 @@ test("should signal participant on a connection that is publishing", async () =>
constant(dataWithPublisher),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
const remoteMatrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
localUser: localRtcMember,
});
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
expect(remoteMatrixLivekitMembers$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
expect(data.length).toEqual(1);
expect(data[0].membership$.value).toBe(bobMembership);
@@ -197,15 +200,16 @@ test("should signal participant on a connection that is not publishing", async (
constant(dataWithPublisher),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
const remoteMatrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
localUser: localRtcMember,
});
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
expect(remoteMatrixLivekitMembers$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
expect(data.length).toEqual(1);
expect(data[0].membership$.value).toBe(bobMembership);
@@ -245,15 +249,16 @@ describe("Publication edge case", () => {
constant(connectionWithPublisher),
);
const matrixLivekitMembers$ = createMatrixLivekitMembers$({
const remoteMatrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
localUser: localRtcMember,
});
await flushPromises();
expect(matrixLivekitMembers$.value.value).toSatisfy(
expect(remoteMatrixLivekitMembers$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
expect(data.length).toEqual(2);
expect(data[0].membership$.value).toBe(bobMembership);
@@ -303,16 +308,17 @@ test("bob is publishing in the wrong connection", async () => {
connectionsWithPublisher$,
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
const remoteMatrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
localUser: localRtcMember,
});
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
expect(remoteMatrixLivekitMembers$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
expect(data.length).toEqual(2);
expect(data[0].membership$.value).toBe(bobMembership);

View File

@@ -62,7 +62,9 @@ interface Props {
Epoch<{ membership: CallMembership; transport?: LivekitTransportConfig }[]>
>;
connectionManager: IConnectionManager;
localUser: { deviceId: string; userId: string };
}
/**
* Combines MatrixRTC and Livekit worlds.
*
@@ -73,13 +75,14 @@ interface Props {
* - out (via public Observable):
* - `remoteMatrixLivekitMember` an observable of MatrixLivekitMember[] to track the remote members and associated livekit data.
*/
export function createMatrixLivekitMembers$({
export function createRemoteMatrixLivekitMembers$({
scope,
membershipsWithTransport$,
connectionManager,
localUser,
}: Props): Behavior<Epoch<RemoteMatrixLivekitMember[]>> {
/**
* Stream of all the call members and their associated livekit data (if available).
* Behavior of all the remote call members and their associated livekit data (if available).
*/
return scope.behavior(
combineLatest([
@@ -91,12 +94,19 @@ export function createMatrixLivekitMembers$({
),
map(([ms, data]) => new Epoch([ms.value, data.value] as const, ms.epoch)),
generateItemsWithEpoch(
"MatrixLivekitMembers",
"RemoteMatrixLivekitMembers",
// Generator function.
// creates an array of `{key, data}[]`
// Each change in the keys (new key) will result in a call to the factory function.
function* ([membershipsWithTransport, managerData]) {
for (const { membership, transport } of membershipsWithTransport) {
// Exclude the local membership
if (
membership.userId === localUser.userId &&
membership.deviceId === localUser.deviceId
)
continue;
const participants = transport
? managerData.getParticipantsForTransport(transport)
: [];

View File

@@ -29,13 +29,13 @@ import {
import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import {
areLivekitTransportsEqual,
createMatrixLivekitMembers$,
createRemoteMatrixLivekitMembers$,
type RemoteMatrixLivekitMember,
} from "./MatrixLivekitMembers.ts";
import { createConnectionManager$ } from "./ConnectionManager.ts";
import { membershipsAndTransports$ } from "../../SessionBehaviors.ts";
import { constant } from "../../Behavior.ts";
import { testJWTToken } from "../../../utils/test-fixtures.ts";
import { localRtcMember, testJWTToken } from "../../../utils/test-fixtures.ts";
// Test the integration of ConnectionManager and MatrixLivekitMerger
@@ -130,14 +130,15 @@ test("bob, carl, then bob joining no tracks yet", () => {
ownMembershipIdentity: ownMemberMock,
});
const matrixLivekitMembers$ = createMatrixLivekitMembers$({
const remoteMatrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$:
membershipsAndTransports.membershipsWithTransport$,
connectionManager,
localUser: localRtcMember,
});
expectObservable(matrixLivekitMembers$).toBe(vMarble, {
expectObservable(remoteMatrixLivekitMembers$).toBe(vMarble, {
a: expect.toSatisfy((e: Epoch<RemoteMatrixLivekitMember[]>) => {
const items = e.value;
expect(items.length).toBe(1);

View File

@@ -92,6 +92,7 @@ export function createMemberMedia(
}: MemberMediaInputs,
): BaseMemberMediaViewModel {
const trackBehavior$ = (
scope: ObservableScope,
source: Track.Source,
): Behavior<TrackReference | undefined> =>
scope.behavior(
@@ -102,8 +103,8 @@ export function createMemberMedia(
),
);
const audio$ = trackBehavior$(audioSource);
const video$ = trackBehavior$(videoSource);
const audio$ = trackBehavior$(scope, audioSource);
const video$ = trackBehavior$(scope, videoSource);
return {
...createBaseMedia(inputs),