mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-18 18:59:23 +00:00
Merge remote-tracking branch 'origin/livekit' into hs/apply-hints
This commit is contained in:
@@ -44,6 +44,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
echo "TAG=latest" >> "$GITHUB_OUTPUT"
|
echo "TAG=latest" >> "$GITHUB_OUTPUT"
|
||||||
|
elif [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+\-rc\.[0-9]+$ ]]; then
|
||||||
|
echo "TAG=rc" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "TAG=other" >> "$GITHUB_OUTPUT"
|
echo "TAG=other" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
@@ -163,6 +165,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [[ "${{ needs.versioning.outputs.TAG }}" == "latest" ]]; then
|
if [[ "${{ needs.versioning.outputs.TAG }}" == "latest" ]]; then
|
||||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
||||||
|
elif [[ "${{ needs.versioning.outputs.TAG }}" == "rc" ]]; then
|
||||||
|
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}-SNAPSHOT" >> "$GITHUB_ENV"
|
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}-SNAPSHOT" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ 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 { describe, expect, it } from "vitest";
|
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
|
import * as PlatformMod from "../src/Platform";
|
||||||
import {
|
import {
|
||||||
getRoomIdentifierFromUrl,
|
getRoomIdentifierFromUrl,
|
||||||
getUrlParams,
|
computeUrlParams,
|
||||||
HeaderStyle,
|
HeaderStyle,
|
||||||
|
getUrlParams,
|
||||||
} from "../src/UrlParams";
|
} from "../src/UrlParams";
|
||||||
|
|
||||||
const ROOM_NAME = "roomNameHere";
|
const ROOM_NAME = "roomNameHere";
|
||||||
@@ -103,16 +106,16 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("preload", () => {
|
describe("preload", () => {
|
||||||
it("defaults to false", () => {
|
it("defaults to false", () => {
|
||||||
expect(getUrlParams().preload).toBe(false);
|
expect(computeUrlParams().preload).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignored in SPA mode", () => {
|
it("ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?preload=true").preload).toBe(false);
|
expect(computeUrlParams("?preload=true").preload).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respected in widget mode", () => {
|
it("respected in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).preload,
|
).preload,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
@@ -121,19 +124,20 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("returnToLobby", () => {
|
describe("returnToLobby", () => {
|
||||||
it("is false in SPA mode", () => {
|
it("is false in SPA mode", () => {
|
||||||
expect(getUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
expect(computeUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to false in widget mode", () => {
|
it("defaults to false in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams("?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo")
|
computeUrlParams(
|
||||||
.returnToLobby,
|
"?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
|
).returnToLobby,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respected in widget mode", () => {
|
it("respected in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).returnToLobby,
|
).returnToLobby,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
@@ -142,12 +146,12 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("userId", () => {
|
describe("userId", () => {
|
||||||
it("is ignored in SPA mode", () => {
|
it("is ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?userId=asd").userId).toBe(null);
|
expect(computeUrlParams("?userId=asd").userId).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in widget mode", () => {
|
it("is parsed in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).userId,
|
).userId,
|
||||||
).toBe("asd");
|
).toBe("asd");
|
||||||
@@ -156,12 +160,12 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("deviceId", () => {
|
describe("deviceId", () => {
|
||||||
it("is ignored in SPA mode", () => {
|
it("is ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?deviceId=asd").deviceId).toBe(null);
|
expect(computeUrlParams("?deviceId=asd").deviceId).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in widget mode", () => {
|
it("is parsed in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).deviceId,
|
).deviceId,
|
||||||
).toBe("asd");
|
).toBe("asd");
|
||||||
@@ -170,12 +174,12 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("baseUrl", () => {
|
describe("baseUrl", () => {
|
||||||
it("is ignored in SPA mode", () => {
|
it("is ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
expect(computeUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in widget mode", () => {
|
it("is parsed in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).baseUrl,
|
).baseUrl,
|
||||||
).toBe("asd");
|
).toBe("asd");
|
||||||
@@ -185,28 +189,28 @@ describe("UrlParams", () => {
|
|||||||
describe("viaServers", () => {
|
describe("viaServers", () => {
|
||||||
it("is ignored in widget mode", () => {
|
it("is ignored in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).viaServers,
|
).viaServers,
|
||||||
).toBe(null);
|
).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in SPA mode", () => {
|
it("is parsed in SPA mode", () => {
|
||||||
expect(getUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
expect(computeUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("homeserver", () => {
|
describe("homeserver", () => {
|
||||||
it("is ignored in widget mode", () => {
|
it("is ignored in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).homeserver,
|
).homeserver,
|
||||||
).toBe(null);
|
).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in SPA mode", () => {
|
it("is parsed in SPA mode", () => {
|
||||||
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
expect(computeUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,7 +241,7 @@ describe("UrlParams", () => {
|
|||||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||||
skipLobby: true,
|
skipLobby: true,
|
||||||
returnToLobby: false,
|
returnToLobby: false,
|
||||||
sendNotificationType: "notification",
|
sendNotificationType: platform === "desktop" ? "notification" : "ring",
|
||||||
});
|
});
|
||||||
const joinExistingCallDefaults = (platform: string): object => ({
|
const joinExistingCallDefaults = (platform: string): object => ({
|
||||||
confineToRoom: true,
|
confineToRoom: true,
|
||||||
@@ -252,24 +256,55 @@ describe("UrlParams", () => {
|
|||||||
skipLobby: false,
|
skipLobby: false,
|
||||||
returnToLobby: false,
|
returnToLobby: false,
|
||||||
sendNotificationType: "notification",
|
sendNotificationType: "notification",
|
||||||
|
defaultAudioEnabled: true,
|
||||||
|
defaultVideoEnabled: true,
|
||||||
});
|
});
|
||||||
it("use no-intent-defaults with unknown intent", () => {
|
it("use no-intent-defaults with unknown intent", () => {
|
||||||
expect(getUrlParams()).toMatchObject(noIntentDefaults);
|
expect(computeUrlParams()).toMatchObject(noIntentDefaults);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores intent if it is not a valid value", () => {
|
it("ignores intent if it is not a valid value", () => {
|
||||||
expect(getUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
expect(computeUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts start_call", () => {
|
it("accepts start_call", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams("?intent=start_call&widgetId=1234&parentUrl=parent.org"),
|
computeUrlParams(
|
||||||
|
"?intent=start_call&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
).toMatchObject(startNewCallDefaults("desktop"));
|
).toMatchObject(startNewCallDefaults("desktop"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts start_call_dm mobile", () => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("android");
|
||||||
|
onTestFinished(() => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("desktop");
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
computeUrlParams(
|
||||||
|
"?intent=start_call_dm&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
|
).toMatchObject(startNewCallDefaults("android"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts start_call_dm mobile and prioritizes overwritten params", () => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("android");
|
||||||
|
onTestFinished(() => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("desktop");
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
computeUrlParams(
|
||||||
|
"?intent=start_call_dm&widgetId=1234&parentUrl=parent.org&sendNotificationType=notification",
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
...startNewCallDefaults("android"),
|
||||||
|
sendNotificationType: "notification",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts join_existing", () => {
|
it("accepts join_existing", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?intent=join_existing&widgetId=1234&parentUrl=parent.org",
|
"?intent=join_existing&widgetId=1234&parentUrl=parent.org",
|
||||||
),
|
),
|
||||||
).toMatchObject(joinExistingCallDefaults("desktop"));
|
).toMatchObject(joinExistingCallDefaults("desktop"));
|
||||||
@@ -278,31 +313,55 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("skipLobby", () => {
|
describe("skipLobby", () => {
|
||||||
it("defaults to false", () => {
|
it("defaults to false", () => {
|
||||||
expect(getUrlParams().skipLobby).toBe(false);
|
expect(computeUrlParams().skipLobby).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to false if intent is start_call in SPA mode", () => {
|
it("defaults to false if intent is start_call in SPA mode", () => {
|
||||||
expect(getUrlParams("?intent=start_call").skipLobby).toBe(false);
|
expect(computeUrlParams("?intent=start_call").skipLobby).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to true if intent is start_call in widget mode", () => {
|
it("defaults to true if intent is start_call in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).skipLobby,
|
).skipLobby,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("default to false if intent is join_existing", () => {
|
it("default to false if intent is join_existing", () => {
|
||||||
expect(getUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
expect(computeUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("header", () => {
|
describe("header", () => {
|
||||||
it("uses header if provided", () => {
|
it("uses header if provided", () => {
|
||||||
expect(getUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
expect(computeUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
||||||
"app_bar",
|
"app_bar",
|
||||||
);
|
);
|
||||||
expect(getUrlParams("?header=none&hideHeader=false").header).toBe("none");
|
expect(computeUrlParams("?header=none&hideHeader=false").header).toBe(
|
||||||
|
"none",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("getUrlParams", () => {
|
||||||
|
it("uses cached values", () => {
|
||||||
|
const spy = vi.spyOn(logger, "info");
|
||||||
|
// call get once
|
||||||
|
const params = getUrlParams("?header=app_bar&hideHeader=true", "");
|
||||||
|
// call get twice
|
||||||
|
expect(getUrlParams("?header=app_bar&hideHeader=true", "")).toBe(params);
|
||||||
|
// expect compute to only be called once
|
||||||
|
// it will only log when it is computing the values
|
||||||
|
expect(spy).toHaveBeenCalledExactlyOnceWith(
|
||||||
|
"UrlParams: final set of url params\n",
|
||||||
|
"intent:",
|
||||||
|
"unknown",
|
||||||
|
"\nproperties:",
|
||||||
|
expect.any(Object),
|
||||||
|
"configuration:",
|
||||||
|
expect.any(Object),
|
||||||
|
"intentAndPlatformDerivedConfiguration:",
|
||||||
|
{},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -239,6 +239,10 @@ interface IntentAndPlatformDerivedConfiguration {
|
|||||||
defaultAudioEnabled?: boolean;
|
defaultAudioEnabled?: boolean;
|
||||||
defaultVideoEnabled?: boolean;
|
defaultVideoEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
interface IntentAndPlatformDerivedConfiguration {
|
||||||
|
defaultAudioEnabled?: boolean;
|
||||||
|
defaultVideoEnabled?: 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
|
||||||
// a specific behavior (such as 'confineToRoom'), rather than one that describes
|
// a specific behavior (such as 'confineToRoom'), rather than one that describes
|
||||||
@@ -329,8 +333,12 @@ let urlParamCache: {
|
|||||||
hash?: string;
|
hash?: string;
|
||||||
params?: UrlParams;
|
params?: UrlParams;
|
||||||
} = {};
|
} = {};
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
>>>>>>> origin/livekit
|
||||||
/**
|
/**
|
||||||
* Gets the app parameters for the current URL.
|
* Gets the url params and loads them from a cache if already computed.
|
||||||
* @param search The URL search string
|
* @param search The URL search string
|
||||||
* @param hash The URL hash
|
* @param hash The URL hash
|
||||||
* @returns The app parameters encoded in the URL
|
* @returns The app parameters encoded in the URL
|
||||||
@@ -341,15 +349,26 @@ export const getUrlParams = (
|
|||||||
/** Skipping the cache might be needed in tests, to allow recomputing based on mocked platform changes. */
|
/** Skipping the cache might be needed in tests, to allow recomputing based on mocked platform changes. */
|
||||||
skipCache = false,
|
skipCache = false,
|
||||||
): UrlParams => {
|
): UrlParams => {
|
||||||
// Only run the param configuration if we do not yet have it cached for this url.
|
|
||||||
if (
|
if (
|
||||||
urlParamCache.search === search &&
|
urlParamCache.search === search &&
|
||||||
urlParamCache.hash === hash &&
|
urlParamCache.hash === hash &&
|
||||||
urlParamCache.params &&
|
urlParamCache.params
|
||||||
!skipCache
|
|
||||||
) {
|
) {
|
||||||
return urlParamCache.params;
|
return urlParamCache.params;
|
||||||
}
|
}
|
||||||
|
const params = computeUrlParams(search, hash);
|
||||||
|
urlParamCache = { search, hash, params };
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the app parameters for the current URL.
|
||||||
|
* @param search The URL search string
|
||||||
|
* @param hash The URL hash
|
||||||
|
* @returns The app parameters encoded in the URL
|
||||||
|
*/
|
||||||
|
export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||||
const parser = new ParamParser(search, hash);
|
const parser = new ParamParser(search, hash);
|
||||||
|
|
||||||
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
||||||
@@ -385,7 +404,7 @@ export const getUrlParams = (
|
|||||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||||
skipLobby: true,
|
skipLobby: true,
|
||||||
returnToLobby: false,
|
returnToLobby: false,
|
||||||
sendNotificationType: "notification" as RTCNotificationType,
|
sendNotificationType: "notification",
|
||||||
autoLeaveWhenOthersLeft: false,
|
autoLeaveWhenOthersLeft: false,
|
||||||
waitForCallPickup: false,
|
waitForCallPickup: false,
|
||||||
};
|
};
|
||||||
@@ -404,6 +423,7 @@ export const getUrlParams = (
|
|||||||
// Fall through
|
// Fall through
|
||||||
case UserIntent.StartNewCallDM:
|
case UserIntent.StartNewCallDM:
|
||||||
intentPreset.skipLobby = true;
|
intentPreset.skipLobby = true;
|
||||||
|
intentPreset.sendNotificationType = "ring";
|
||||||
intentPreset.autoLeaveWhenOthersLeft = true;
|
intentPreset.autoLeaveWhenOthersLeft = true;
|
||||||
intentPreset.waitForCallPickup = true;
|
intentPreset.waitForCallPickup = true;
|
||||||
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
||||||
@@ -526,7 +546,11 @@ export const getUrlParams = (
|
|||||||
intentAndPlatformDerivedConfiguration,
|
intentAndPlatformDerivedConfiguration,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
const params = {
|
const params = {
|
||||||
|
=======
|
||||||
|
return {
|
||||||
|
>>>>>>> origin/livekit
|
||||||
...properties,
|
...properties,
|
||||||
...intentPreset,
|
...intentPreset,
|
||||||
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ import { prefetchSounds } from "../soundUtils";
|
|||||||
import { useAudioContext } from "../useAudioContext";
|
import { useAudioContext } from "../useAudioContext";
|
||||||
import ringtoneMp3 from "../sound/ringtone.mp3?url";
|
import ringtoneMp3 from "../sound/ringtone.mp3?url";
|
||||||
import ringtoneOgg from "../sound/ringtone.ogg?url";
|
import ringtoneOgg from "../sound/ringtone.ogg?url";
|
||||||
|
import { ObservableScope } from "../state/ObservableScope.ts";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
|
|
||||||
@@ -144,8 +145,13 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
sfuConfig,
|
sfuConfig,
|
||||||
props.e2eeSystem,
|
props.e2eeSystem,
|
||||||
);
|
);
|
||||||
const connStateObservable$ = useObservable(
|
const observableScope = useInitial(() => new ObservableScope());
|
||||||
(inputs$) => inputs$.pipe(map(([connState]) => connState)),
|
const connStateBehavior$ = useObservable(
|
||||||
|
(inputs$) =>
|
||||||
|
observableScope.behavior(
|
||||||
|
inputs$.pipe(map(([connState]) => connState)),
|
||||||
|
connState,
|
||||||
|
),
|
||||||
[connState],
|
[connState],
|
||||||
);
|
);
|
||||||
const [vm, setVm] = useState<CallViewModel | null>(null);
|
const [vm, setVm] = useState<CallViewModel | null>(null);
|
||||||
@@ -188,7 +194,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
waitForCallPickup:
|
waitForCallPickup:
|
||||||
waitForCallPickup && sendNotificationType === "ring",
|
waitForCallPickup && sendNotificationType === "ring",
|
||||||
},
|
},
|
||||||
connStateObservable$,
|
connStateBehavior$,
|
||||||
reactionsReader.raisedHands$,
|
reactionsReader.raisedHands$,
|
||||||
reactionsReader.reactions$,
|
reactionsReader.reactions$,
|
||||||
);
|
);
|
||||||
@@ -204,7 +210,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
livekitRoom,
|
livekitRoom,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
props.e2eeSystem,
|
props.e2eeSystem,
|
||||||
connStateObservable$,
|
connStateBehavior$,
|
||||||
autoLeaveWhenOthersLeft,
|
autoLeaveWhenOthersLeft,
|
||||||
sendNotificationType,
|
sendNotificationType,
|
||||||
waitForCallPickup,
|
waitForCallPickup,
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent;
|
|||||||
interface CallViewModelInputs {
|
interface CallViewModelInputs {
|
||||||
remoteParticipants$: Behavior<RemoteParticipant[]>;
|
remoteParticipants$: Behavior<RemoteParticipant[]>;
|
||||||
rtcMembers$: Behavior<Partial<CallMembership>[]>;
|
rtcMembers$: Behavior<Partial<CallMembership>[]>;
|
||||||
connectionState$: Observable<ECConnectionState>;
|
livekitConnectionState$: Behavior<ECConnectionState>;
|
||||||
speaking: Map<Participant, Observable<boolean>>;
|
speaking: Map<Participant, Observable<boolean>>;
|
||||||
mediaDevices: MediaDevices;
|
mediaDevices: MediaDevices;
|
||||||
initialSyncState: SyncState;
|
initialSyncState: SyncState;
|
||||||
@@ -276,7 +276,9 @@ function withCallViewModel(
|
|||||||
{
|
{
|
||||||
remoteParticipants$ = constant([]),
|
remoteParticipants$ = constant([]),
|
||||||
rtcMembers$ = constant([localRtcMember]),
|
rtcMembers$ = constant([localRtcMember]),
|
||||||
connectionState$ = of(ConnectionState.Connected),
|
livekitConnectionState$: connectionState$ = constant(
|
||||||
|
ConnectionState.Connected,
|
||||||
|
),
|
||||||
speaking = new Map(),
|
speaking = new Map(),
|
||||||
mediaDevices = mockMediaDevices({}),
|
mediaDevices = mockMediaDevices({}),
|
||||||
initialSyncState = SyncState.Syncing,
|
initialSyncState = SyncState.Syncing,
|
||||||
@@ -384,7 +386,7 @@ test("participants are retained during a focus switch", () => {
|
|||||||
b: [],
|
b: [],
|
||||||
}),
|
}),
|
||||||
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
||||||
connectionState$: behavior(connectionInputMarbles, {
|
livekitConnectionState$: behavior(connectionInputMarbles, {
|
||||||
c: ConnectionState.Connected,
|
c: ConnectionState.Connected,
|
||||||
s: ECAddonConnectionState.ECSwitchingFocus,
|
s: ECAddonConnectionState.ECSwitchingFocus,
|
||||||
}),
|
}),
|
||||||
@@ -1251,6 +1253,41 @@ describe("waitForCallPickup$", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("regression test: does stop ringing in case livekitConnectionState$ emits after didSendCallNotification$ has already emitted", () => {
|
||||||
|
withTestScheduler(({ schedule, expectObservable, behavior }) => {
|
||||||
|
withCallViewModel(
|
||||||
|
{
|
||||||
|
livekitConnectionState$: behavior("d 9ms c", {
|
||||||
|
d: ConnectionState.Disconnected,
|
||||||
|
c: ConnectionState.Connected,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
(vm, rtcSession) => {
|
||||||
|
// Fire a call notification IMMEDIATELY (its important for this test, that this happens before the livekitConnectionState$ emits)
|
||||||
|
schedule("n", {
|
||||||
|
n: () => {
|
||||||
|
rtcSession.emit(
|
||||||
|
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||||
|
mockRingEvent("$notif1", 30),
|
||||||
|
mockLegacyRingEvent,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expectObservable(vm.callPickupState$).toBe("a 9ms b 19ms c", {
|
||||||
|
a: "unknown",
|
||||||
|
b: "ringing",
|
||||||
|
c: "timeout",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
waitForCallPickup: true,
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("ringing -> success if someone joins before timeout", () => {
|
test("ringing -> success if someone joins before timeout", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
// Someone joins at 20ms (both LiveKit participant and MatrixRTC member)
|
// Someone joins at 20ms (both LiveKit participant and MatrixRTC member)
|
||||||
@@ -1305,7 +1342,7 @@ describe("waitForCallPickup$", () => {
|
|||||||
a: [localRtcMember],
|
a: [localRtcMember],
|
||||||
b: [localRtcMember, aliceRtcMember],
|
b: [localRtcMember, aliceRtcMember],
|
||||||
}),
|
}),
|
||||||
connectionState$,
|
livekitConnectionState$: connectionState$,
|
||||||
},
|
},
|
||||||
(vm, rtcSession) => {
|
(vm, rtcSession) => {
|
||||||
// Notify at 5ms so we enter ringing, then get disconnected 5ms later
|
// Notify at 5ms so we enter ringing, then get disconnected 5ms later
|
||||||
|
|||||||
@@ -880,68 +880,77 @@ export class CallViewModel extends ViewModel {
|
|||||||
? this.allOthersLeft$
|
? this.allOthersLeft$
|
||||||
: NEVER;
|
: NEVER;
|
||||||
|
|
||||||
|
private readonly didSendCallNotification$ = fromEvent(
|
||||||
|
this.matrixRTCSession,
|
||||||
|
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||||
|
) as Observable<
|
||||||
|
Parameters<
|
||||||
|
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
|
||||||
|
>
|
||||||
|
>;
|
||||||
/**
|
/**
|
||||||
* Whenever the RTC session tells us that it intends to ring the remote
|
* Whenever the RTC session tells us that it intends to ring the remote
|
||||||
* participant's devices, this emits an Observable tracking the current state of
|
* participant's devices, this emits an Observable tracking the current state of
|
||||||
* that ringing process.
|
* that ringing process.
|
||||||
*/
|
*/
|
||||||
private readonly ring$: Observable<
|
// This is a behavior since we need to store the latest state for when we subscribe to this after `didSendCallNotification$`
|
||||||
Observable<"ringing" | "timeout" | "decline">
|
// has already emitted but we still need the latest observable with a timeout timer that only gets created on after receiving `notificationEvent`.
|
||||||
> = (
|
// A behavior will emit the latest observable with the running timer to new subscribers.
|
||||||
fromEvent(
|
// see also: callPickupState$ and in particular the line: `return this.ring$.pipe(mergeAll());` here we otherwise might get an EMPTY observable if
|
||||||
this.matrixRTCSession,
|
// `ring$` would not be a behavior.
|
||||||
MatrixRTCSessionEvent.DidSendCallNotification,
|
private readonly ring$: Behavior<"ringing" | "timeout" | "decline" | null> =
|
||||||
) as Observable<
|
this.scope.behavior(
|
||||||
Parameters<
|
this.didSendCallNotification$.pipe(
|
||||||
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
|
filter(
|
||||||
>
|
([notificationEvent]) =>
|
||||||
>
|
notificationEvent.notification_type === "ring",
|
||||||
).pipe(
|
|
||||||
filter(
|
|
||||||
([notificationEvent]) => notificationEvent.notification_type === "ring",
|
|
||||||
),
|
|
||||||
map(([notificationEvent]) => {
|
|
||||||
const lifetimeMs = notificationEvent?.lifetime ?? 0;
|
|
||||||
return concat(
|
|
||||||
lifetimeMs === 0
|
|
||||||
? // If no lifetime, skip the ring state
|
|
||||||
EMPTY
|
|
||||||
: // Ring until lifetime ms have passed
|
|
||||||
timer(lifetimeMs).pipe(
|
|
||||||
ignoreElements(),
|
|
||||||
startWith("ringing" as const),
|
|
||||||
),
|
|
||||||
// The notification lifetime has timed out, meaning ringing has likely
|
|
||||||
// stopped on all receiving clients.
|
|
||||||
of("timeout" as const),
|
|
||||||
NEVER,
|
|
||||||
).pipe(
|
|
||||||
takeUntil(
|
|
||||||
(
|
|
||||||
fromEvent(this.matrixRoom, RoomEvent.Timeline) as Observable<
|
|
||||||
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
|
|
||||||
>
|
|
||||||
).pipe(
|
|
||||||
filter(
|
|
||||||
([event]) =>
|
|
||||||
event.getType() === EventType.RTCDecline &&
|
|
||||||
event.getRelation()?.rel_type === "m.reference" &&
|
|
||||||
event.getRelation()?.event_id === notificationEvent.event_id &&
|
|
||||||
event.getSender() !== this.userId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
endWith("decline" as const),
|
switchMap(([notificationEvent]) => {
|
||||||
);
|
const lifetimeMs = notificationEvent?.lifetime ?? 0;
|
||||||
}),
|
return concat(
|
||||||
);
|
lifetimeMs === 0
|
||||||
|
? // If no lifetime, skip the ring state
|
||||||
|
of(null)
|
||||||
|
: // Ring until lifetime ms have passed
|
||||||
|
timer(lifetimeMs).pipe(
|
||||||
|
ignoreElements(),
|
||||||
|
startWith("ringing" as const),
|
||||||
|
),
|
||||||
|
// The notification lifetime has timed out, meaning ringing has likely
|
||||||
|
// stopped on all receiving clients.
|
||||||
|
of("timeout" as const),
|
||||||
|
// This makes sure we will not drop into the `endWith("decline" as const)` state
|
||||||
|
NEVER,
|
||||||
|
).pipe(
|
||||||
|
takeUntil(
|
||||||
|
(
|
||||||
|
fromEvent(this.matrixRoom, RoomEvent.Timeline) as Observable<
|
||||||
|
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
|
||||||
|
>
|
||||||
|
).pipe(
|
||||||
|
filter(
|
||||||
|
([event]) =>
|
||||||
|
event.getType() === EventType.RTCDecline &&
|
||||||
|
event.getRelation()?.rel_type === "m.reference" &&
|
||||||
|
event.getRelation()?.event_id ===
|
||||||
|
notificationEvent.event_id &&
|
||||||
|
event.getSender() !== this.userId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
endWith("decline" as const),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether some Matrix user other than ourself is joined to the call.
|
* Whether some Matrix user other than ourself is joined to the call.
|
||||||
*/
|
*/
|
||||||
private readonly someoneElseJoined$ = this.memberships$.pipe(
|
private readonly someoneElseJoined$ = this.memberships$.pipe(
|
||||||
map((ms) => ms.some((m) => m.sender !== this.userId)),
|
map((ms) => ms.some((m) => m.sender !== this.userId)),
|
||||||
);
|
) as Behavior<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current call pickup state of the call.
|
* The current call pickup state of the call.
|
||||||
@@ -960,25 +969,19 @@ export class CallViewModel extends ViewModel {
|
|||||||
? this.scope.behavior<
|
? this.scope.behavior<
|
||||||
"unknown" | "ringing" | "timeout" | "decline" | "success"
|
"unknown" | "ringing" | "timeout" | "decline" | "success"
|
||||||
>(
|
>(
|
||||||
combineLatest([
|
combineLatest(
|
||||||
this.livekitConnectionState$,
|
[this.livekitConnectionState$, this.someoneElseJoined$, this.ring$],
|
||||||
this.someoneElseJoined$,
|
(livekitConnectionState, someoneElseJoined, ring) => {
|
||||||
]).pipe(
|
|
||||||
switchMap(([livekitConnectionState, someoneElseJoined]) => {
|
|
||||||
if (livekitConnectionState === ConnectionState.Disconnected) {
|
if (livekitConnectionState === ConnectionState.Disconnected) {
|
||||||
// Do not ring until we're connected.
|
// Do not ring until we're connected.
|
||||||
return of("unknown" as const);
|
return "unknown" as const;
|
||||||
} else if (someoneElseJoined) {
|
} else if (someoneElseJoined) {
|
||||||
return of("success" as const);
|
return "success" as const;
|
||||||
}
|
}
|
||||||
// Show the ringing state of the most recent ringing attempt.
|
// Show the ringing state of the most recent ringing attempt.
|
||||||
return this.ring$.pipe(switchAll());
|
// as long as we have not yet sent an RTC notification event, ring will be null -> callPickupState$ = unknown.
|
||||||
}),
|
return ring ?? ("unknown" as const);
|
||||||
// The state starts as 'unknown' because we don't know if the RTC
|
},
|
||||||
// session will actually send a notify event yet. It will only be
|
|
||||||
// known once we send our own membership and see that we were the
|
|
||||||
// first one to join.
|
|
||||||
startWith("unknown" as const),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: constant(null);
|
: constant(null);
|
||||||
@@ -1690,7 +1693,11 @@ export class CallViewModel extends ViewModel {
|
|||||||
private readonly livekitRoom: LivekitRoom,
|
private readonly livekitRoom: LivekitRoom,
|
||||||
private readonly mediaDevices: MediaDevices,
|
private readonly mediaDevices: MediaDevices,
|
||||||
private readonly options: CallViewModelOptions,
|
private readonly options: CallViewModelOptions,
|
||||||
|
<<<<<<< HEAD
|
||||||
public readonly livekitConnectionState$: Observable<ECConnectionState>,
|
public readonly livekitConnectionState$: Observable<ECConnectionState>,
|
||||||
|
=======
|
||||||
|
public readonly livekitConnectionState$: Behavior<ECConnectionState>,
|
||||||
|
>>>>>>> origin/livekit
|
||||||
private readonly handsRaisedSubject$: Observable<
|
private readonly handsRaisedSubject$: Observable<
|
||||||
Record<string, RaisedHandInfo>
|
Record<string, RaisedHandInfo>
|
||||||
>,
|
>,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
localRtcMember,
|
localRtcMember,
|
||||||
} from "./test-fixtures";
|
} from "./test-fixtures";
|
||||||
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||||
|
import { constant } from "../state/Behavior";
|
||||||
|
|
||||||
export function getBasicRTCSession(
|
export function getBasicRTCSession(
|
||||||
members: RoomMember[],
|
members: RoomMember[],
|
||||||
@@ -154,7 +155,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
...callViewModelOptions,
|
...callViewModelOptions,
|
||||||
},
|
},
|
||||||
of(ConnectionState.Connected),
|
constant(ConnectionState.Connected),
|
||||||
handRaisedSubject$,
|
handRaisedSubject$,
|
||||||
reactionsSubject$,
|
reactionsSubject$,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user