mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-21 19:09:20 +00:00
Merge branch 'livekit' into fkwp/change_video_codec
This commit is contained in:
@@ -28,8 +28,6 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
||||||
"jsx-a11y/media-has-caption": "off",
|
"jsx-a11y/media-has-caption": "off",
|
||||||
// We should use the js-sdk logger, never console directly.
|
|
||||||
"no-console": ["error"],
|
|
||||||
"react/display-name": "error",
|
"react/display-name": "error",
|
||||||
// Encourage proper usage of Promises:
|
// Encourage proper usage of Promises:
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
@@ -46,6 +44,15 @@ module.exports = {
|
|||||||
"rxjs/no-exposed-subjects": "error",
|
"rxjs/no-exposed-subjects": "error",
|
||||||
"rxjs/finnish": ["error", { names: { "^this$": false } }],
|
"rxjs/finnish": ["error", { names: { "^this$": false } }],
|
||||||
},
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["src/*/**"],
|
||||||
|
rules: {
|
||||||
|
// In application code we should use the js-sdk logger, never console directly.
|
||||||
|
"no-console": ["error"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: "detect",
|
version: "detect",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
|
|||||||
|
|
||||||
| Name | Values | Required for widget | Required for SPA | Description |
|
| Name | Values | Required for widget | Required for SPA | Description |
|
||||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `intent` | `start_call`, `join_existing`, `start_call_dm`, `join_existing_dm. | No, defaults to `start_call` | No, defaults to `start_call` | The intent is a special url parameter that defines the defaults for all the other parameters. In most cases it should be enough to only set the intent to setup element-call. |
|
||||||
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
||||||
| `analyticsID` (deprecated: use `posthogUserId` instead) | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
| `analyticsID` (deprecated: use `posthogUserId` instead) | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
||||||
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
||||||
@@ -59,7 +60,6 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
|
|||||||
| `header` | `none`, `standard` or `app_bar` | No, defaults to `standard` | No, defaults to `standard` | The style of headers to show. `standard` is the default arrangement, `none` hides the header entirely, and `app_bar` produces a header with a back button like you might see in mobile apps. The callback for the back button is `window.controls.onBackButtonPressed`. |
|
| `header` | `none`, `standard` or `app_bar` | No, defaults to `standard` | No, defaults to `standard` | The style of headers to show. `standard` is the default arrangement, `none` hides the header entirely, and `app_bar` produces a header with a back button like you might see in mobile apps. The callback for the back button is `window.controls.onBackButtonPressed`. |
|
||||||
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
||||||
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
||||||
| `intent` | `start_call` or `join_existing` | No, defaults to `start_call` | No, defaults to `start_call` | The intent of the user with respect to the call. e.g. if they clicked a Start Call button, this would be `start_call`. If it was a Join Call button, it would be `join_existing`. |
|
|
||||||
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
||||||
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
||||||
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default {
|
import type { UserConfig } from "i18next-parser";
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
keySeparator: ".",
|
keySeparator: ".",
|
||||||
namespaceSeparator: false,
|
namespaceSeparator: false,
|
||||||
contextSeparator: "|",
|
contextSeparator: "|",
|
||||||
@@ -26,3 +28,5 @@ export default {
|
|||||||
input: ["src/**/*.{ts,tsx}"],
|
input: ["src/**/*.{ts,tsx}"],
|
||||||
sort: true,
|
sort: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
11
knip.ts
11
knip.ts
@@ -1,8 +1,15 @@
|
|||||||
import { KnipConfig } from "knip";
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type KnipConfig } from "knip";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
vite: {
|
vite: {
|
||||||
config: ["vite.config.js", "vite-embedded.config.js"],
|
config: ["vite.config.ts", "vite-embedded.config.ts"],
|
||||||
},
|
},
|
||||||
entry: ["src/main.tsx", "i18next-parser.config.ts"],
|
entry: ["src/main.tsx", "i18next-parser.config.ts"],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
|
|||||||
75
playwright/restricted-sfu.spec.ts
Normal file
75
playwright/restricted-sfu.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { sleep } from "matrix-js-sdk/lib/utils.js";
|
||||||
|
|
||||||
|
test("Should request JWT token before starting the call", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
let sfGetTimestamp = 0;
|
||||||
|
let sendStateEventTimestamp = 0;
|
||||||
|
await page.route(
|
||||||
|
"**/matrix-rtc.m.localhost/livekit/jwt/sfu/get",
|
||||||
|
async (route) => {
|
||||||
|
await sleep(2000); // Simulate very slow request
|
||||||
|
await route.continue();
|
||||||
|
sfGetTimestamp = Date.now();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.route(
|
||||||
|
"**/state/org.matrix.msc3401.call.member/**",
|
||||||
|
async (route) => {
|
||||||
|
await route.continue();
|
||||||
|
sendStateEventTimestamp = Date.now();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByTestId("home_callName").click();
|
||||||
|
await page.getByTestId("home_callName").fill("HelloCall");
|
||||||
|
await page.getByTestId("home_displayName").click();
|
||||||
|
await page.getByTestId("home_displayName").fill("John Doe");
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
|
||||||
|
// Join the call
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
await page.waitForTimeout(4000);
|
||||||
|
// Ensure that the call is connected
|
||||||
|
await page
|
||||||
|
.locator("div")
|
||||||
|
.filter({ hasText: /^HelloCall$/ })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
expect(sfGetTimestamp).toBeGreaterThan(0);
|
||||||
|
expect(sendStateEventTimestamp).toBeGreaterThan(0);
|
||||||
|
expect(sfGetTimestamp).toBeLessThan(sendStateEventTimestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Error when pre-warming the focus are caught by the ErrorBoundary", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.route("**/openid/request_token", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 418, // Simulate an error not retryable
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("home_callName").click();
|
||||||
|
await page.getByTestId("home_callName").fill("HelloCall");
|
||||||
|
await page.getByTestId("home_displayName").click();
|
||||||
|
await page.getByTestId("home_displayName").fill("John Doe");
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
|
||||||
|
// Join the call
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
|
||||||
|
// Should fail
|
||||||
|
await expect(page.getByText("Something went wrong")).toBeVisible();
|
||||||
|
});
|
||||||
@@ -100,5 +100,5 @@ test("When creator left, avoid reconnect to the same SFU", async ({
|
|||||||
// https://github.com/element-hq/element-call/issues/3344
|
// https://github.com/element-hq/element-call/issues/3344
|
||||||
// The app used to request a new jwt token then to reconnect to the SFU
|
// The app used to request a new jwt token then to reconnect to the SFU
|
||||||
expect(wsConnectionCount).toBe(1);
|
expect(wsConnectionCount).toBe(1);
|
||||||
expect(sfuGetCallCount).toBe(1);
|
expect(sfuGetCallCount).toBe(2 /* the first one is for the warmup */);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ interface RoomIdentifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum UserIntent {
|
export enum UserIntent {
|
||||||
// TODO: add DM vs room call
|
|
||||||
StartNewCall = "start_call",
|
StartNewCall = "start_call",
|
||||||
JoinExistingCall = "join_existing",
|
JoinExistingCall = "join_existing",
|
||||||
|
StartNewCallDM = "start_call_dm",
|
||||||
|
JoinExistingCallDM = "join_existing_dm",
|
||||||
Unknown = "unknown",
|
Unknown = "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +210,12 @@ export interface UrlConfiguration {
|
|||||||
* Whether and what type of notification EC should send, when the user joins the call.
|
* Whether and what type of notification EC should send, when the user joins the call.
|
||||||
*/
|
*/
|
||||||
sendNotificationType?: RTCNotificationType;
|
sendNotificationType?: RTCNotificationType;
|
||||||
|
/**
|
||||||
|
* Whether the app should automatically leave the call when there
|
||||||
|
* is no one left in the call.
|
||||||
|
* This is one part to make the call matrixRTC session behave like a telephone call.
|
||||||
|
*/
|
||||||
|
autoLeaveWhenOthersLeft: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you need to add a new flag to this interface, prefer a name that describes
|
// If you need to add a new flag to this interface, prefer a name that describes
|
||||||
@@ -276,10 +283,16 @@ class ParamParser {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the flag exists and is not "false".
|
||||||
|
*/
|
||||||
public getFlagParam(name: string, defaultValue = false): boolean {
|
public getFlagParam(name: string, defaultValue = false): boolean {
|
||||||
const param = this.getParam(name);
|
const param = this.getParam(name);
|
||||||
return param === null ? defaultValue : param !== "false";
|
return param === null ? defaultValue : param !== "false";
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Returns the value of the flag if it exists, or undefined if it does not.
|
||||||
|
*/
|
||||||
public getFlag(name: string): boolean | undefined {
|
public getFlag(name: string): boolean | undefined {
|
||||||
const param = this.getParam(name);
|
const param = this.getParam(name);
|
||||||
return param !== null ? param !== "false" : undefined;
|
return param !== null ? param !== "false" : undefined;
|
||||||
@@ -333,6 +346,7 @@ export const getUrlParams = (
|
|||||||
skipLobby: true,
|
skipLobby: true,
|
||||||
returnToLobby: false,
|
returnToLobby: false,
|
||||||
sendNotificationType: "notification" as RTCNotificationType,
|
sendNotificationType: "notification" as RTCNotificationType,
|
||||||
|
autoLeaveWhenOthersLeft: false,
|
||||||
};
|
};
|
||||||
switch (intent) {
|
switch (intent) {
|
||||||
case UserIntent.StartNewCall:
|
case UserIntent.StartNewCall:
|
||||||
@@ -347,6 +361,20 @@ export const getUrlParams = (
|
|||||||
skipLobby: false,
|
skipLobby: false,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case UserIntent.StartNewCallDM:
|
||||||
|
intentPreset = {
|
||||||
|
...inAppDefault,
|
||||||
|
skipLobby: true,
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case UserIntent.JoinExistingCallDM:
|
||||||
|
intentPreset = {
|
||||||
|
...inAppDefault,
|
||||||
|
skipLobby: true,
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
};
|
||||||
|
break;
|
||||||
// Non widget usecase defaults
|
// Non widget usecase defaults
|
||||||
default:
|
default:
|
||||||
intentPreset = {
|
intentPreset = {
|
||||||
@@ -362,6 +390,7 @@ export const getUrlParams = (
|
|||||||
skipLobby: false,
|
skipLobby: false,
|
||||||
returnToLobby: false,
|
returnToLobby: false,
|
||||||
sendNotificationType: undefined,
|
sendNotificationType: undefined,
|
||||||
|
autoLeaveWhenOthersLeft: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,12 +442,13 @@ export const getUrlParams = (
|
|||||||
"ring",
|
"ring",
|
||||||
"notification",
|
"notification",
|
||||||
]),
|
]),
|
||||||
|
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...properties,
|
...properties,
|
||||||
...intentPreset,
|
...intentPreset,
|
||||||
...pickBy(configuration, (v) => v !== undefined),
|
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
6
src/icons/FullScreenMaximise.svg
Normal file
6
src/icons/FullScreenMaximise.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 14C4.55228 14 5 14.4477 5 15V19H9C9.55228 19 10 19.4477 10 20C10 20.5523 9.55228 21 9 21H3V15C3 14.4477 3.44772 14 4 14Z"/>
|
||||||
|
<path d="M20 14C20.5523 14 21 14.4477 21 15V21H15C14.4477 21 14 20.5523 14 20C14 19.4477 14.4477 19 15 19H19V15C19 14.4477 19.4477 14 20 14Z" />
|
||||||
|
<path d="M9 3C9.55228 3 10 3.44772 10 4C10 4.55228 9.55228 5 9 5H5V9C5 9.55228 4.55228 10 4 10C3.44772 10 3 9.55228 3 9V3H9Z" />
|
||||||
|
<path d="M21 9C21 9.55228 20.5523 10 20 10C19.4477 10 19 9.55228 19 9V5H15C14.4477 5 14 4.55228 14 4C14 3.44772 14.4477 3 15 3H21V9Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 658 B |
6
src/icons/FullScreenMinimise.svg
Normal file
6
src/icons/FullScreenMinimise.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 20C10 20.5523 9.55228 21 9 21C8.44772 21 8 20.5523 8 20V16H4C3.44772 16 3 15.5523 3 15C3 14.4477 3.44772 14 4 14H10V20Z" />
|
||||||
|
<path d="M20 14C20.5523 14 21 14.4477 21 15C21 15.5523 20.5523 16 20 16H16V20C16 20.5523 15.5523 21 15 21C14.4477 21 14 20.5523 14 20V14H20Z" />
|
||||||
|
<path d="M9 3C9.55228 3 10 3.44772 10 4V10H4C3.44772 10 3 9.55228 3 9C3 8.44772 3.44772 8 4 8H8V4C8 3.44772 8.44772 3 9 3Z" />
|
||||||
|
<path d="M15 3C15.5523 3 16 3.44772 16 4V8H20C20.5523 8 21 8.44772 21 9C21 9.55228 20.5523 10 20 10H14V4C14 3.44772 14.4477 3 15 3Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 656 B |
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024-2025 New Vector Ltd.
|
Copyright 2024-2025 New Vector Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024-2025 New Vector Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function CallEventAudioRenderer({
|
|||||||
const audioEngineRef = useLatest(audioEngineCtx);
|
const audioEngineRef = useLatest(audioEngineCtx);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const joinSub = vm.memberChanges$
|
const joinSub = vm.participantChanges$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(
|
filter(
|
||||||
({ joined, ids }) =>
|
({ joined, ids }) =>
|
||||||
@@ -72,7 +72,7 @@ export function CallEventAudioRenderer({
|
|||||||
void audioEngineRef.current?.playSound("join");
|
void audioEngineRef.current?.playSound("join");
|
||||||
});
|
});
|
||||||
|
|
||||||
const leftSub = vm.memberChanges$
|
const leftSub = vm.participantChanges$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(
|
filter(
|
||||||
({ ids, left }) =>
|
({ ids, left }) =>
|
||||||
|
|||||||
@@ -166,7 +166,11 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
const roomName = useRoomName(room);
|
const roomName = useRoomName(room);
|
||||||
const roomAvatar = useRoomAvatar(room);
|
const roomAvatar = useRoomAvatar(room);
|
||||||
const { perParticipantE2EE, returnToLobby } = useUrlParams();
|
const {
|
||||||
|
perParticipantE2EE,
|
||||||
|
returnToLobby,
|
||||||
|
password: passwordFromUrl,
|
||||||
|
} = useUrlParams();
|
||||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||||
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
|
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
|
||||||
const [useExperimentalToDeviceTransport] = useSetting(
|
const [useExperimentalToDeviceTransport] = useSetting(
|
||||||
@@ -174,7 +178,6 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Save the password once we start the groupCallView
|
// Save the password once we start the groupCallView
|
||||||
const { password: passwordFromUrl } = useUrlParams();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
|
if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
|
||||||
}, [passwordFromUrl, room.roomId]);
|
}, [passwordFromUrl, room.roomId]);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import useMeasure from "react-use-measure";
|
|||||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { BehaviorSubject, map } from "rxjs";
|
import { BehaviorSubject, map } from "rxjs";
|
||||||
import { useObservable } from "observable-hooks";
|
import { useObservable, useSubscription } from "observable-hooks";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||||
import {
|
import {
|
||||||
@@ -140,11 +140,11 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[Lifecycle] InCallView Component mounted, livekitroom state ${livekitRoom?.state}`,
|
`[Lifecycle] InCallView Component mounted, livekit room state ${livekitRoom?.state}`,
|
||||||
);
|
);
|
||||||
return (): void => {
|
return (): void => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[Lifecycle] InCallView Component unmounted, livekitroom state ${livekitRoom?.state}`,
|
`[Lifecycle] InCallView Component unmounted, livekit room state ${livekitRoom?.state}`,
|
||||||
);
|
);
|
||||||
livekitRoom
|
livekitRoom
|
||||||
?.disconnect()
|
?.disconnect()
|
||||||
@@ -159,6 +159,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
};
|
};
|
||||||
}, [livekitRoom]);
|
}, [livekitRoom]);
|
||||||
|
|
||||||
|
const { autoLeaveWhenOthersLeft } = useUrlParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (livekitRoom !== undefined) {
|
if (livekitRoom !== undefined) {
|
||||||
const reactionsReader = new ReactionsReader(props.rtcSession);
|
const reactionsReader = new ReactionsReader(props.rtcSession);
|
||||||
@@ -166,7 +168,10 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
props.rtcSession,
|
props.rtcSession,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
props.e2eeSystem,
|
{
|
||||||
|
encryptionSystem: props.e2eeSystem,
|
||||||
|
autoLeaveWhenOthersLeft,
|
||||||
|
},
|
||||||
connStateObservable$,
|
connStateObservable$,
|
||||||
reactionsReader.raisedHands$,
|
reactionsReader.raisedHands$,
|
||||||
reactionsReader.reactions$,
|
reactionsReader.reactions$,
|
||||||
@@ -183,6 +188,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
mediaDevices,
|
mediaDevices,
|
||||||
props.e2eeSystem,
|
props.e2eeSystem,
|
||||||
connStateObservable$,
|
connStateObservable$,
|
||||||
|
autoLeaveWhenOthersLeft,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (livekitRoom === undefined || vm === null) return null;
|
if (livekitRoom === undefined || vm === null) return null;
|
||||||
@@ -313,6 +319,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||||
const switchCamera = useSwitchCamera(vm.localVideo$);
|
const switchCamera = useSwitchCamera(vm.localVideo$);
|
||||||
|
useSubscription(vm.autoLeaveWhenOthersLeft$, onLeave);
|
||||||
|
|
||||||
// Ideally we could detect taps by listening for click events and checking
|
// Ideally we could detect taps by listening for click events and checking
|
||||||
// that the pointerType of the event is "touch", but this isn't yet supported
|
// that the pointerType of the event is "touch", but this isn't yet supported
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ test("It joins the correct Session", async () => {
|
|||||||
roomId: "roomId",
|
roomId: "roomId",
|
||||||
client: {
|
client: {
|
||||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||||
|
getOpenIdToken: vi.fn().mockResolvedValue({
|
||||||
|
access_token: "ACCCESS_TOKEN",
|
||||||
|
token_type: "Bearer",
|
||||||
|
matrix_server_name: "localhost",
|
||||||
|
expires_in: 10000,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
@@ -195,6 +201,12 @@ test("It should not fail with configuration error if homeserver config has livek
|
|||||||
roomId: "roomId",
|
roomId: "roomId",
|
||||||
client: {
|
client: {
|
||||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||||
|
getOpenIdToken: vi.fn().mockResolvedValue({
|
||||||
|
access_token: "ACCCESS_TOKEN",
|
||||||
|
token_type: "Bearer",
|
||||||
|
matrix_server_name: "localhost",
|
||||||
|
expires_in: 10000,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
|
||||||
import {
|
import {
|
||||||
isLivekitFocus,
|
isLivekitFocus,
|
||||||
isLivekitFocusConfig,
|
isLivekitFocusConfig,
|
||||||
type LivekitFocus,
|
type LivekitFocus,
|
||||||
type LivekitFocusActive,
|
type LivekitFocusActive,
|
||||||
|
type MatrixRTCSession,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
||||||
|
|
||||||
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||||
@@ -20,6 +20,7 @@ import { Config } from "./config/Config";
|
|||||||
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
|
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
|
||||||
import { MatrixRTCFocusMissingError } from "./utils/errors";
|
import { MatrixRTCFocusMissingError } from "./utils/errors";
|
||||||
import { getUrlParams } from "./UrlParams";
|
import { getUrlParams } from "./UrlParams";
|
||||||
|
import { getSFUConfigWithOpenID } from "./livekit/openIDSFU.ts";
|
||||||
|
|
||||||
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
||||||
|
|
||||||
@@ -46,6 +47,9 @@ async function makePreferredLivekitFoci(
|
|||||||
preferredFoci.push(focusInUse);
|
preferredFoci.push(focusInUse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warm up the first focus we owned, to ensure livekit room is created before any state event sent.
|
||||||
|
let toWarmUp: LivekitFocus | undefined;
|
||||||
|
|
||||||
// Prioritize the .well-known/matrix/client, if available, over the configured SFU
|
// Prioritize the .well-known/matrix/client, if available, over the configured SFU
|
||||||
const domain = rtcSession.room.client.getDomain();
|
const domain = rtcSession.room.client.getDomain();
|
||||||
if (domain) {
|
if (domain) {
|
||||||
@@ -55,18 +59,17 @@ async function makePreferredLivekitFoci(
|
|||||||
FOCI_WK_KEY
|
FOCI_WK_KEY
|
||||||
];
|
];
|
||||||
if (Array.isArray(wellKnownFoci)) {
|
if (Array.isArray(wellKnownFoci)) {
|
||||||
preferredFoci.push(
|
const validWellKnownFoci = wellKnownFoci
|
||||||
...wellKnownFoci
|
.filter((f) => !!f)
|
||||||
.filter((f) => !!f)
|
.filter(isLivekitFocusConfig)
|
||||||
.filter(isLivekitFocusConfig)
|
.map((wellKnownFocus) => {
|
||||||
.map((wellKnownFocus) => {
|
logger.log("Adding livekit focus from well known: ", wellKnownFocus);
|
||||||
logger.log(
|
return { ...wellKnownFocus, livekit_alias: livekitAlias };
|
||||||
"Adding livekit focus from well known: ",
|
});
|
||||||
wellKnownFocus,
|
if (validWellKnownFoci.length > 0) {
|
||||||
);
|
toWarmUp = validWellKnownFoci[0];
|
||||||
return { ...wellKnownFocus, livekit_alias: livekitAlias };
|
}
|
||||||
}),
|
preferredFoci.push(...validWellKnownFoci);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +80,15 @@ async function makePreferredLivekitFoci(
|
|||||||
livekit_service_url: urlFromConf,
|
livekit_service_url: urlFromConf,
|
||||||
livekit_alias: livekitAlias,
|
livekit_alias: livekitAlias,
|
||||||
};
|
};
|
||||||
|
toWarmUp = toWarmUp ?? focusFormConf;
|
||||||
logger.log("Adding livekit focus from config: ", focusFormConf);
|
logger.log("Adding livekit focus from config: ", focusFormConf);
|
||||||
preferredFoci.push(focusFormConf);
|
preferredFoci.push(focusFormConf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toWarmUp) {
|
||||||
|
// this will call the jwt/sfu/get endpoint to pre create the livekit room.
|
||||||
|
await getSFUConfigWithOpenID(rtcSession.room.client, toWarmUp);
|
||||||
|
}
|
||||||
if (preferredFoci.length === 0)
|
if (preferredFoci.length === 0)
|
||||||
throw new MatrixRTCFocusMissingError(domain ?? "");
|
throw new MatrixRTCFocusMissingError(domain ?? "");
|
||||||
return Promise.resolve(preferredFoci);
|
return Promise.resolve(preferredFoci);
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ import {
|
|||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||||
|
|
||||||
import { CallViewModel, type Layout } from "./CallViewModel";
|
import {
|
||||||
|
CallViewModel,
|
||||||
|
type CallViewModelOptions,
|
||||||
|
type Layout,
|
||||||
|
} from "./CallViewModel";
|
||||||
import {
|
import {
|
||||||
mockLivekitRoom,
|
mockLivekitRoom,
|
||||||
mockLocalParticipant,
|
mockLocalParticipant,
|
||||||
@@ -71,6 +75,7 @@ import {
|
|||||||
local,
|
local,
|
||||||
localId,
|
localId,
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
|
localRtcMemberDevice2,
|
||||||
} from "../utils/test-fixtures";
|
} from "../utils/test-fixtures";
|
||||||
import { ObservableScope } from "./ObservableScope";
|
import { ObservableScope } from "./ObservableScope";
|
||||||
import { MediaDevices } from "./MediaDevices";
|
import { MediaDevices } from "./MediaDevices";
|
||||||
@@ -231,6 +236,10 @@ function withCallViewModel(
|
|||||||
vm: CallViewModel,
|
vm: CallViewModel,
|
||||||
subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> },
|
subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> },
|
||||||
) => void,
|
) => void,
|
||||||
|
options: CallViewModelOptions = {
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
autoLeaveWhenOthersLeft: false,
|
||||||
|
},
|
||||||
): void {
|
): void {
|
||||||
const room = mockMatrixRoom({
|
const room = mockMatrixRoom({
|
||||||
client: {
|
client: {
|
||||||
@@ -281,9 +290,7 @@ function withCallViewModel(
|
|||||||
rtcSession as unknown as MatrixRTCSession,
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
liveKitRoom,
|
liveKitRoom,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
{
|
options,
|
||||||
kind: E2eeType.PER_PARTICIPANT,
|
|
||||||
},
|
|
||||||
connectionState$,
|
connectionState$,
|
||||||
raisedHands$,
|
raisedHands$,
|
||||||
new BehaviorSubject({}),
|
new BehaviorSubject({}),
|
||||||
@@ -978,7 +985,7 @@ test("should strip RTL characters from displayname", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable, behavior }) => {
|
||||||
// There should always be one tile for each MatrixRTCSession
|
// There should always be one tile for each MatrixRTCSession
|
||||||
const expectedLayoutMarbles = "ab";
|
const expectedLayoutMarbles = "ab";
|
||||||
|
|
||||||
@@ -1037,6 +1044,176 @@ it("should rank raised hands above video feeds and below speakers and presenters
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function nooneEverThere$<T>(
|
||||||
|
hot: (marbles: string, values: Record<string, T[]>) => Observable<T[]>,
|
||||||
|
): Observable<T[]> {
|
||||||
|
return hot("a-b-c-d", {
|
||||||
|
a: [], // Start empty
|
||||||
|
b: [], // Alice joins
|
||||||
|
c: [], // Alice still there
|
||||||
|
d: [], // Alice leaves
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function participantJoinLeave$(
|
||||||
|
hot: (
|
||||||
|
marbles: string,
|
||||||
|
values: Record<string, RemoteParticipant[]>,
|
||||||
|
) => Observable<RemoteParticipant[]>,
|
||||||
|
): Observable<RemoteParticipant[]> {
|
||||||
|
return hot("a-b-c-d", {
|
||||||
|
a: [], // Start empty
|
||||||
|
b: [aliceParticipant], // Alice joins
|
||||||
|
c: [aliceParticipant], // Alice still there
|
||||||
|
d: [], // Alice leaves
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rtcMemberJoinLeave$(
|
||||||
|
hot: (
|
||||||
|
marbles: string,
|
||||||
|
values: Record<string, CallMembership[]>,
|
||||||
|
) => Observable<CallMembership[]>,
|
||||||
|
): Observable<CallMembership[]> {
|
||||||
|
return hot("a-b-c-d", {
|
||||||
|
a: [], // Start empty
|
||||||
|
b: [aliceRtcMember], // Alice joins
|
||||||
|
c: [aliceRtcMember], // Alice still there
|
||||||
|
d: [], // Alice leaves
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("allOthersLeft$ emits only when someone joined and then all others left", () => {
|
||||||
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
|
// Test scenario 1: No one ever joins - should only emit initial false and never emit again
|
||||||
|
withCallViewModel(
|
||||||
|
scope.behavior(nooneEverThere$(hot), []),
|
||||||
|
scope.behavior(nooneEverThere$(hot), []),
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map(),
|
||||||
|
mockMediaDevices({}),
|
||||||
|
(vm) => {
|
||||||
|
expectObservable(vm.allOthersLeft$).toBe("n------", { n: false });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allOthersLeft$ emits true when someone joined and then all others left", () => {
|
||||||
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
|
withCallViewModel(
|
||||||
|
scope.behavior(participantJoinLeave$(hot), []),
|
||||||
|
scope.behavior(rtcMemberJoinLeave$(hot), []),
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map(),
|
||||||
|
mockMediaDevices({}),
|
||||||
|
(vm) => {
|
||||||
|
expectObservable(vm.allOthersLeft$).toBe(
|
||||||
|
"n-----u", // false initially, then at frame 6: true then false emissions in same frame
|
||||||
|
{ n: false, u: true }, // map(() => {})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("autoLeaveWhenOthersLeft$ emits only when autoLeaveWhenOthersLeft option is enabled", () => {
|
||||||
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
|
withCallViewModel(
|
||||||
|
scope.behavior(participantJoinLeave$(hot), []),
|
||||||
|
scope.behavior(rtcMemberJoinLeave$(hot), []),
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map(),
|
||||||
|
mockMediaDevices({}),
|
||||||
|
(vm) => {
|
||||||
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe(
|
||||||
|
"------e", // false initially, then at frame 6: true then false emissions in same frame
|
||||||
|
{ e: undefined },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("autoLeaveWhenOthersLeft$ never emits autoLeaveWhenOthersLeft option is enabled but no-one is there", () => {
|
||||||
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
|
withCallViewModel(
|
||||||
|
scope.behavior(nooneEverThere$(hot), []),
|
||||||
|
scope.behavior(nooneEverThere$(hot), []),
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map(),
|
||||||
|
mockMediaDevices({}),
|
||||||
|
(vm) => {
|
||||||
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("-------");
|
||||||
|
},
|
||||||
|
{
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("autoLeaveWhenOthersLeft$ doesn't emit when autoLeaveWhenOthersLeft option is disabled and all others left", () => {
|
||||||
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
|
withCallViewModel(
|
||||||
|
scope.behavior(participantJoinLeave$(hot), []),
|
||||||
|
scope.behavior(rtcMemberJoinLeave$(hot), []),
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map(),
|
||||||
|
mockMediaDevices({}),
|
||||||
|
(vm) => {
|
||||||
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("-------");
|
||||||
|
},
|
||||||
|
{
|
||||||
|
autoLeaveWhenOthersLeft: false,
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("autoLeaveWhenOthersLeft$ doesn't emits when autoLeaveWhenOthersLeft option is enabled and all others left", () => {
|
||||||
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
|
withCallViewModel(
|
||||||
|
scope.behavior(
|
||||||
|
hot("a-b-c-d", {
|
||||||
|
a: [], // Alone
|
||||||
|
b: [aliceParticipant], // Alice joins
|
||||||
|
c: [aliceParticipant],
|
||||||
|
d: [], // Local joins with a second device
|
||||||
|
}),
|
||||||
|
[], //Alice leaves
|
||||||
|
),
|
||||||
|
scope.behavior(
|
||||||
|
hot("a-b-c-d", {
|
||||||
|
a: [localRtcMember], // Start empty
|
||||||
|
b: [localRtcMember, aliceRtcMember], // Alice joins
|
||||||
|
c: [localRtcMember, aliceRtcMember, localRtcMemberDevice2], // Alice still there
|
||||||
|
d: [localRtcMember, localRtcMemberDevice2], // The second Alice leaves
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map(),
|
||||||
|
mockMediaDevices({}),
|
||||||
|
(vm) => {
|
||||||
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("------e", {
|
||||||
|
e: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("audio output changes when toggling earpiece mode", () => {
|
test("audio output changes when toggling earpiece mode", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
|
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
|||||||
import { type MediaDevices } from "./MediaDevices";
|
import { type MediaDevices } from "./MediaDevices";
|
||||||
import { type Behavior } from "./Behavior";
|
import { type Behavior } from "./Behavior";
|
||||||
|
|
||||||
|
export interface CallViewModelOptions {
|
||||||
|
encryptionSystem: EncryptionSystem;
|
||||||
|
autoLeaveWhenOthersLeft?: boolean;
|
||||||
|
}
|
||||||
// How long we wait after a focus switch before showing the real participant
|
// How long we wait after a focus switch before showing the real participant
|
||||||
// list again
|
// list again
|
||||||
const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
|
const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
|
||||||
@@ -473,49 +477,47 @@ export class CallViewModel extends ViewModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private readonly memberships$: Observable<CallMembership[]> = merge(
|
||||||
|
// Handle call membership changes.
|
||||||
|
fromEvent(this.matrixRTCSession, MatrixRTCSessionEvent.MembershipsChanged),
|
||||||
|
// Handle room membership changes (and displayname updates)
|
||||||
|
fromEvent(this.matrixRTCSession.room, RoomStateEvent.Members),
|
||||||
|
).pipe(
|
||||||
|
startWith(this.matrixRTCSession.memberships),
|
||||||
|
map(() => {
|
||||||
|
return this.matrixRTCSession.memberships;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displaynames for each member of the call. This will disambiguate
|
* Displaynames for each member of the call. This will disambiguate
|
||||||
* any displaynames that clashes with another member. Only members
|
* any displaynames that clashes with another member. Only members
|
||||||
* joined to the call are considered here.
|
* joined to the call are considered here.
|
||||||
*/
|
*/
|
||||||
public readonly memberDisplaynames$ = this.scope.behavior(
|
public readonly memberDisplaynames$ = this.memberships$.pipe(
|
||||||
merge(
|
map((memberships) => {
|
||||||
// Handle call membership changes.
|
const displaynameMap = new Map<string, string>();
|
||||||
fromEvent(
|
const { room } = this.matrixRTCSession;
|
||||||
this.matrixRTCSession,
|
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
|
||||||
),
|
|
||||||
// Handle room membership changes (and displayname updates)
|
|
||||||
fromEvent(this.matrixRTCSession.room, RoomStateEvent.Members),
|
|
||||||
).pipe(
|
|
||||||
startWith(null),
|
|
||||||
map(() => {
|
|
||||||
const displaynameMap = new Map<string, string>();
|
|
||||||
const { room, memberships } = this.matrixRTCSession;
|
|
||||||
|
|
||||||
// We only consider RTC members for disambiguation as they are the only visible members.
|
// We only consider RTC members for disambiguation as they are the only visible members.
|
||||||
for (const rtcMember of memberships) {
|
for (const rtcMember of memberships) {
|
||||||
const matrixIdentifier = `${rtcMember.sender}:${rtcMember.deviceId}`;
|
const matrixIdentifier = `${rtcMember.sender}:${rtcMember.deviceId}`;
|
||||||
const { member } = getRoomMemberFromRtcMember(rtcMember, room);
|
const { member } = getRoomMemberFromRtcMember(rtcMember, room);
|
||||||
if (!member) {
|
if (!member) {
|
||||||
logger.error(
|
logger.error("Could not find member for media id:", matrixIdentifier);
|
||||||
"Could not find member for media id:",
|
continue;
|
||||||
matrixIdentifier,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const disambiguate = shouldDisambiguate(member, memberships, room);
|
|
||||||
displaynameMap.set(
|
|
||||||
matrixIdentifier,
|
|
||||||
calculateDisplayName(member, disambiguate),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return displaynameMap;
|
const disambiguate = shouldDisambiguate(member, memberships, room);
|
||||||
}),
|
displaynameMap.set(
|
||||||
// It turns out that doing the disambiguation above is rather expensive on Safari (10x slower
|
matrixIdentifier,
|
||||||
// than on Chrome/Firefox). This means it is important that we multicast the result so that we
|
calculateDisplayName(member, disambiguate),
|
||||||
// don't do this work more times than we need to. This is achieved by converting to a behavior:
|
);
|
||||||
),
|
}
|
||||||
|
return displaynameMap;
|
||||||
|
}),
|
||||||
|
// It turns out that doing the disambiguation above is rather expensive on Safari (10x slower
|
||||||
|
// than on Chrome/Firefox). This means it is important that we multicast the result so that we
|
||||||
|
// don't do this work more times than we need to. This is achieved by converting to a behavior:
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly handsRaised$ = this.scope.behavior(this.handsRaisedSubject$);
|
public readonly handsRaised$ = this.scope.behavior(this.handsRaisedSubject$);
|
||||||
@@ -612,7 +614,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
indexedMediaId,
|
indexedMediaId,
|
||||||
member,
|
member,
|
||||||
participant,
|
participant,
|
||||||
this.encryptionSystem,
|
this.options.encryptionSystem,
|
||||||
this.livekitRoom,
|
this.livekitRoom,
|
||||||
this.memberDisplaynames$.pipe(
|
this.memberDisplaynames$.pipe(
|
||||||
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
|
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
|
||||||
@@ -635,7 +637,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
screenShareId,
|
screenShareId,
|
||||||
member,
|
member,
|
||||||
participant,
|
participant,
|
||||||
this.encryptionSystem,
|
this.options.encryptionSystem,
|
||||||
this.livekitRoom,
|
this.livekitRoom,
|
||||||
this.memberDisplaynames$.pipe(
|
this.memberDisplaynames$.pipe(
|
||||||
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
|
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
|
||||||
@@ -676,7 +678,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
nonMemberId,
|
nonMemberId,
|
||||||
undefined,
|
undefined,
|
||||||
participant,
|
participant,
|
||||||
this.encryptionSystem,
|
this.options.encryptionSystem,
|
||||||
this.livekitRoom,
|
this.livekitRoom,
|
||||||
this.memberDisplaynames$.pipe(
|
this.memberDisplaynames$.pipe(
|
||||||
map(
|
map(
|
||||||
@@ -726,18 +728,77 @@ export class CallViewModel extends ViewModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly memberChanges$ = this.userMedia$
|
/**
|
||||||
.pipe(map((mediaItems) => mediaItems.map((m) => m.id)))
|
* This observable tracks the currently connected participants.
|
||||||
.pipe(
|
*
|
||||||
scan<string[], { ids: string[]; joined: string[]; left: string[] }>(
|
* - Each participant has one livekit connection
|
||||||
(prev, ids) => {
|
* - Each participant has a corresponding MatrixRTC membership state event
|
||||||
const left = prev.ids.filter((id) => !ids.includes(id));
|
* - There can be multiple participants for one matrix user.
|
||||||
const joined = ids.filter((id) => !prev.ids.includes(id));
|
*/
|
||||||
return { ids, joined, left };
|
public readonly participantChanges$ = this.userMedia$.pipe(
|
||||||
},
|
map((mediaItems) => mediaItems.map((m) => m.id)),
|
||||||
{ ids: [], joined: [], left: [] },
|
scan<string[], { ids: string[]; joined: string[]; left: string[] }>(
|
||||||
),
|
(prev, ids) => {
|
||||||
);
|
const left = prev.ids.filter((id) => !ids.includes(id));
|
||||||
|
const joined = ids.filter((id) => !prev.ids.includes(id));
|
||||||
|
return { ids, joined, left };
|
||||||
|
},
|
||||||
|
{ ids: [], joined: [], left: [] },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This observable tracks the matrix users that are currently in the call.
|
||||||
|
* There can be just one matrix user with multiple participants (see also participantChanges$)
|
||||||
|
*/
|
||||||
|
public readonly matrixUserChanges$ = this.userMedia$.pipe(
|
||||||
|
map(
|
||||||
|
(mediaItems) =>
|
||||||
|
new Set(
|
||||||
|
mediaItems
|
||||||
|
.map((m) => m.vm.member?.userId)
|
||||||
|
.filter((id) => id !== undefined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
scan<
|
||||||
|
Set<string>,
|
||||||
|
{
|
||||||
|
userIds: Set<string>;
|
||||||
|
joinedUserIds: Set<string>;
|
||||||
|
leftUserIds: Set<string>;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(prevState, userIds) => {
|
||||||
|
const left = new Set(
|
||||||
|
[...prevState.userIds].filter((id) => !userIds.has(id)),
|
||||||
|
);
|
||||||
|
const joined = new Set(
|
||||||
|
[...userIds].filter((id) => !prevState.userIds.has(id)),
|
||||||
|
);
|
||||||
|
return { userIds: userIds, joinedUserIds: joined, leftUserIds: left };
|
||||||
|
},
|
||||||
|
{ userIds: new Set(), joinedUserIds: new Set(), leftUserIds: new Set() },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public readonly allOthersLeft$ = this.matrixUserChanges$.pipe(
|
||||||
|
map(({ userIds, leftUserIds }) => {
|
||||||
|
const userId = this.matrixRTCSession.room.client.getUserId();
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn("Could access client.getUserId to compute allOthersLeft");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return userIds.size === 1 && userIds.has(userId) && leftUserIds.size > 0;
|
||||||
|
}),
|
||||||
|
startWith(false),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
|
||||||
|
public readonly autoLeaveWhenOthersLeft$ = this.allOthersLeft$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter((leave) => (leave && this.options.autoLeaveWhenOthersLeft) ?? false),
|
||||||
|
map(() => {}),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of MediaItems that we want to display, that are of type ScreenShare
|
* List of MediaItems that we want to display, that are of type ScreenShare
|
||||||
@@ -1426,7 +1487,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
private readonly matrixRTCSession: MatrixRTCSession,
|
private readonly matrixRTCSession: MatrixRTCSession,
|
||||||
private readonly livekitRoom: LivekitRoom,
|
private readonly livekitRoom: LivekitRoom,
|
||||||
private readonly mediaDevices: MediaDevices,
|
private readonly mediaDevices: MediaDevices,
|
||||||
private readonly encryptionSystem: EncryptionSystem,
|
private readonly options: CallViewModelOptions,
|
||||||
private readonly connectionState$: Observable<ECConnectionState>,
|
private readonly connectionState$: Observable<ECConnectionState>,
|
||||||
private readonly handsRaisedSubject$: Observable<
|
private readonly handsRaisedSubject$: Observable<
|
||||||
Record<string, RaisedHandInfo>
|
Record<string, RaisedHandInfo>
|
||||||
|
|||||||
@@ -88,40 +88,48 @@ Please see LICENSE in the repository root for full details.
|
|||||||
padding: var(--cpd-space-2x);
|
padding: var(--cpd-space-2x);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--cpd-radius-pill-effect);
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
background: var(--cpd-color-alpha-gray-1400);
|
background: rgba(from var(--cpd-color-gray-100) r g b / 0.6);
|
||||||
box-shadow: var(--small-drop-shadow);
|
box-shadow: var(--small-drop-shadow);
|
||||||
transition:
|
transition:
|
||||||
opacity 0.15s,
|
opacity 0.15s,
|
||||||
background-color 0.1s;
|
background-color 0.1s;
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
--inset: 6px;
|
--inset: 6px;
|
||||||
inset-block-end: var(--inset);
|
inset-block-end: var(--inset);
|
||||||
inset-inline-end: var(--inset);
|
inset-inline-end: var(--inset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bottomRightButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
position: absolute;
|
||||||
|
inset-block-end: var(--cpd-space-1x);
|
||||||
|
inset-inline-end: var(--cpd-space-1x);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.expand > svg {
|
.expand > svg {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--cpd-color-icon-on-solid-primary);
|
color: var(--cpd-color-icon-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover) {
|
@media (hover) {
|
||||||
.expand:hover {
|
.expand:hover {
|
||||||
background: var(--cpd-color-bg-action-primary-hovered);
|
background: var(--cpd-color-gray-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand:active {
|
.expand:active {
|
||||||
background: var(--cpd-color-bg-action-primary-pressed);
|
background: var(--cpd-color-gray-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover) {
|
@media (hover) {
|
||||||
.tile:hover > button {
|
.tile:hover > div > button {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile:has(:focus-visible) > button {
|
.tile:has(:focus-visible) > div > button {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import classNames from "classnames";
|
|||||||
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
||||||
import { type RoomMember } from "matrix-js-sdk";
|
import { type RoomMember } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import FullScreenMaximiseIcon from "../icons/FullScreenMaximise.svg?react";
|
||||||
|
import FullScreenMinimiseIcon from "../icons/FullScreenMinimise.svg?react";
|
||||||
import { MediaView } from "./MediaView";
|
import { MediaView } from "./MediaView";
|
||||||
import styles from "./SpotlightTile.module.css";
|
import styles from "./SpotlightTile.module.css";
|
||||||
import {
|
import {
|
||||||
@@ -210,6 +212,26 @@ export const SpotlightTile: FC<Props> = ({
|
|||||||
const canGoBack = visibleIndex > 0;
|
const canGoBack = visibleIndex > 0;
|
||||||
const canGoToNext = visibleIndex !== -1 && visibleIndex < media.length - 1;
|
const canGoToNext = visibleIndex !== -1 && visibleIndex < media.length - 1;
|
||||||
|
|
||||||
|
const isFullscreen = useCallback((): boolean => {
|
||||||
|
const rootElement = document.body;
|
||||||
|
if (rootElement && document.fullscreenElement) return true;
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const FullScreenIcon = isFullscreen()
|
||||||
|
? FullScreenMinimiseIcon
|
||||||
|
: FullScreenMaximiseIcon;
|
||||||
|
|
||||||
|
const onToggleFullscreen = useCallback(() => {
|
||||||
|
const rootElement = document.body;
|
||||||
|
if (!rootElement) return;
|
||||||
|
if (isFullscreen()) {
|
||||||
|
void document?.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
void rootElement.requestFullscreen();
|
||||||
|
}
|
||||||
|
}, [isFullscreen]);
|
||||||
|
|
||||||
// To keep track of which item is visible, we need an intersection observer
|
// To keep track of which item is visible, we need an intersection observer
|
||||||
// hooked up to the root element and the items. Because the items will run
|
// hooked up to the root element and the items. Because the items will run
|
||||||
// their effects before their parent does, we need to do this dance with an
|
// their effects before their parent does, we need to do this dance with an
|
||||||
@@ -292,17 +314,28 @@ export const SpotlightTile: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{onToggleExpanded && (
|
<div className={styles.bottomRightButtons}>
|
||||||
<button
|
<button
|
||||||
className={classNames(styles.expand)}
|
className={classNames(styles.expand)}
|
||||||
aria-label={
|
aria-label={"maximise"}
|
||||||
expanded ? t("video_tile.collapse") : t("video_tile.expand")
|
onClick={onToggleFullscreen}
|
||||||
}
|
|
||||||
onClick={onToggleExpanded}
|
|
||||||
>
|
>
|
||||||
<ToggleExpandIcon aria-hidden width={20} height={20} />
|
<FullScreenIcon aria-hidden width={20} height={20} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
|
{onToggleExpanded && (
|
||||||
|
<button
|
||||||
|
className={classNames(styles.expand)}
|
||||||
|
aria-label={
|
||||||
|
expanded ? t("video_tile.collapse") : t("video_tile.expand")
|
||||||
|
}
|
||||||
|
onClick={onToggleExpanded}
|
||||||
|
>
|
||||||
|
<ToggleExpandIcon aria-hidden width={20} height={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{canGoToNext && (
|
{canGoToNext && (
|
||||||
<button
|
<button
|
||||||
className={classNames(styles.advance, styles.next)}
|
className={classNames(styles.advance, styles.next)}
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import {
|
|||||||
mockLocalParticipant,
|
mockLocalParticipant,
|
||||||
} from "./test";
|
} from "./test";
|
||||||
|
|
||||||
export const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
export const localRtcMember = mockRtcMembership("@carol:example.org", "1111");
|
||||||
|
export const localRtcMemberDevice2 = mockRtcMembership(
|
||||||
|
"@carol:example.org",
|
||||||
|
"2222",
|
||||||
|
);
|
||||||
export const local = mockMatrixRoomMember(localRtcMember);
|
export const local = mockMatrixRoomMember(localRtcMember);
|
||||||
export const localParticipant = mockLocalParticipant({ identity: "" });
|
export const localParticipant = mockLocalParticipant({ identity: "" });
|
||||||
export const localId = `${local.userId}:${localRtcMember.deviceId}`;
|
export const localId = `${local.userId}:${localRtcMember.deviceId}`;
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
liveKitRoom,
|
liveKitRoom,
|
||||||
mockMediaDevices({}),
|
mockMediaDevices({}),
|
||||||
{
|
{
|
||||||
kind: E2eeType.PER_PARTICIPANT,
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
},
|
},
|
||||||
of(ConnectionState.Connected),
|
of(ConnectionState.Connected),
|
||||||
handRaisedSubject$,
|
handRaisedSubject$,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export interface OurRunHelpers extends RunHelpers {
|
|||||||
values?: { [marble: string]: T },
|
values?: { [marble: string]: T },
|
||||||
error?: unknown,
|
error?: unknown,
|
||||||
): Behavior<T>;
|
): Behavior<T>;
|
||||||
|
scope: ObservableScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestRunnerGlobal {
|
interface TestRunnerGlobal {
|
||||||
@@ -96,6 +97,7 @@ export function withTestScheduler(
|
|||||||
scheduler.run((helpers) =>
|
scheduler.run((helpers) =>
|
||||||
continuation({
|
continuation({
|
||||||
...helpers,
|
...helpers,
|
||||||
|
scope,
|
||||||
schedule(marbles, actions) {
|
schedule(marbles, actions) {
|
||||||
const actionsObservable$ = helpers
|
const actionsObservable$ = helpers
|
||||||
.cold(marbles)
|
.cold(marbles)
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { defineConfig, mergeConfig } from "vite";
|
import { defineConfig, mergeConfig } from "vite";
|
||||||
import fullConfig from "./vite.config";
|
|
||||||
import generateFile from "vite-plugin-generate-file";
|
import generateFile from "vite-plugin-generate-file";
|
||||||
|
|
||||||
|
import fullConfig from "./vite.config";
|
||||||
|
|
||||||
const base = "./";
|
const base = "./";
|
||||||
|
|
||||||
// Config for embedded deployments (possibly hosted under a non-root path)
|
// Config for embedded deployments (possibly hosted under a non-root path)
|
||||||
@@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineConfig, loadEnv, searchForWorkspaceRoot } from "vite";
|
import {
|
||||||
|
loadEnv,
|
||||||
|
searchForWorkspaceRoot,
|
||||||
|
type ConfigEnv,
|
||||||
|
type UserConfig,
|
||||||
|
} from "vite";
|
||||||
import svgrPlugin from "vite-plugin-svgr";
|
import svgrPlugin from "vite-plugin-svgr";
|
||||||
import { createHtmlPlugin } from "vite-plugin-html";
|
import { createHtmlPlugin } from "vite-plugin-html";
|
||||||
import { codecovVitePlugin } from "@codecov/vite-plugin";
|
import { codecovVitePlugin } from "@codecov/vite-plugin";
|
||||||
@@ -15,10 +20,14 @@ import { realpathSync } from "fs";
|
|||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode, packageType }) => {
|
// Modified type helper from defineConfig to allow for packageType (see defineConfig from vite)
|
||||||
|
export default ({
|
||||||
|
mode,
|
||||||
|
packageType,
|
||||||
|
}: ConfigEnv & { packageType?: "full" | "embedded" }): UserConfig => {
|
||||||
const env = loadEnv(mode, process.cwd());
|
const env = loadEnv(mode, process.cwd());
|
||||||
// Environment variables with the VITE_ prefix are accessible at runtime.
|
// Environment variables with the VITE_ prefix are accessible at runtime.
|
||||||
// So, we set this to allow for build/package specific behaviour.
|
// So, we set this to allow for build/package specific behavior.
|
||||||
// In future we might be able to do what is needed via code splitting at
|
// In future we might be able to do what is needed via code splitting at
|
||||||
// build time.
|
// build time.
|
||||||
process.env.VITE_PACKAGE = packageType ?? "full";
|
process.env.VITE_PACKAGE = packageType ?? "full";
|
||||||
@@ -93,7 +102,7 @@ export default defineConfig(({ mode, packageType }) => {
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
assetFileNames: ({ originalFileNames }) => {
|
assetFileNames: ({ originalFileNames }): string => {
|
||||||
if (originalFileNames) {
|
if (originalFileNames) {
|
||||||
for (const name of originalFileNames) {
|
for (const name of originalFileNames) {
|
||||||
// Custom asset name for locales to include the locale code in the filename
|
// Custom asset name for locales to include the locale code in the filename
|
||||||
@@ -143,4 +152,4 @@ export default defineConfig(({ mode, packageType }) => {
|
|||||||
exclude: ["@matrix-org/matrix-sdk-crypto-wasm"],
|
exclude: ["@matrix-org/matrix-sdk-crypto-wasm"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineConfig, mergeConfig } from "vitest/config";
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
import viteConfig from "./vite.config.js";
|
|
||||||
|
import viteConfig from "./vite.config";
|
||||||
|
|
||||||
export default defineConfig((configEnv) =>
|
export default defineConfig((configEnv) =>
|
||||||
mergeConfig(
|
mergeConfig(
|
||||||
@@ -10046,8 +10046,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"livekit-client@npm:^2.13.0":
|
"livekit-client@npm:^2.13.0":
|
||||||
version: 2.15.4
|
version: 2.15.5
|
||||||
resolution: "livekit-client@npm:2.15.4"
|
resolution: "livekit-client@npm:2.15.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@livekit/mutex": "npm:1.1.1"
|
"@livekit/mutex": "npm:1.1.1"
|
||||||
"@livekit/protocol": "npm:1.39.3"
|
"@livekit/protocol": "npm:1.39.3"
|
||||||
@@ -10060,7 +10060,7 @@ __metadata:
|
|||||||
webrtc-adapter: "npm:^9.0.1"
|
webrtc-adapter: "npm:^9.0.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@types/dom-mediacapture-record": ^1
|
"@types/dom-mediacapture-record": ^1
|
||||||
checksum: 10c0/f12a3b604aed8e075791c60a75c9eea5467e5a0bab48c2eb23216ec357e89a50199274121e0330b61f946d0be9556058824ce1b832003e722786e54ba33017a6
|
checksum: 10c0/52a70bdd39d802737ed7c25ae5d06daf9921156c4fc74f918009e86204430b2d200b66c55cefab949be4e5411cbc4d25eac92976f62f96b7226057a5b0706baa
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user