mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-23 20:59:21 +00:00
Compare commits
10 Commits
robin/noti
...
robin/noti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64aec47b50 | ||
|
|
f39dbf5e87 | ||
|
|
55923a8d5e | ||
|
|
a127f73155 | ||
|
|
5af3537454 | ||
|
|
46698055e6 | ||
|
|
7ffa18f9df | ||
|
|
0ea737a34d | ||
|
|
d19d221fd0 | ||
|
|
f1c2945eff |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,7 +9,6 @@ dist-ssr
|
||||
public/config.json
|
||||
backend/synapse_tmp/*
|
||||
/coverage
|
||||
config.json
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"default": "Standard",
|
||||
"default_named": "Standard<2> ({{name}} )</2>",
|
||||
"handset": "Ohrhörer",
|
||||
"earpiece": "Ohrhörer",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon{{n}}",
|
||||
"speaker": "Lautsprecher",
|
||||
|
||||
@@ -79,6 +79,11 @@
|
||||
"use_to_device_key_transport": "Use to device key transport. This will fallback to room key transport when another call member sent a room key"
|
||||
},
|
||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||
"earpiece": {
|
||||
"overlay_back_button": "Back to Speaker Mode",
|
||||
"overlay_description": "Only works while using app",
|
||||
"overlay_title": "Earpiece Mode"
|
||||
},
|
||||
"error": {
|
||||
"call_is_not_supported": "Call is not supported",
|
||||
"call_not_found": "Call not found",
|
||||
@@ -105,11 +110,6 @@
|
||||
"knock_reject_heading": "Access denied",
|
||||
"reason": "Reason: {{reason}}"
|
||||
},
|
||||
"handset": {
|
||||
"overlay_back_button": "Back to Speaker Mode",
|
||||
"overlay_description": "Only works while using app",
|
||||
"overlay_title": "Handset Mode"
|
||||
},
|
||||
"hangup_button_label": "End call",
|
||||
"header_label": "Element Call Home",
|
||||
"header_participants_label": "Participants",
|
||||
@@ -181,7 +181,7 @@
|
||||
"change_device_button": "Change audio device",
|
||||
"default": "Default",
|
||||
"default_named": "Default <2>({{name}})</2>",
|
||||
"handset": "Handset",
|
||||
"earpiece": "Earpiece",
|
||||
"loudspeaker": "Loudspeaker",
|
||||
"microphone": "Microphone",
|
||||
"microphone_numbered": "Microphone {{n}}",
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vector-im/compound-design-tokens": "^5.0.0",
|
||||
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||
"@vector-im/compound-web": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { Heading, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import { CollapseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CollapseIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Header, LeftNav, RightNav } from "./Header";
|
||||
@@ -42,6 +45,7 @@ interface Props {
|
||||
*/
|
||||
export const AppBar: FC<Props> = ({ children }) => {
|
||||
const { t } = useTranslation();
|
||||
const BackIcon = platform === "ios" ? CollapseIcon : ArrowLeftIcon;
|
||||
const onBackClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
window.controls.onBackButtonPressed?.();
|
||||
@@ -65,7 +69,7 @@ export const AppBar: FC<Props> = ({ children }) => {
|
||||
<LeftNav>
|
||||
<Tooltip label={t("common.back")}>
|
||||
<IconButton onClick={onBackClick}>
|
||||
<CollapseIcon />
|
||||
<BackIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</LeftNav>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getRoomIdentifierFromUrl,
|
||||
getUrlParams,
|
||||
HeaderStyle,
|
||||
UserIntent,
|
||||
} from "../src/UrlParams";
|
||||
|
||||
const ROOM_NAME = "roomNameHere";
|
||||
@@ -82,16 +82,6 @@ describe("UrlParams", () => {
|
||||
getRoomIdentifierFromUrl("", `?roomId=${ROOM_ID}`, "").roomId,
|
||||
).toBe(ROOM_ID);
|
||||
});
|
||||
it("(roomId with unprintable characters)", () => {
|
||||
const invisibleChar = "\u2066";
|
||||
expect(
|
||||
getRoomIdentifierFromUrl(
|
||||
"",
|
||||
`?roomId=${invisibleChar}${ROOM_ID}${invisibleChar}`,
|
||||
"",
|
||||
).roomId,
|
||||
).toBe(ROOM_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores room alias", () => {
|
||||
@@ -211,68 +201,24 @@ describe("UrlParams", () => {
|
||||
});
|
||||
|
||||
describe("intent", () => {
|
||||
const noIntentDefaults = {
|
||||
confineToRoom: false,
|
||||
appPrompt: true,
|
||||
preload: false,
|
||||
header: HeaderStyle.Standard,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
allowIceFallback: false,
|
||||
perParticipantE2EE: false,
|
||||
controlledAudioDevices: false,
|
||||
skipLobby: false,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: undefined,
|
||||
};
|
||||
const startNewCallDefaults = (platform: string): object => ({
|
||||
confineToRoom: true,
|
||||
appPrompt: false,
|
||||
preload: true,
|
||||
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
allowIceFallback: true,
|
||||
perParticipantE2EE: true,
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
skipLobby: true,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification",
|
||||
});
|
||||
const joinExistingCallDefaults = (platform: string): object => ({
|
||||
confineToRoom: true,
|
||||
appPrompt: false,
|
||||
preload: true,
|
||||
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
allowIceFallback: true,
|
||||
perParticipantE2EE: true,
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
skipLobby: false,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification",
|
||||
});
|
||||
it("use no-intent-defaults with unknown intent", () => {
|
||||
expect(getUrlParams()).toMatchObject(noIntentDefaults);
|
||||
it("defaults to unknown", () => {
|
||||
expect(getUrlParams().intent).toBe(UserIntent.Unknown);
|
||||
});
|
||||
|
||||
it("ignores intent if it is not a valid value", () => {
|
||||
expect(getUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||
expect(getUrlParams("?intent=foo").intent).toBe(UserIntent.Unknown);
|
||||
});
|
||||
|
||||
it("accepts start_call", () => {
|
||||
expect(
|
||||
getUrlParams("?intent=start_call&widgetId=1234&parentUrl=parent.org"),
|
||||
).toMatchObject(startNewCallDefaults("desktop"));
|
||||
expect(getUrlParams("?intent=start_call").intent).toBe(
|
||||
UserIntent.StartNewCall,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts join_existing", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?intent=join_existing&widgetId=1234&parentUrl=parent.org",
|
||||
),
|
||||
).toMatchObject(joinExistingCallDefaults("desktop"));
|
||||
expect(getUrlParams("?intent=join_existing").intent).toBe(
|
||||
UserIntent.JoinExistingCall,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -304,5 +250,9 @@ describe("UrlParams", () => {
|
||||
);
|
||||
expect(getUrlParams("?header=none&hideHeader=false").header).toBe("none");
|
||||
});
|
||||
it("converts hideHeader to the correct header value", () => {
|
||||
expect(getUrlParams("?hideHeader=true").header).toBe("none");
|
||||
expect(getUrlParams("?hideHeader=false").header).toBe("standard");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
353
src/UrlParams.ts
353
src/UrlParams.ts
@@ -9,12 +9,10 @@ import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { type RTCNotificationType } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { pickBy } from "lodash-es";
|
||||
|
||||
import { Config } from "./config/Config";
|
||||
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "./e2ee/e2eeType";
|
||||
import { platform } from "./Platform";
|
||||
|
||||
interface RoomIdentifier {
|
||||
roomAlias: string | null;
|
||||
@@ -23,7 +21,6 @@ interface RoomIdentifier {
|
||||
}
|
||||
|
||||
export enum UserIntent {
|
||||
// TODO: add DM vs room call
|
||||
StartNewCall = "start_call",
|
||||
JoinExistingCall = "join_existing",
|
||||
Unknown = "unknown",
|
||||
@@ -35,12 +32,12 @@ export enum HeaderStyle {
|
||||
AppBar = "app_bar",
|
||||
}
|
||||
|
||||
/**
|
||||
* The UrlProperties are used to pass required data to the widget.
|
||||
* Those are different in different rooms, users, devices. They do not configure the behavior of the
|
||||
* widget but provide the required data to the widget.
|
||||
*/
|
||||
export interface UrlProperties {
|
||||
// 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
|
||||
// the situations that call for this behavior ('isEmbedded'). This makes it
|
||||
// clearer what each flag means, and helps us avoid coupling Element Call's
|
||||
// behavior to the needs of specific consumers.
|
||||
export interface UrlParams {
|
||||
// Widget api related params
|
||||
widgetId: string | null;
|
||||
parentUrl: string | null;
|
||||
@@ -52,11 +49,45 @@ export interface UrlProperties {
|
||||
* is also not validated, where it is in useRoomIdentifier().
|
||||
*/
|
||||
roomId: string | null;
|
||||
/**
|
||||
* Whether the app should keep the user confined to the current call/room.
|
||||
*/
|
||||
confineToRoom: boolean;
|
||||
/**
|
||||
* Whether upon entering a room, the user should be prompted to launch the
|
||||
* native mobile app. (Affects only Android and iOS.)
|
||||
*
|
||||
* The app prompt must also be enabled in the config for this to take effect.
|
||||
*/
|
||||
appPrompt: boolean;
|
||||
/**
|
||||
* Whether the app should pause before joining the call until it sees an
|
||||
* io.element.join widget action, allowing it to be preloaded.
|
||||
*/
|
||||
preload: boolean;
|
||||
/**
|
||||
* 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: HeaderStyle;
|
||||
/**
|
||||
* Whether the controls should be shown. For screen recording no controls can be desired.
|
||||
*/
|
||||
showControls: boolean;
|
||||
/**
|
||||
* Whether to hide the screen-sharing button.
|
||||
*/
|
||||
hideScreensharing: boolean;
|
||||
/**
|
||||
* Whether to use end-to-end encryption.
|
||||
*/
|
||||
e2eEnabled: boolean;
|
||||
/**
|
||||
* The user's ID (only used in matryoshka mode).
|
||||
*/
|
||||
userId: string | null;
|
||||
|
||||
/**
|
||||
* The display name to use for auto-registration.
|
||||
*/
|
||||
@@ -93,97 +124,15 @@ export interface UrlProperties {
|
||||
* The Posthog API key. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
posthogApiKey: string | null;
|
||||
/**
|
||||
* Whether to use end-to-end encryption.
|
||||
*/
|
||||
e2eEnabled: boolean;
|
||||
/**
|
||||
* E2EE password
|
||||
*/
|
||||
password: string | null;
|
||||
/** This defines the homeserver that is going to be used when joining a room.
|
||||
* It has to be set to a non default value for links to rooms
|
||||
* that are not on the default homeserver,
|
||||
* that is in use for the current user.
|
||||
*/
|
||||
viaServers: string | null;
|
||||
|
||||
/**
|
||||
* This defines the homeserver that is going to be used when registering
|
||||
* a new (guest) user.
|
||||
* This can be user to configure a non default guest user server when
|
||||
* creating a spa link.
|
||||
*/
|
||||
homeserver: string | null;
|
||||
|
||||
/**
|
||||
* The rageshake submit URL. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
rageshakeSubmitUrl: string | null;
|
||||
|
||||
/**
|
||||
* The Sentry DSN. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
sentryDsn: string | null;
|
||||
|
||||
/**
|
||||
* The Sentry environment. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
sentryEnvironment: string | null;
|
||||
/**
|
||||
* The theme to use for element call.
|
||||
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
||||
*/
|
||||
theme: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration for the app, which can be set via URL parameters.
|
||||
* Those property are different to the UrlProperties, since they are all optional
|
||||
* and configure the behavior of the app. Their value is the same if EC is used in
|
||||
* the same context but with different accounts/users.
|
||||
*
|
||||
* Their defaults can be controlled by the `intent` property.
|
||||
*/
|
||||
export interface UrlConfiguration {
|
||||
/**
|
||||
* Whether the app should keep the user confined to the current call/room.
|
||||
*/
|
||||
confineToRoom: boolean;
|
||||
/**
|
||||
* Whether upon entering a room, the user should be prompted to launch the
|
||||
* native mobile app. (Affects only Android and iOS.)
|
||||
*
|
||||
* The app prompt must also be enabled in the config for this to take effect.
|
||||
*/
|
||||
appPrompt: boolean;
|
||||
/**
|
||||
* Whether the app should pause before joining the call until it sees an
|
||||
* io.element.join widget action, allowing it to be preloaded.
|
||||
*/
|
||||
preload: boolean;
|
||||
/**
|
||||
* 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: HeaderStyle;
|
||||
/**
|
||||
* Whether the controls should be shown. For screen recording no controls can be desired.
|
||||
*/
|
||||
showControls: boolean;
|
||||
/**
|
||||
* Whether to hide the screen-sharing button.
|
||||
*/
|
||||
hideScreensharing: boolean;
|
||||
|
||||
/**
|
||||
* Whether the app is allowed to use fallback STUN servers for ICE in case the
|
||||
* user's homeserver doesn't provide any.
|
||||
*/
|
||||
allowIceFallback: boolean;
|
||||
|
||||
/**
|
||||
* E2EE password
|
||||
*/
|
||||
password: string | null;
|
||||
/**
|
||||
* Whether the app should use per participant keys for E2EE.
|
||||
*/
|
||||
@@ -205,19 +154,52 @@ export interface UrlConfiguration {
|
||||
* This is useful for video rooms.
|
||||
*/
|
||||
returnToLobby: boolean;
|
||||
/**
|
||||
* The theme to use for element call.
|
||||
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
||||
*/
|
||||
theme: string | null;
|
||||
/** This defines the homeserver that is going to be used when joining a room.
|
||||
* It has to be set to a non default value for links to rooms
|
||||
* that are not on the default homeserver,
|
||||
* that is in use for the current user.
|
||||
*/
|
||||
viaServers: string | null;
|
||||
/**
|
||||
* This defines the homeserver that is going to be used when registering
|
||||
* a new (guest) user.
|
||||
* This can be user to configure a non default guest user server when
|
||||
* creating a spa link.
|
||||
*/
|
||||
homeserver: string | null;
|
||||
|
||||
/**
|
||||
* The user's intent 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`.
|
||||
*/
|
||||
intent: string | null;
|
||||
|
||||
/**
|
||||
* The rageshake submit URL. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
rageshakeSubmitUrl: string | null;
|
||||
|
||||
/**
|
||||
* The Sentry DSN. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
sentryDsn: string | null;
|
||||
|
||||
/**
|
||||
* The Sentry environment. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
sentryEnvironment: string | null;
|
||||
/**
|
||||
* Whether and what type of notification EC should send, when the user joins the call.
|
||||
*/
|
||||
sendNotificationType?: RTCNotificationType;
|
||||
}
|
||||
|
||||
// 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
|
||||
// the situations that call for this behavior ('isEmbedded'). This makes it
|
||||
// clearer what each flag means, and helps us avoid coupling Element Call's
|
||||
// behavior to the needs of specific consumers.
|
||||
export interface UrlParams extends UrlProperties, UrlConfiguration {}
|
||||
|
||||
// This is here as a stopgap, but what would be far nicer is a function that
|
||||
// takes a UrlParams and returns a query string. That would enable us to
|
||||
// consolidate all the data about URL parameters and their meanings to this one
|
||||
@@ -258,17 +240,6 @@ class ParamParser {
|
||||
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
|
||||
}
|
||||
|
||||
public getEnumParam<T extends string>(
|
||||
name: string,
|
||||
type: { [s: string]: T } | ArrayLike<T>,
|
||||
): T | undefined {
|
||||
const value = this.getParam(name);
|
||||
if (value !== null && Object.values(type).includes(value as T)) {
|
||||
return value as T;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getAllParams(name: string): string[] {
|
||||
return [
|
||||
...this.fragmentParams.getAll(name),
|
||||
@@ -280,10 +251,6 @@ class ParamParser {
|
||||
const param = this.getParam(name);
|
||||
return param === null ? defaultValue : param !== "false";
|
||||
}
|
||||
public getFlag(name: string): boolean | undefined {
|
||||
const param = this.getParam(name);
|
||||
return param !== null ? param !== "false" : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,79 +267,46 @@ export const getUrlParams = (
|
||||
|
||||
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
||||
|
||||
let intent = parser.getParam("intent");
|
||||
if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) {
|
||||
intent = UserIntent.Unknown;
|
||||
}
|
||||
|
||||
// Check hideHeader for backwards compatibility. If header is set, hideHeader
|
||||
// is ignored.
|
||||
const header =
|
||||
parser.getParam("header") ??
|
||||
(parser.getFlagParam("hideHeader")
|
||||
? HeaderStyle.None
|
||||
: HeaderStyle.Standard);
|
||||
|
||||
const sendNotificationType = ["ring", "notification"].includes(
|
||||
parser.getParam("sendNotificationType") ?? "",
|
||||
)
|
||||
? (parser.getParam("sendNotificationType") as RTCNotificationType)
|
||||
: undefined;
|
||||
const widgetId = parser.getParam("widgetId");
|
||||
const parentUrl = parser.getParam("parentUrl");
|
||||
const isWidget = !!widgetId && !!parentUrl;
|
||||
|
||||
/**
|
||||
* The user's intent 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`.
|
||||
* This is a platform specific default set of parameters, that allows to minize the configuration
|
||||
* needed to start a call. And empowers the EC codebase to control the platform/intent behavior in
|
||||
* a central place.
|
||||
*
|
||||
* In short: either provide url query parameters of UrlConfiguration or set the intent
|
||||
* (or the global defaults will be used).
|
||||
*/
|
||||
const intent = !isWidget
|
||||
? UserIntent.Unknown
|
||||
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
|
||||
// Here we only use constants and `platform` to determine the intent preset.
|
||||
let intentPreset: UrlConfiguration;
|
||||
const inAppDefault = {
|
||||
confineToRoom: true,
|
||||
appPrompt: false,
|
||||
preload: true,
|
||||
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
allowIceFallback: true,
|
||||
perParticipantE2EE: true,
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
skipLobby: true,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification" as RTCNotificationType,
|
||||
};
|
||||
switch (intent) {
|
||||
case UserIntent.StartNewCall:
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: true,
|
||||
};
|
||||
break;
|
||||
case UserIntent.JoinExistingCall:
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: false,
|
||||
};
|
||||
break;
|
||||
// Non widget usecase defaults
|
||||
default:
|
||||
intentPreset = {
|
||||
confineToRoom: false,
|
||||
appPrompt: true,
|
||||
preload: false,
|
||||
header: HeaderStyle.Standard,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
allowIceFallback: false,
|
||||
perParticipantE2EE: false,
|
||||
controlledAudioDevices: false,
|
||||
skipLobby: false,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const properties: UrlProperties = {
|
||||
return {
|
||||
widgetId,
|
||||
parentUrl,
|
||||
|
||||
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl:
|
||||
// what would we do if it were invalid? If the widget API says that's what
|
||||
// the room ID is, then that's what it is.
|
||||
roomId: parser.getParam("roomId"),
|
||||
password: parser.getParam("password"),
|
||||
// This flag has 'embed' as an alias for historical reasons
|
||||
confineToRoom:
|
||||
parser.getFlagParam("confineToRoom") || parser.getFlagParam("embed"),
|
||||
appPrompt: parser.getFlagParam("appPrompt", true),
|
||||
preload: isWidget ? parser.getFlagParam("preload") : false,
|
||||
header: header as HeaderStyle,
|
||||
showControls: parser.getFlagParam("showControls", true),
|
||||
hideScreensharing: parser.getFlagParam("hideScreensharing"),
|
||||
e2eEnabled: parser.getFlagParam("enableE2EE", true),
|
||||
userId: isWidget ? parser.getParam("userId") : null,
|
||||
displayName: parser.getParam("displayName"),
|
||||
deviceId: isWidget ? parser.getParam("deviceId") : null,
|
||||
@@ -380,9 +314,24 @@ export const getUrlParams = (
|
||||
lang: parser.getParam("lang"),
|
||||
fonts: parser.getAllParams("font"),
|
||||
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
||||
allowIceFallback: parser.getFlagParam("allowIceFallback"),
|
||||
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
|
||||
controlledAudioDevices: parser.getFlagParam(
|
||||
"controlledAudioDevices",
|
||||
// the deprecated property name
|
||||
parser.getFlagParam("controlledMediaDevices"),
|
||||
),
|
||||
skipLobby: parser.getFlagParam(
|
||||
"skipLobby",
|
||||
isWidget && intent === UserIntent.StartNewCall,
|
||||
),
|
||||
// In SPA mode the user should always exit to the home screen when hanging
|
||||
// up, rather than being sent back to the lobby
|
||||
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : false,
|
||||
theme: parser.getParam("theme"),
|
||||
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
||||
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
||||
intent,
|
||||
posthogApiHost: parser.getParam("posthogApiHost"),
|
||||
posthogApiKey: parser.getParam("posthogApiKey"),
|
||||
posthogUserId:
|
||||
@@ -390,35 +339,7 @@ export const getUrlParams = (
|
||||
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
|
||||
sentryDsn: parser.getParam("sentryDsn"),
|
||||
sentryEnvironment: parser.getParam("sentryEnvironment"),
|
||||
e2eEnabled: parser.getFlagParam("enableE2EE", true),
|
||||
};
|
||||
|
||||
const configuration: Partial<UrlConfiguration> = {
|
||||
confineToRoom: parser.getFlag("confineToRoom"),
|
||||
appPrompt: parser.getFlag("appPrompt"),
|
||||
preload: isWidget ? parser.getFlag("preload") : undefined,
|
||||
// Check hideHeader for backwards compatibility. If header is set, hideHeader
|
||||
// is ignored.
|
||||
header: parser.getEnumParam("header", HeaderStyle),
|
||||
showControls: parser.getFlag("showControls"),
|
||||
hideScreensharing: parser.getFlag("hideScreensharing"),
|
||||
allowIceFallback: parser.getFlag("allowIceFallback"),
|
||||
perParticipantE2EE: parser.getFlag("perParticipantE2EE"),
|
||||
controlledAudioDevices: parser.getFlag("controlledAudioDevices"),
|
||||
skipLobby: isWidget ? parser.getFlag("skipLobby") : false,
|
||||
// In SPA mode the user should always exit to the home screen when hanging
|
||||
// up, rather than being sent back to the lobby
|
||||
returnToLobby: isWidget ? parser.getFlag("returnToLobby") : false,
|
||||
sendNotificationType: parser.getEnumParam("sendNotificationType", [
|
||||
"ring",
|
||||
"notification",
|
||||
]),
|
||||
};
|
||||
|
||||
return {
|
||||
...properties,
|
||||
...intentPreset,
|
||||
...pickBy(configuration, (v) => v !== undefined),
|
||||
sendNotificationType,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -477,16 +398,10 @@ export function getRoomIdentifierFromUrl(
|
||||
|
||||
// Make sure roomId is valid
|
||||
let roomId: string | null = parser.getParam("roomId");
|
||||
if (roomId !== null) {
|
||||
// Replace any non-printable characters that another client may have inserted.
|
||||
// For instance on iOS, some copied links end up with zero width characters on the end which get encoded into the URL.
|
||||
// This isn't valid for a roomId, so we can freely strip the content.
|
||||
roomId = roomId.replaceAll(/^[^ -~]+|[^ -~]+$/g, "");
|
||||
if (!roomId.startsWith("!")) {
|
||||
roomId = null;
|
||||
} else if (!roomId.includes("")) {
|
||||
roomId = null;
|
||||
}
|
||||
if (!roomId?.startsWith("!")) {
|
||||
roomId = null;
|
||||
} else if (!roomId.includes("")) {
|
||||
roomId = null;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -32,7 +32,7 @@ exports[`AppBar > renders 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z"
|
||||
d="M12.207 5.293a1 1 0 0 1 0 1.414L7.914 11H18.5a1 1 0 1 1 0 2H7.914l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6a1 1 0 0 1 0-1.414l6-6a1 1 0 0 1 1.414 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,3 @@
|
||||
.overlay > p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
min-height: var(--cpd-space-32x);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { type FC } from "react";
|
||||
import { BigIcon, Button, Heading, Text } from "@vector-im/compound-web";
|
||||
import { VoiceCallIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { EarpieceIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./EarpieceOverlay.module.css";
|
||||
@@ -22,12 +22,12 @@ export const EarpieceOverlay: FC<Props> = ({ show, onBackToVideoPressed }) => {
|
||||
return (
|
||||
<div className={styles.overlay} data-show={show}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<VoiceCallIcon aria-hidden />
|
||||
<EarpieceIcon aria-hidden />
|
||||
</BigIcon>
|
||||
<Heading as="h2" weight="semibold" size="md">
|
||||
{t("handset.overlay_title")}
|
||||
{t("earpiece.overlay_title")}
|
||||
</Heading>
|
||||
<Text>{t("handset.overlay_description")}</Text>
|
||||
<Text>{t("earpiece.overlay_description")}</Text>
|
||||
<Button
|
||||
kind="primary"
|
||||
size="sm"
|
||||
@@ -35,10 +35,8 @@ export const EarpieceOverlay: FC<Props> = ({ show, onBackToVideoPressed }) => {
|
||||
onBackToVideoPressed?.();
|
||||
}}
|
||||
>
|
||||
{t("handset.overlay_back_button")}
|
||||
{t("earpiece.overlay_back_button")}
|
||||
</Button>
|
||||
{/* This spacer is used to give the overlay an offset to the top. */}
|
||||
<div className={styles.spacer} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import { useObservable } from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||
import {
|
||||
VoiceCallSolidIcon,
|
||||
EarpieceIcon,
|
||||
VolumeOnSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -458,9 +458,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
useMemo(() => {
|
||||
if (audioOutputSwitcher === null) return null;
|
||||
const isEarpieceTarget = audioOutputSwitcher.targetOutput === "earpiece";
|
||||
const Icon = isEarpieceTarget ? VoiceCallSolidIcon : VolumeOnSolidIcon;
|
||||
const Icon = isEarpieceTarget ? EarpieceIcon : VolumeOnSolidIcon;
|
||||
const label = isEarpieceTarget
|
||||
? t("settings.devices.handset")
|
||||
? t("settings.devices.earpiece")
|
||||
: t("settings.devices.loudspeaker");
|
||||
|
||||
return (
|
||||
|
||||
@@ -191,11 +191,7 @@ describe("useMuteStates", () => {
|
||||
mockConfig();
|
||||
|
||||
render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
"/room/?skipLobby=true&widgetId=1234&parentUrl=www.parent.org",
|
||||
]}
|
||||
>
|
||||
<MemoryRouter initialEntries={["/room/?skipLobby=true"]}>
|
||||
<MediaDevicesContext value={mockMediaDevices()}>
|
||||
<TestComponent />
|
||||
</MediaDevicesContext>
|
||||
|
||||
@@ -98,30 +98,18 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M8.929 15.1a13.6 13.6 0 0 0 4.654 3.066q2.62 1.036 5.492.923h.008l.003-.004.003-.002-.034-3.124-3.52-.483-1.791 1.792-.645-.322a13.5 13.5 0 0 1-3.496-2.52 13.4 13.4 0 0 1-2.52-3.496l-.322-.644 1.792-1.792-.483-3.519-3.123-.034-.003.002-.003.004v.002a13.65 13.65 0 0 0 .932 5.492A13.4 13.4 0 0 0 8.93 15.1m3.92 4.926a15.6 15.6 0 0 1-5.334-3.511 15.4 15.4 0 0 1-3.505-5.346 15.6 15.6 0 0 1-1.069-6.274 1.93 1.93 0 0 1 .589-1.366c.366-.366.84-.589 1.386-.589h.01l3.163.035a1.96 1.96 0 0 1 1.958 1.694v.005l.487 3.545v.003c.043.297.025.605-.076.907a2 2 0 0 1-.485.773l-.762.762a11.4 11.4 0 0 0 3.206 3.54q.457.33.948.614l.762-.761a2 2 0 0 1 .774-.486c.302-.1.61-.118.907-.076l3.553.487a1.96 1.96 0 0 1 1.694 1.958l.034 3.174c0 .546-.223 1.02-.588 1.386-.361.36-.827.582-1.363.588a15.3 15.3 0 0 1-6.29-1.062"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
<path
|
||||
d="M14 2c3.93 0 7 3.07 7 7a1 1 0 0 1-2 0c0-2.8-2.2-5-5-5S9 6.2 9 9c0 .93.29 1.98.82 2.94.71 1.29 1.53 1.92 2.32 2.53.92.71 1.88 1.44 2.39 3 .5 1.5 1 2.01 1.71 2.38.2.09.47.15.76.15 1.1 0 2-.9 2-2a1 1 0 1 1 2 0 4 4 0 0 1-5.64 3.65c-1.36-.71-2.13-1.73-2.73-3.55-.32-.98-.9-1.43-1.71-2.05-.87-.67-1.94-1.5-2.85-3.15C7.38 11.65 7 10.26 7 9c0-3.93 3.07-7 7-7"
|
||||
/>
|
||||
<path
|
||||
d="M6.145 1.3a1 1 0 0 1 1.427 1.4A8.97 8.97 0 0 0 5 9c0 2.3.862 4.397 2.281 5.988l.291.312.069.077A1 1 0 0 1 6.22 16.77l-.075-.07-.356-.38A10.96 10.96 0 0 1 3 9c0-2.998 1.2-5.717 3.145-7.7M14 6.5a2.5 2.5 0 0 1 0 5 2.501 2.501 0 0 1 0-5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Handset Mode
|
||||
Earpiece Mode
|
||||
</h2>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
|
||||
@@ -137,9 +125,6 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
Back to Speaker Mode
|
||||
</button>
|
||||
<div
|
||||
class="spacer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="container"
|
||||
|
||||
@@ -98,7 +98,7 @@ export const DeviceSelection: FC<Props> = ({
|
||||
labelText = t("settings.devices.loudspeaker");
|
||||
break;
|
||||
case "earpiece":
|
||||
labelText = t("settings.devices.handset");
|
||||
labelText = t("settings.devices.earpiece");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1048,7 +1048,7 @@ test("audio output changes when toggling earpiece mode", () => {
|
||||
|
||||
window.controls.setAvailableAudioDevices([
|
||||
{ id: "speaker", name: "Speaker", isSpeaker: true },
|
||||
{ id: "earpiece", name: "Handset", isEarpiece: true },
|
||||
{ id: "earpiece", name: "Earpiece", isEarpiece: true },
|
||||
{ id: "headphones", name: "Headphones" },
|
||||
]);
|
||||
window.controls.setAudioDevice("headphones");
|
||||
|
||||
@@ -106,10 +106,6 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
if (!baseUrl) throw new Error("Base URL must be supplied");
|
||||
|
||||
// These are all the event types the app uses
|
||||
const sendEvent = [
|
||||
EventType.CallNotify, // Sent as a deprecated fallback
|
||||
EventType.RTCNotification,
|
||||
];
|
||||
const sendRecvEvent = [
|
||||
"org.matrix.rageshake_request",
|
||||
EventType.CallEncryptionKeysPrefix,
|
||||
@@ -133,7 +129,6 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
{ eventType: EventType.RoomEncryption },
|
||||
{ eventType: EventType.GroupCallMemberPrefix },
|
||||
];
|
||||
|
||||
const sendRecvToDevice = [
|
||||
EventType.CallInvite,
|
||||
EventType.CallCandidates,
|
||||
@@ -151,7 +146,7 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
const client = createRoomWidgetClient(
|
||||
api,
|
||||
{
|
||||
sendEvent: [...sendEvent, ...sendRecvEvent],
|
||||
sendEvent: sendRecvEvent,
|
||||
receiveEvent: sendRecvEvent,
|
||||
sendState,
|
||||
receiveState,
|
||||
|
||||
22
yarn.lock
22
yarn.lock
@@ -5594,9 +5594,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vector-im/compound-design-tokens@npm:^5.0.0":
|
||||
version: 5.0.2
|
||||
resolution: "@vector-im/compound-design-tokens@npm:5.0.2"
|
||||
"@vector-im/compound-design-tokens@npm:^4.0.0":
|
||||
version: 4.0.5
|
||||
resolution: "@vector-im/compound-design-tokens@npm:4.0.5"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
react: ^17 || ^18 || ^19.0.0
|
||||
@@ -5605,13 +5605,13 @@ __metadata:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
checksum: 10c0/4337ef74c269602a7801fe5361ee95aa453529e2eac2fa80620e6c8d089551c3e77061d4690755023487aae2281be0a5df959a355ce3fb13e151e40822b74e03
|
||||
checksum: 10c0/ae84195a5c854ac54e64b615729cb5f669da1b798b961be4370e1baf257a57e871f53c6df555702513a99fcc5576bff50033a2ed676b36c7344f1d470826f10f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vector-im/compound-web@npm:^8.0.0":
|
||||
version: 8.2.0
|
||||
resolution: "@vector-im/compound-web@npm:8.2.0"
|
||||
version: 8.1.2
|
||||
resolution: "@vector-im/compound-web@npm:8.1.2"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.27.0"
|
||||
"@radix-ui/react-context-menu": "npm:^2.2.1"
|
||||
@@ -5626,12 +5626,12 @@ __metadata:
|
||||
"@fontsource/inconsolata": ^5
|
||||
"@fontsource/inter": ^5
|
||||
"@types/react": "*"
|
||||
"@vector-im/compound-design-tokens": ">=1.6.1 <6.0.0"
|
||||
"@vector-im/compound-design-tokens": ">=1.6.1 <5.0.0"
|
||||
react: ^18 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/4ac4074dcf9611bdff7de4bf66763397c926d6312f31758bcabe3e7bf704cb76bc2ce1023fe5f2cf0d05e97c9c540fef8b63edea7a521a2f7b4b7fbcb883fb17
|
||||
checksum: 10c0/4d16794f20b5577bb32442a8261ccad87d83ef91d9212e4ff12af90c4423790859b9a712764211c751079d384bf4dc88522961e008a9bd9f46653fdd75cdc195
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7477,7 +7477,7 @@ __metadata:
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.31.0"
|
||||
"@typescript-eslint/parser": "npm:^8.31.0"
|
||||
"@use-gesture/react": "npm:^10.2.11"
|
||||
"@vector-im/compound-design-tokens": "npm:^5.0.0"
|
||||
"@vector-im/compound-design-tokens": "npm:^4.0.0"
|
||||
"@vector-im/compound-web": "npm:^8.0.0"
|
||||
"@vitejs/plugin-react": "npm:^4.0.1"
|
||||
"@vitest/coverage-v8": "npm:^3.0.0"
|
||||
@@ -10247,7 +10247,7 @@ __metadata:
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=develop":
|
||||
version: 37.11.0
|
||||
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=11c9e39e5a1e56ea347dbd16ca7c75059127f7d5"
|
||||
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=aa79236ce23cef85583c802c0a9e8d1e77814d51"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.12.5"
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.0.0"
|
||||
@@ -10263,7 +10263,7 @@ __metadata:
|
||||
sdp-transform: "npm:^2.14.1"
|
||||
unhomoglyph: "npm:^1.0.6"
|
||||
uuid: "npm:11"
|
||||
checksum: 10c0/31018452c9578948c47f82648a58d4000b657103b20a38cb1672035bc530e0aaa68ac03bc600f4bfaad9f72f36842490be963251a02d2926dcfe697ff6e459b3
|
||||
checksum: 10c0/33fb0918b0742c6a00ed0e4fd3cc958ae824d407f8e0e65687a011f8bbb058163e4123a7affce47368ca8489b96ccdceddfe98becc6a7c7aff7ab68b4e63a072
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user