mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Compare commits
1 Commits
quenting/d
...
af/bump-wi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a5ca14967 |
1
.github/workflows/build.yaml
vendored
1
.github/workflows/build.yaml
vendored
@@ -23,4 +23,3 @@ jobs:
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
3
.github/workflows/element-call.yaml
vendored
3
.github/workflows/element-call.yaml
vendored
@@ -14,8 +14,6 @@ on:
|
||||
required: true
|
||||
SENTRY_AUTH_TOKEN:
|
||||
required: true
|
||||
CODECOV_TOKEN:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -38,7 +36,6 @@ jobs:
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
VITE_APP_VERSION: ${{ inputs.vite_app_version }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- name: Upload Artifact
|
||||
|
||||
@@ -2,3 +2,9 @@ FROM nginxinc/nginx-unprivileged:alpine
|
||||
|
||||
COPY ./dist /app
|
||||
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
USER root
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html
|
||||
|
||||
USER 101
|
||||
|
||||
@@ -106,7 +106,7 @@ rc_message:
|
||||
|
||||
MSC3266 allows to request a room summary of rooms you are not joined. The
|
||||
summary contains the room join rules. We need that to decide if the user gets
|
||||
prompted with the option to knock ("Request to join call"), a cannot join error or the
|
||||
prompted with the option to knock ("ask to join"), a cannot join error or the
|
||||
join view.
|
||||
|
||||
Element Call requires a Livekit SFU alongside a [Livekit JWT
|
||||
|
||||
@@ -3,16 +3,23 @@ server {
|
||||
server_name localhost;
|
||||
|
||||
root /app;
|
||||
gzip_static on;
|
||||
|
||||
location / {
|
||||
# disable cache entriely by default (apart from Etag which is accurate enough)
|
||||
add_header Cache-Control 'private no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
|
||||
if_modified_since off;
|
||||
expires off;
|
||||
# also turn off last-modified since they are just the timestamps of the file in the docker image
|
||||
# and may or may not bear any resemblance to when the resource changed
|
||||
add_header Last-Modified "";
|
||||
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# assets can be cached because they have hashed filenames
|
||||
location /assets {
|
||||
add_header Cache-Control "public, immutable, max-age=31536000";
|
||||
expires 1w;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
location /apple-app-site-association {
|
||||
|
||||
13
package.json
13
package.json
@@ -24,9 +24,6 @@
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@codecov/vite-plugin": "^1.3.0",
|
||||
"@fontsource/inconsolata": "^5.1.0",
|
||||
"@fontsource/inter": "^5.1.0",
|
||||
"@formatjs/intl-durationformat": "^0.6.1",
|
||||
"@formatjs/intl-segmenter": "^11.7.3",
|
||||
"@livekit/components-core": "^0.11.0",
|
||||
@@ -51,9 +48,8 @@
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -89,9 +85,9 @@
|
||||
"jsdom": "^25.0.0",
|
||||
"knip": "^5.27.2",
|
||||
"livekit-client": "^2.5.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.1",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#8e9a04cdec0f88fc876bbbf406db55b0677f005d",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#fcb69b16ad8d170c67ea844f83543d467bbd7707",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
@@ -112,9 +108,8 @@
|
||||
"typescript": "^5.1.6",
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"vaul": "^1.0.0",
|
||||
"vaul": "^0.9.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-compression2": "^1.3.1",
|
||||
"vite-plugin-html-template": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.0.0",
|
||||
"vitest": "^2.0.0",
|
||||
|
||||
BIN
public/fonts/Inter/Inter-Bold.woff
Normal file
BIN
public/fonts/Inter/Inter-Bold.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-Bold.woff2
Normal file
BIN
public/fonts/Inter/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-BoldItalic.woff
Normal file
BIN
public/fonts/Inter/Inter-BoldItalic.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-BoldItalic.woff2
Normal file
BIN
public/fonts/Inter/Inter-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-Italic.woff
Normal file
BIN
public/fonts/Inter/Inter-Italic.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-Italic.woff2
Normal file
BIN
public/fonts/Inter/Inter-Italic.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-Medium.woff
Normal file
BIN
public/fonts/Inter/Inter-Medium.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-Medium.woff2
Normal file
BIN
public/fonts/Inter/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-MediumItalic.woff
Normal file
BIN
public/fonts/Inter/Inter-MediumItalic.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-MediumItalic.woff2
Normal file
BIN
public/fonts/Inter/Inter-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-Regular.woff
Normal file
BIN
public/fonts/Inter/Inter-Regular.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-Regular.woff2
Normal file
BIN
public/fonts/Inter/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-SemiBold.woff
Normal file
BIN
public/fonts/Inter/Inter-SemiBold.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-SemiBold.woff2
Normal file
BIN
public/fonts/Inter/Inter-SemiBold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-SemiBoldItalic.woff
Normal file
BIN
public/fonts/Inter/Inter-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter/Inter-SemiBoldItalic.woff2
Normal file
BIN
public/fonts/Inter/Inter-SemiBoldItalic.woff2
Normal file
Binary file not shown.
@@ -81,8 +81,8 @@
|
||||
"call_ended_heading": "Anruf beendet",
|
||||
"failed_heading": "Beitreten fehlgeschlagen",
|
||||
"failed_text": "Anruf nicht gefunden oder Beitritt nicht erlaubt",
|
||||
"knock_reject_body": "Die Teilnahmeanfrage wurde abgelehnt.",
|
||||
"knock_reject_heading": "Zugriff verweigert",
|
||||
"knock_reject_body": "Die Mitglieder des Raums haben Deine Beitrittsanfrage abgelehnt.",
|
||||
"knock_reject_heading": "Beitritt nicht erlaubt",
|
||||
"reason": "Grund"
|
||||
},
|
||||
"hangup_button_label": "Anruf beenden",
|
||||
@@ -100,11 +100,11 @@
|
||||
"layout_grid_label": "Raster",
|
||||
"layout_spotlight_label": "Fokus",
|
||||
"lobby": {
|
||||
"ask_to_join": "Teilnahmeanfrage senden",
|
||||
"ask_to_join": "Beitritt anfragen",
|
||||
"join_as_guest": "Als Gast beitreten",
|
||||
"join_button": "Anruf beitreten",
|
||||
"leave_button": "Zurück zu kürzlichen Anrufen",
|
||||
"waiting_for_invite": "Anfrage gesendet! Warten auf Teilnahmefreigabe …"
|
||||
"waiting_for_invite": "Anfrage gesendet"
|
||||
},
|
||||
"log_in": "Anmelden",
|
||||
"logging_in": "Anmelden …",
|
||||
@@ -115,7 +115,7 @@
|
||||
"matrix_id": "Matrix-ID: {{id}}",
|
||||
"microphone_off": "Mikrofon aus",
|
||||
"microphone_on": "Mikrofon an",
|
||||
"mute_microphone_button_label": "Mikrofon stumm schalten",
|
||||
"mute_microphone_button_label": "Mikrofon deaktivieren",
|
||||
"participant_count_one": "{{count, number}}",
|
||||
"participant_count_other": "{{count, number}}",
|
||||
"qr_code": "QR-Code",
|
||||
@@ -184,15 +184,14 @@
|
||||
"unauthenticated_view_body": "Noch nicht registriert? <2>Konto erstellen</2>",
|
||||
"unauthenticated_view_eula_caption": "Mit einem Klick auf „Los geht’s“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
|
||||
"unauthenticated_view_login_button": "Melde dich mit deinem Konto an",
|
||||
"unmute_microphone_button_label": "Mikrofon einschalten",
|
||||
"unmute_microphone_button_label": "Mikrofon aktivieren",
|
||||
"version": "{{productName}} Version: {{version}}",
|
||||
"video_tile": {
|
||||
"always_show": "Immer anzeigen",
|
||||
"change_fit_contain": "An Fenster anpassen",
|
||||
"collapse": "Minimieren",
|
||||
"expand": "Erweitern",
|
||||
"mute_for_me": "Für mich stumm schalten",
|
||||
"muted_for_me": "Für mich stumm geschaltet",
|
||||
"mute_for_me": "Für mich stummschalten",
|
||||
"volume": "Lautstärke"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
"call_ended_heading": "Call ended",
|
||||
"failed_heading": "Failed to join",
|
||||
"failed_text": "Call not found or is not accessible.",
|
||||
"knock_reject_body": "Your request to join was declined.",
|
||||
"knock_reject_heading": "Access denied",
|
||||
"knock_reject_body": "The room members declined your request to join.",
|
||||
"knock_reject_heading": "Not allowed to join",
|
||||
"reason": "Reason"
|
||||
},
|
||||
"hangup_button_label": "End call",
|
||||
@@ -100,11 +100,11 @@
|
||||
"layout_grid_label": "Grid",
|
||||
"layout_spotlight_label": "Spotlight",
|
||||
"lobby": {
|
||||
"ask_to_join": "Request to join call",
|
||||
"ask_to_join": "Ask to join call",
|
||||
"join_as_guest": "Join as guest",
|
||||
"join_button": "Join call",
|
||||
"leave_button": "Back to recents",
|
||||
"waiting_for_invite": "Request sent! Waiting for permission to join…"
|
||||
"waiting_for_invite": "Request sent"
|
||||
},
|
||||
"log_in": "Log In",
|
||||
"logging_in": "Logging in…",
|
||||
@@ -192,7 +192,6 @@
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"mute_for_me": "Mute for me",
|
||||
"muted_for_me": "Muted for me",
|
||||
"volume": "Volume"
|
||||
}
|
||||
}
|
||||
|
||||
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
@@ -6,7 +6,6 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import "matrix-js-sdk/src/@types/global";
|
||||
import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat";
|
||||
import { Controls } from "../controls";
|
||||
|
||||
declare global {
|
||||
@@ -24,9 +23,4 @@ declare global {
|
||||
// Safari only supports this prefixed, so tell the type system about it
|
||||
webkitRequestFullscreen: () => void;
|
||||
}
|
||||
|
||||
namespace Intl {
|
||||
// Add DurationFormat as part of the Intl namespace because we polyfill it
|
||||
const DurationFormat: typeof PolyfillDurationFormat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,19 @@ import {
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
ClientEvent,
|
||||
ICreateClientOpts,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { ClientEvent, type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
|
||||
import type { WidgetApi } from "matrix-widget-api";
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import { fallbackICEServerAllowed, initClient } from "./utils/matrix";
|
||||
import { widget } from "./widget";
|
||||
import {
|
||||
PosthogAnalytics,
|
||||
@@ -30,6 +36,7 @@ import {
|
||||
} from "./analytics/PosthogAnalytics";
|
||||
import { translatedError } from "./TranslatedError";
|
||||
import { useEventTarget } from "./useEvents";
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -352,7 +359,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export type InitResult = {
|
||||
type InitResult = {
|
||||
widgetApi: WidgetApi | null;
|
||||
client: MatrixClient;
|
||||
passwordlessUser: boolean;
|
||||
@@ -369,8 +376,50 @@ async function loadClient(): Promise<InitResult | null> {
|
||||
passwordlessUser: false,
|
||||
};
|
||||
} else {
|
||||
const { initSPA } = await import("./utils/spa");
|
||||
return initSPA(loadSession, clearSession);
|
||||
// We're running as a standalone application
|
||||
try {
|
||||
const session = loadSession();
|
||||
if (!session) {
|
||||
logger.log("No session stored; continuing without a client");
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.log("Using a standalone client");
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } = session;
|
||||
const initClientParams: ICreateClientOpts = {
|
||||
baseUrl: Config.defaultHomeserverUrl()!,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
livekitServiceURL: Config.get().livekit?.livekit_service_url,
|
||||
};
|
||||
|
||||
try {
|
||||
const client = await initClient(initClientParams, true);
|
||||
return {
|
||||
widgetApi: null,
|
||||
client,
|
||||
passwordlessUser,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof MatrixError && err.errcode === "M_UNKNOWN_TOKEN") {
|
||||
// We can't use this session anymore, so let's log it out
|
||||
logger.log(
|
||||
"The session from local store is invalid; continuing without a client",
|
||||
);
|
||||
clearSession();
|
||||
// returning null = "no client` pls register" (undefined = "loading" which is the current value when reaching this line)
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details.
|
||||
import { FC, useCallback } from "react";
|
||||
import { Root, Track, Range, Thumb } from "@radix-ui/react-slider";
|
||||
import classNames from "classnames";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import styles from "./Slider.module.css";
|
||||
|
||||
@@ -67,10 +66,7 @@ export const Slider: FC<Props> = ({
|
||||
<Track className={styles.track}>
|
||||
<Range className={styles.highlight} />
|
||||
</Track>
|
||||
{/* Note: This is expected not to be visible on mobile.*/}
|
||||
<Tooltip placement="top" label={Math.round(value * 100).toString() + "%"}>
|
||||
<Thumb className={styles.handle} aria-label={label} />
|
||||
</Tooltip>
|
||||
<Thumb className={styles.handle} aria-label={label} />
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,16 +39,13 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
aria-labelledby="radix-:ra:"
|
||||
class="overlay modal drawer"
|
||||
data-state="open"
|
||||
data-vaul-animate="true"
|
||||
data-vaul-custom-container="false"
|
||||
data-vaul-delayed-snap-points="false"
|
||||
data-vaul-drawer=""
|
||||
data-vaul-drawer-direction="bottom"
|
||||
data-vaul-snap-points="false"
|
||||
id="radix-:r9:"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
vaul-drawer=""
|
||||
vaul-drawer-direction="bottom"
|
||||
vaul-drawer-visible="true"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { merge } from "lodash-es";
|
||||
import { merge } from "lodash";
|
||||
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import {
|
||||
|
||||
105
src/index.css
105
src/index.css
@@ -5,14 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
@layer normalize, compound-legacy, compound;
|
||||
/* Inter unexpectedly contains various codepoints which collide with emoji, even
|
||||
when variation-16 is applied to request the emoji variant. From eyeballing
|
||||
the emoji picker, these are: 20e3, 23cf, 24c2, 25a0-25c1, 2665, 2764, 2b06, 2b1c.
|
||||
Therefore we define a unicode-range to load which excludes the glyphs
|
||||
(to avoid having to maintain a fork of Inter). */
|
||||
|
||||
@import url("@fontsource/inter/400.css");
|
||||
@import url("@fontsource/inter/500.css");
|
||||
@import url("@fontsource/inter/600.css");
|
||||
@import url("@fontsource/inter/700.css");
|
||||
@import url("@fontsource/inconsolata/400.css");
|
||||
@import url("@fontsource/inconsolata/700.css");
|
||||
@layer normalize, compound-legacy, compound;
|
||||
|
||||
@import url("normalize.css/normalize.css") layer(normalize);
|
||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css")
|
||||
@@ -53,6 +52,94 @@ layer(compound);
|
||||
--stopgap-background-85: rgba(16, 19, 23, 0.85);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-Regular.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Regular.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-Italic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Italic.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-Medium.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Medium.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-MediumItalic.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-SemiBold.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-Bold.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Bold.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src:
|
||||
url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-BoldItalic.woff") format("woff");
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
background-size: calc(max(1440px, 100vw)) calc(max(800px, 100vh));
|
||||
@@ -98,6 +185,10 @@ body[data-platform="ios"] {
|
||||
--cpd-font-family-sans: -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
|
||||
}
|
||||
|
||||
body[data-platform="desktop"] {
|
||||
--cpd-font-family-sans: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
@layer compound-legacy {
|
||||
h1,
|
||||
h2,
|
||||
|
||||
@@ -9,9 +9,9 @@ import { expect, test } from "vitest";
|
||||
|
||||
import { Initializer } from "../src/initializer";
|
||||
|
||||
test("initBeforeReact sets font family from URL param", async () => {
|
||||
test("initBeforeReact sets font family from URL param", () => {
|
||||
window.location.hash = "#?font=DejaVu Sans";
|
||||
await Initializer.initBeforeReact();
|
||||
Initializer.initBeforeReact();
|
||||
expect(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--font-family",
|
||||
@@ -19,9 +19,9 @@ test("initBeforeReact sets font family from URL param", async () => {
|
||||
).toBe('"DejaVu Sans"');
|
||||
});
|
||||
|
||||
test("initBeforeReact sets font scale from URL param", async () => {
|
||||
test("initBeforeReact sets font scale from URL param", () => {
|
||||
window.location.hash = "#?fontScale=1.2";
|
||||
await Initializer.initBeforeReact();
|
||||
Initializer.initBeforeReact();
|
||||
expect(
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--font-scale"),
|
||||
).toBe("1.2");
|
||||
|
||||
@@ -11,8 +11,6 @@ import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import Backend from "i18next-http-backend";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill";
|
||||
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { Config } from "./config/Config";
|
||||
@@ -43,17 +41,10 @@ export class Initializer {
|
||||
return Initializer.internalInstance?.isInitialized;
|
||||
}
|
||||
|
||||
public static async initBeforeReact(): Promise<void> {
|
||||
const polyfills: Promise<unknown>[] = [];
|
||||
if (shouldPolyfillSegmenter()) {
|
||||
polyfills.push(import("@formatjs/intl-segmenter/polyfill-force"));
|
||||
}
|
||||
|
||||
if (shouldPolyfillDurationFormat()) {
|
||||
polyfills.push(import("@formatjs/intl-durationformat/polyfill-force"));
|
||||
}
|
||||
|
||||
await Promise.all(polyfills);
|
||||
public static initBeforeReact(): void {
|
||||
// this maybe also needs to return a promise in the future,
|
||||
// if we have to do async inits before showing the loading screen
|
||||
// but this should be avoided if possible
|
||||
|
||||
//i18n
|
||||
const languageDetector = new LanguageDetector();
|
||||
@@ -63,7 +54,7 @@ export class Initializer {
|
||||
lookup: () => getUrlParams().lang ?? undefined,
|
||||
});
|
||||
|
||||
await i18n
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
@@ -83,6 +74,9 @@ export class Initializer {
|
||||
order: ["urlFragment", "navigator"],
|
||||
caches: [],
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Failed to initialize i18n", e);
|
||||
});
|
||||
|
||||
// Custom Themeing
|
||||
|
||||
23
src/main.tsx
23
src/main.tsx
@@ -20,6 +20,8 @@ import {
|
||||
setLogExtension as setLKLogExtension,
|
||||
setLogLevel as setLKLogLevel,
|
||||
} from "livekit-client";
|
||||
import "@formatjs/intl-segmenter/polyfill";
|
||||
import "@formatjs/intl-durationformat/polyfill";
|
||||
|
||||
import { App } from "./App";
|
||||
import { init as initRageshake } from "./settings/rageshake";
|
||||
@@ -55,17 +57,12 @@ if (fatalError !== null) {
|
||||
throw fatalError; // Stop the app early
|
||||
}
|
||||
|
||||
Initializer.initBeforeReact()
|
||||
.then(() => {
|
||||
const history = createBrowserHistory();
|
||||
Initializer.initBeforeReact();
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App history={history} />
|
||||
</StrictMode>,
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Failed to initialize app", e);
|
||||
root.render(e.message);
|
||||
});
|
||||
const history = createBrowserHistory();
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App history={history} />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -11,12 +11,19 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { DurationFormat } from "@formatjs/intl-durationformat";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ReactionIndicator } from "./ReactionIndicator";
|
||||
|
||||
const durationFormatter = new DurationFormat(undefined, {
|
||||
minutesDisplay: "always",
|
||||
secondsDisplay: "always",
|
||||
hoursDisplay: "auto",
|
||||
style: "digital",
|
||||
});
|
||||
|
||||
export function RaisedHandIndicator({
|
||||
raisedHandTime,
|
||||
miniature,
|
||||
@@ -31,17 +38,6 @@ export function RaisedHandIndicator({
|
||||
const { t } = useTranslation();
|
||||
const [raisedHandDuration, setRaisedHandDuration] = useState("");
|
||||
|
||||
const durationFormatter = useMemo(
|
||||
() =>
|
||||
new Intl.DurationFormat(undefined, {
|
||||
minutesDisplay: "always",
|
||||
secondsDisplay: "always",
|
||||
hoursDisplay: "auto",
|
||||
style: "digital",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const clickCallback = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
||||
(event) => {
|
||||
if (!onClick) {
|
||||
@@ -73,7 +69,7 @@ export function RaisedHandIndicator({
|
||||
calculateTime();
|
||||
const to = setInterval(calculateTime, 1000);
|
||||
return (): void => clearInterval(to);
|
||||
}, [setRaisedHandDuration, raisedHandTime, showTimer, durationFormatter]);
|
||||
}, [setRaisedHandDuration, raisedHandTime, showTimer]);
|
||||
|
||||
if (!raisedHandTime) {
|
||||
return;
|
||||
|
||||
77
src/room/GroupCallLoader.tsx
Normal file
77
src/room/GroupCallLoader.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2022-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { Heading, Text } from "@vector-im/compound-web";
|
||||
|
||||
import { Link } from "../button/Link";
|
||||
import {
|
||||
useLoadGroupCall,
|
||||
GroupCallStatus,
|
||||
CallTerminatedMessage,
|
||||
} from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
roomIdOrAlias: string;
|
||||
viaServers: string[];
|
||||
children: (groupCallState: GroupCallStatus) => JSX.Element;
|
||||
}
|
||||
|
||||
export function GroupCallLoader({
|
||||
client,
|
||||
roomIdOrAlias,
|
||||
viaServers,
|
||||
children,
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
|
||||
|
||||
switch (groupCallState.kind) {
|
||||
case "loaded":
|
||||
case "waitForInvite":
|
||||
case "canKnock":
|
||||
return children(groupCallState);
|
||||
case "loading":
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("common.loading")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
case "failed":
|
||||
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{t("group_call_loader.failed_heading")}</Heading>
|
||||
<Text>{t("group_call_loader.failed_text")}</Text>
|
||||
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
|
||||
dupes of this flow, let's make a common component and put it here. */}
|
||||
<Link to="/">{t("common.home")}</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else if (groupCallState.error instanceof CallTerminatedMessage) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{groupCallState.error.message}</Heading>
|
||||
<Text>{groupCallState.error.messageBody}</Text>
|
||||
{groupCallState.error.reason && (
|
||||
<>
|
||||
{t("group_call_loader.reason")}:
|
||||
<Text size="sm">"{groupCallState.error.reason}"</Text>
|
||||
</>
|
||||
)}
|
||||
<Link to="/">{t("common.home")}</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,37 +177,29 @@ export const GroupCallView: FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (skipLobby) {
|
||||
if (widget) {
|
||||
if (preload) {
|
||||
// In preload mode without lobby we wait for a join action before entering
|
||||
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
(async (): Promise<void> => {
|
||||
await defaultDeviceSetup(
|
||||
ev.detail.data as unknown as JoinCallData,
|
||||
);
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
widget!.api.transport.reply(ev.detail, {});
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
});
|
||||
};
|
||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||
return (): void => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
} else {
|
||||
// No lobby and no preload: we enter the rtc session right away
|
||||
(async (): Promise<void> => {
|
||||
await defaultDeviceSetup({ audioInput: null, videoInput: null });
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
void enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
}
|
||||
if (widget && preload && skipLobby) {
|
||||
// In preload mode without lobby we wait for a join action before entering
|
||||
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
(async (): Promise<void> => {
|
||||
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
widget!.api.transport.reply(ev.detail, {});
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
});
|
||||
};
|
||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||
return (): void => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
} else if (widget && !preload && skipLobby) {
|
||||
// No lobby and no preload: we enter the rtc session right away
|
||||
(async (): Promise<void> => {
|
||||
await defaultDeviceSetup({ audioInput: null, videoInput: null });
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
});
|
||||
}
|
||||
}, [rtcSession, preload, skipLobby, perParticipantE2EE]);
|
||||
|
||||
|
||||
@@ -5,16 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, useEffect, useState, ReactNode, useRef } from "react";
|
||||
import { FC, useEffect, useState, useCallback, ReactNode } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import { Heading, Text } from "@vector-im/compound-web";
|
||||
|
||||
import { useClientLegacy } from "../ClientContext";
|
||||
import { ErrorView, FullScreenView, LoadingView } from "../FullScreenView";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { RoomAuthView } from "./RoomAuthView";
|
||||
import { GroupCallLoader } from "./GroupCallLoader";
|
||||
import { GroupCallView } from "./GroupCallView";
|
||||
import { useRoomIdentifier, useUrlParams } from "../UrlParams";
|
||||
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
||||
@@ -22,14 +21,13 @@ import { HomePage } from "../home/HomePage";
|
||||
import { platform } from "../Platform";
|
||||
import { AppSelectionModal } from "./AppSelectionModal";
|
||||
import { widget } from "../widget";
|
||||
import { CallTerminatedMessage, useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import { GroupCallStatus } from "./useLoadGroupCall";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import { useMuteStates } from "./MuteStates";
|
||||
import { useOptInAnalytics } from "../settings/settings";
|
||||
import { Config } from "../config/Config";
|
||||
import { Link } from "../button/Link";
|
||||
|
||||
export const RoomPage: FC = () => {
|
||||
const {
|
||||
@@ -55,7 +53,6 @@ export const RoomPage: FC = () => {
|
||||
useClientLegacy();
|
||||
const { avatarUrl, displayName: userDisplayName } = useProfile(client);
|
||||
|
||||
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
|
||||
const muteStates = useMuteStates();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,112 +82,82 @@ export const RoomPage: FC = () => {
|
||||
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
|
||||
}, [optInAnalytics, setOptInAnalytics]);
|
||||
|
||||
const wasInWaitForInviteState = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupCallState.kind === "loaded" && wasInWaitForInviteState.current) {
|
||||
logger.log("Play join sound 'Not yet implemented'");
|
||||
}
|
||||
}, [groupCallState.kind]);
|
||||
|
||||
const groupCallView = (): JSX.Element => {
|
||||
switch (groupCallState.kind) {
|
||||
case "loaded":
|
||||
return (
|
||||
<GroupCallView
|
||||
client={client!}
|
||||
rtcSession={groupCallState.rtcSession}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
confineToRoom={confineToRoom}
|
||||
preload={preload}
|
||||
skipLobby={skipLobby || wasInWaitForInviteState.current}
|
||||
hideHeader={hideHeader}
|
||||
muteStates={muteStates}
|
||||
/>
|
||||
);
|
||||
case "waitForInvite":
|
||||
case "canKnock": {
|
||||
wasInWaitForInviteState.current =
|
||||
wasInWaitForInviteState.current ||
|
||||
groupCallState.kind === "waitForInvite";
|
||||
const knock =
|
||||
groupCallState.kind === "canKnock" ? groupCallState.knock : null;
|
||||
const label: string | JSX.Element =
|
||||
groupCallState.kind === "canKnock" ? (
|
||||
t("lobby.ask_to_join")
|
||||
) : (
|
||||
<>
|
||||
{t("lobby.waiting_for_invite")}
|
||||
<CheckIcon />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<LobbyView
|
||||
client={client!}
|
||||
matrixInfo={{
|
||||
userId: client!.getUserId() ?? "",
|
||||
displayName: userDisplayName ?? "",
|
||||
avatarUrl: avatarUrl ?? "",
|
||||
roomAlias: null,
|
||||
roomId: groupCallState.roomSummary.room_id,
|
||||
roomName: groupCallState.roomSummary.name ?? "",
|
||||
roomAvatar: groupCallState.roomSummary.avatar_url ?? null,
|
||||
e2eeSystem: {
|
||||
kind: groupCallState.roomSummary["im.nheko.summary.encryption"]
|
||||
? E2eeType.PER_PARTICIPANT
|
||||
: E2eeType.NONE,
|
||||
},
|
||||
}}
|
||||
onEnter={(): void => knock?.()}
|
||||
enterLabel={label}
|
||||
waitingForInvite={groupCallState.kind === "waitForInvite"}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={hideHeader}
|
||||
participantCount={null}
|
||||
muteStates={muteStates}
|
||||
onShareClick={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "loading":
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("common.loading")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
case "failed":
|
||||
wasInWaitForInviteState.current = false;
|
||||
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
|
||||
const groupCallView = useCallback(
|
||||
(groupCallState: GroupCallStatus): JSX.Element => {
|
||||
switch (groupCallState.kind) {
|
||||
case "loaded":
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{t("group_call_loader.failed_heading")}</Heading>
|
||||
<Text>{t("group_call_loader.failed_text")}</Text>
|
||||
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
|
||||
dupes of this flow, let's make a common component and put it here. */}
|
||||
<Link to="/">{t("common.home")}</Link>
|
||||
</FullScreenView>
|
||||
<GroupCallView
|
||||
client={client!}
|
||||
rtcSession={groupCallState.rtcSession}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
confineToRoom={confineToRoom}
|
||||
preload={preload}
|
||||
skipLobby={skipLobby}
|
||||
hideHeader={hideHeader}
|
||||
muteStates={muteStates}
|
||||
/>
|
||||
);
|
||||
} else if (groupCallState.error instanceof CallTerminatedMessage) {
|
||||
case "waitForInvite":
|
||||
case "canKnock": {
|
||||
const knock =
|
||||
groupCallState.kind === "canKnock" ? groupCallState.knock : null;
|
||||
const label: string | JSX.Element =
|
||||
groupCallState.kind === "canKnock" ? (
|
||||
t("lobby.ask_to_join")
|
||||
) : (
|
||||
<>
|
||||
{t("lobby.waiting_for_invite")}
|
||||
<CheckIcon />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{groupCallState.error.message}</Heading>
|
||||
<Text>{groupCallState.error.messageBody}</Text>
|
||||
{groupCallState.error.reason && (
|
||||
<>
|
||||
{t("group_call_loader.reason")}:
|
||||
<Text size="sm">"{groupCallState.error.reason}"</Text>
|
||||
</>
|
||||
)}
|
||||
<Link to="/">{t("common.home")}</Link>
|
||||
</FullScreenView>
|
||||
<LobbyView
|
||||
client={client!}
|
||||
matrixInfo={{
|
||||
userId: client!.getUserId() ?? "",
|
||||
displayName: userDisplayName ?? "",
|
||||
avatarUrl: avatarUrl ?? "",
|
||||
roomAlias: null,
|
||||
roomId: groupCallState.roomSummary.room_id,
|
||||
roomName: groupCallState.roomSummary.name ?? "",
|
||||
roomAvatar: groupCallState.roomSummary.avatar_url ?? null,
|
||||
e2eeSystem: {
|
||||
kind: groupCallState.roomSummary[
|
||||
"im.nheko.summary.encryption"
|
||||
]
|
||||
? E2eeType.PER_PARTICIPANT
|
||||
: E2eeType.NONE,
|
||||
},
|
||||
}}
|
||||
onEnter={(): void => knock?.()}
|
||||
enterLabel={label}
|
||||
waitingForInvite={groupCallState.kind === "waitForInvite"}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={hideHeader}
|
||||
participantCount={null}
|
||||
muteStates={muteStates}
|
||||
onShareClick={null}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
}
|
||||
default:
|
||||
return <> </>;
|
||||
}
|
||||
};
|
||||
default:
|
||||
return <> </>;
|
||||
}
|
||||
},
|
||||
[
|
||||
client,
|
||||
passwordlessUser,
|
||||
confineToRoom,
|
||||
preload,
|
||||
skipLobby,
|
||||
hideHeader,
|
||||
muteStates,
|
||||
t,
|
||||
userDisplayName,
|
||||
avatarUrl,
|
||||
],
|
||||
);
|
||||
|
||||
let content: ReactNode;
|
||||
if (loading || isRegistering) {
|
||||
@@ -203,7 +170,15 @@ export const RoomPage: FC = () => {
|
||||
// TODO: This doesn't belong here, the app routes need to be reworked
|
||||
content = <HomePage />;
|
||||
} else {
|
||||
content = groupCallView();
|
||||
content = (
|
||||
<GroupCallLoader
|
||||
client={client}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
viaServers={viaServers}
|
||||
>
|
||||
{groupCallView}
|
||||
</GroupCallLoader>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -117,8 +117,8 @@ export class CallTerminatedMessage extends Error {
|
||||
}
|
||||
|
||||
export const useLoadGroupCall = (
|
||||
client: MatrixClient | undefined,
|
||||
roomIdOrAlias: string | null,
|
||||
client: MatrixClient,
|
||||
roomIdOrAlias: string,
|
||||
viaServers: string[],
|
||||
): GroupCallStatus => {
|
||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||
@@ -159,9 +159,6 @@ export const useLoadGroupCall = (
|
||||
?.getContent().reason;
|
||||
|
||||
useEffect(() => {
|
||||
if (!client || !roomIdOrAlias) {
|
||||
return;
|
||||
}
|
||||
const getRoomByAlias = async (alias: string): Promise<Room> => {
|
||||
// We lowercase the localpart when we create the room, so we must lowercase
|
||||
// it here too (we just do the whole alias). We can't do the same to room IDs
|
||||
|
||||
@@ -28,7 +28,7 @@ Please see LICENSE in the repository root for full details.
|
||||
// purge on startup to prevent logs from accumulating.
|
||||
|
||||
import EventEmitter from "events";
|
||||
import { throttle } from "lodash-es";
|
||||
import { throttle } from "lodash";
|
||||
import { Logger, logger } from "matrix-js-sdk/src/logger";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import loglevel, { LoggingMethod } from "loglevel";
|
||||
|
||||
@@ -102,7 +102,7 @@ export const playReactionsSound = new Setting<boolean>(
|
||||
|
||||
export const soundEffectVolumeSetting = new Setting<number>(
|
||||
"sound-effect-volume",
|
||||
0.5,
|
||||
1,
|
||||
);
|
||||
|
||||
export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);
|
||||
|
||||
@@ -6,6 +6,9 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ComponentProps, useCallback, useEffect, useState } from "react";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import pako from "pako";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
ClientEvent,
|
||||
@@ -20,14 +23,11 @@ import { Config } from "../config/Config";
|
||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||
import { RageshakeRequestModal } from "../room/RageshakeRequestModal";
|
||||
|
||||
const gzip = async (text: string): Promise<Blob> => {
|
||||
// pako is relatively large (200KB), so we only import it when needed
|
||||
const { gzip: pakoGzip } = await import("pako");
|
||||
|
||||
const gzip = (text: string): Blob => {
|
||||
// encode as UTF-8
|
||||
const buf = new TextEncoder().encode(text);
|
||||
// compress
|
||||
return new Blob([pakoGzip(buf)]);
|
||||
return new Blob([pako.gzip(buf)]);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -253,14 +253,12 @@ export function useSubmitRageshake(): {
|
||||
const logs = await getLogsForReport();
|
||||
|
||||
for (const entry of logs) {
|
||||
body.append("compressed-log", await gzip(entry.lines), entry.id);
|
||||
body.append("compressed-log", gzip(entry.lines), entry.id);
|
||||
}
|
||||
|
||||
body.append(
|
||||
"file",
|
||||
await gzip(
|
||||
ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump(),
|
||||
),
|
||||
gzip(ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump()),
|
||||
"traces.json.gz",
|
||||
);
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,7 @@ import {
|
||||
RemoteParticipant,
|
||||
} from "livekit-client";
|
||||
import * as ComponentsCore from "@livekit/components-core";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import { CallViewModel, Layout } from "./CallViewModel";
|
||||
import {
|
||||
|
||||
@@ -672,7 +672,7 @@ export class CallViewModel extends ViewModel {
|
||||
this.gridModeUserSelection.next(value);
|
||||
}
|
||||
|
||||
private readonly gridLayoutMedia: Observable<GridLayoutMedia> = combineLatest(
|
||||
private readonly gridLayout: Observable<LayoutMedia> = combineLatest(
|
||||
[this.grid, this.spotlight],
|
||||
(grid, spotlight) => ({
|
||||
type: "grid",
|
||||
@@ -683,28 +683,28 @@ export class CallViewModel extends ViewModel {
|
||||
}),
|
||||
);
|
||||
|
||||
private readonly spotlightLandscapeLayoutMedia: Observable<SpotlightLandscapeLayoutMedia> =
|
||||
private readonly spotlightLandscapeLayout: Observable<LayoutMedia> =
|
||||
combineLatest([this.grid, this.spotlight], (grid, spotlight) => ({
|
||||
type: "spotlight-landscape",
|
||||
spotlight,
|
||||
grid,
|
||||
}));
|
||||
|
||||
private readonly spotlightPortraitLayoutMedia: Observable<SpotlightPortraitLayoutMedia> =
|
||||
private readonly spotlightPortraitLayout: Observable<LayoutMedia> =
|
||||
combineLatest([this.grid, this.spotlight], (grid, spotlight) => ({
|
||||
type: "spotlight-portrait",
|
||||
spotlight,
|
||||
grid,
|
||||
}));
|
||||
|
||||
private readonly spotlightExpandedLayoutMedia: Observable<SpotlightExpandedLayoutMedia> =
|
||||
private readonly spotlightExpandedLayout: Observable<LayoutMedia> =
|
||||
combineLatest([this.spotlight, this.pip], (spotlight, pip) => ({
|
||||
type: "spotlight-expanded",
|
||||
spotlight,
|
||||
pip: pip ?? undefined,
|
||||
}));
|
||||
|
||||
private readonly oneOnOneLayoutMedia: Observable<OneOnOneLayoutMedia | null> =
|
||||
private readonly oneOnOneLayout: Observable<LayoutMedia | null> =
|
||||
this.mediaItems.pipe(
|
||||
map((mediaItems) => {
|
||||
if (mediaItems.length !== 2) return null;
|
||||
@@ -722,8 +722,9 @@ export class CallViewModel extends ViewModel {
|
||||
}),
|
||||
);
|
||||
|
||||
private readonly pipLayoutMedia: Observable<LayoutMedia> =
|
||||
this.spotlight.pipe(map((spotlight) => ({ type: "pip", spotlight })));
|
||||
private readonly pipLayout: Observable<LayoutMedia> = this.spotlight.pipe(
|
||||
map((spotlight) => ({ type: "pip", spotlight })),
|
||||
);
|
||||
|
||||
/**
|
||||
* The media to be used to produce a layout.
|
||||
@@ -736,24 +737,24 @@ export class CallViewModel extends ViewModel {
|
||||
switchMap((gridMode) => {
|
||||
switch (gridMode) {
|
||||
case "grid":
|
||||
return this.oneOnOneLayoutMedia.pipe(
|
||||
return this.oneOnOneLayout.pipe(
|
||||
switchMap((oneOnOne) =>
|
||||
oneOnOne === null ? this.gridLayoutMedia : of(oneOnOne),
|
||||
oneOnOne === null ? this.gridLayout : of(oneOnOne),
|
||||
),
|
||||
);
|
||||
case "spotlight":
|
||||
return this.spotlightExpanded.pipe(
|
||||
switchMap((expanded) =>
|
||||
expanded
|
||||
? this.spotlightExpandedLayoutMedia
|
||||
: this.spotlightLandscapeLayoutMedia,
|
||||
? this.spotlightExpandedLayout
|
||||
: this.spotlightLandscapeLayout,
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
case "narrow":
|
||||
return this.oneOnOneLayoutMedia.pipe(
|
||||
return this.oneOnOneLayout.pipe(
|
||||
switchMap((oneOnOne) =>
|
||||
oneOnOne === null
|
||||
? combineLatest(
|
||||
@@ -761,12 +762,12 @@ export class CallViewModel extends ViewModel {
|
||||
(grid, spotlight) =>
|
||||
grid.length > smallMobileCallThreshold ||
|
||||
spotlight.some((vm) => vm instanceof ScreenShareViewModel)
|
||||
? this.spotlightPortraitLayoutMedia
|
||||
: this.gridLayoutMedia,
|
||||
? this.spotlightPortraitLayout
|
||||
: this.gridLayout,
|
||||
).pipe(switchAll())
|
||||
: // The expanded spotlight layout makes for a better one-on-one
|
||||
// experience in narrow windows
|
||||
this.spotlightExpandedLayoutMedia,
|
||||
this.spotlightExpandedLayout,
|
||||
),
|
||||
);
|
||||
case "flat":
|
||||
@@ -776,14 +777,14 @@ export class CallViewModel extends ViewModel {
|
||||
case "grid":
|
||||
// Yes, grid mode actually gets you a "spotlight" layout in
|
||||
// this window mode.
|
||||
return this.spotlightLandscapeLayoutMedia;
|
||||
return this.spotlightLandscapeLayout;
|
||||
case "spotlight":
|
||||
return this.spotlightExpandedLayoutMedia;
|
||||
return this.spotlightExpandedLayout;
|
||||
}
|
||||
}),
|
||||
);
|
||||
case "pip":
|
||||
return this.pipLayoutMedia;
|
||||
return this.pipLayout;
|
||||
}
|
||||
}),
|
||||
this.scope.state(),
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
VisibilityOnIcon,
|
||||
UserProfileIcon,
|
||||
ExpandIcon,
|
||||
VolumeOffSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -63,7 +62,6 @@ interface TileProps {
|
||||
interface UserMediaTileProps extends TileProps {
|
||||
vm: UserMediaViewModel;
|
||||
mirror: boolean;
|
||||
locallyMuted: boolean;
|
||||
menuStart?: ReactNode;
|
||||
menuEnd?: ReactNode;
|
||||
}
|
||||
@@ -73,7 +71,6 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
{
|
||||
vm,
|
||||
showSpeakingIndicators,
|
||||
locallyMuted,
|
||||
menuStart,
|
||||
menuEnd,
|
||||
className,
|
||||
@@ -99,16 +96,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
);
|
||||
const { raisedHands, lowerHand, reactions } = useReactions();
|
||||
|
||||
const AudioIcon = locallyMuted
|
||||
? VolumeOffSolidIcon
|
||||
: audioEnabled
|
||||
? MicOnSolidIcon
|
||||
: MicOffSolidIcon;
|
||||
const audioIconLabel = locallyMuted
|
||||
? t("video_tile.muted_for_me")
|
||||
: audioEnabled
|
||||
? t("microphone_on")
|
||||
: t("microphone_off");
|
||||
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menu = (
|
||||
@@ -146,11 +134,11 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
[styles.handRaised]: !showSpeaking && !!handRaised,
|
||||
})}
|
||||
nameTagLeadingIcon={
|
||||
<AudioIcon
|
||||
<MicIcon
|
||||
width={20}
|
||||
height={20}
|
||||
aria-label={audioIconLabel}
|
||||
data-muted={locallyMuted || !audioEnabled}
|
||||
aria-label={audioEnabled ? t("microphone_on") : t("microphone_off")}
|
||||
data-muted={!audioEnabled}
|
||||
className={styles.muteIcon}
|
||||
/>
|
||||
}
|
||||
@@ -211,7 +199,6 @@ const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
|
||||
<UserMediaTile
|
||||
ref={ref}
|
||||
vm={vm}
|
||||
locallyMuted={false}
|
||||
mirror={mirror}
|
||||
menuStart={
|
||||
<ToggleMenuItem
|
||||
@@ -268,7 +255,6 @@ const RemoteUserMediaTile = forwardRef<
|
||||
<UserMediaTile
|
||||
ref={ref}
|
||||
vm={vm}
|
||||
locallyMuted={locallyMuted}
|
||||
mirror={false}
|
||||
menuStart={
|
||||
<>
|
||||
|
||||
@@ -195,12 +195,11 @@ export const ReactionsProvider = ({
|
||||
// Skip any event without a sender or event ID.
|
||||
if (!sender || !reactionEventId) return;
|
||||
|
||||
room.client
|
||||
.decryptEventIfNeeded(event)
|
||||
.catch((e) => logger.warn(`Failed to decrypt ${event.getId()}`, e));
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) return;
|
||||
|
||||
if (event.getType() === ElementCallReactionEventType) {
|
||||
room.client
|
||||
.decryptEventIfNeeded(event)
|
||||
.catch((e) => logger.warn(`Failed to decrypt ${event.getId()}`, e));
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) return;
|
||||
const content: ECallReactionEventContent = event.getContent();
|
||||
|
||||
const membershipEventId = content?.["m.relates_to"]?.event_id;
|
||||
|
||||
@@ -123,7 +123,6 @@ export async function initClient(
|
||||
localTimeoutMs: 5000,
|
||||
useE2eForGroupCall: e2eEnabled,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
store: new MemoryStore(),
|
||||
});
|
||||
|
||||
// In case of logging in a new matrix account but there is still crypto local store. This is needed for:
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ICreateClientOpts } from "matrix-js-sdk/src/client";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { Config } from "../config/Config";
|
||||
import { fallbackICEServerAllowed, initClient } from "./matrix";
|
||||
import type { InitResult, Session } from "../ClientContext";
|
||||
|
||||
export async function initSPA(
|
||||
loadSession: () => Session | undefined,
|
||||
clearSession: () => void,
|
||||
): Promise<InitResult | null> {
|
||||
// We're running as a standalone application
|
||||
try {
|
||||
const session = loadSession();
|
||||
if (!session) {
|
||||
logger.log("No session stored; continuing without a client");
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.log("Using a standalone client");
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } = session;
|
||||
const initClientParams: ICreateClientOpts = {
|
||||
baseUrl: Config.defaultHomeserverUrl()!,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed,
|
||||
livekitServiceURL: Config.get().livekit?.livekit_service_url,
|
||||
};
|
||||
|
||||
try {
|
||||
const client = await initClient(initClientParams, true);
|
||||
return {
|
||||
widgetApi: null,
|
||||
client,
|
||||
passwordlessUser,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof MatrixError && err.errcode === "M_UNKNOWN_TOKEN") {
|
||||
// We can't use this session anymore, so let's log it out
|
||||
logger.log(
|
||||
"The session from local store is invalid; continuing without a client",
|
||||
);
|
||||
clearSession();
|
||||
// returning null = "no client` pls register" (undefined = "loading" which is the current value when reaching this line)
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import "global-jsdom/register";
|
||||
import "@formatjs/intl-durationformat/polyfill";
|
||||
import "@formatjs/intl-segmenter/polyfill";
|
||||
import i18n from "i18next";
|
||||
import posthog from "posthog-js";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
@@ -16,7 +14,6 @@ import { cleanup } from "@testing-library/react";
|
||||
import "vitest-axe/extend-expect";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import EN_GB from "../public/locales/en-GB/app.json";
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
// Bare-minimum i18n config
|
||||
@@ -25,13 +22,6 @@ i18n
|
||||
.init({
|
||||
lng: "en-GB",
|
||||
fallbackLng: "en-GB",
|
||||
supportedLngs: ["en-GB"],
|
||||
// We embed the translations, so that it never needs to fetch
|
||||
resources: {
|
||||
"en-GB": {
|
||||
app: EN_GB,
|
||||
},
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false, // React has built-in XSS protections
|
||||
},
|
||||
|
||||
@@ -6,10 +6,8 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import { compression } from "vite-plugin-compression2";
|
||||
import svgrPlugin from "vite-plugin-svgr";
|
||||
import htmlTemplate from "vite-plugin-html-template";
|
||||
import { codecovVitePlugin } from "@codecov/vite-plugin";
|
||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import basicSsl from "@vitejs/plugin-basic-ssl";
|
||||
@@ -33,16 +31,6 @@ export default defineConfig(({ mode }) => {
|
||||
title: env.VITE_PRODUCT_NAME || "Element Call",
|
||||
},
|
||||
}),
|
||||
|
||||
codecovVitePlugin({
|
||||
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
|
||||
bundleName: "element-call",
|
||||
uploadToken: process.env.CODECOV_TOKEN,
|
||||
}),
|
||||
|
||||
compression({
|
||||
exclude: [/config.json/],
|
||||
}),
|
||||
];
|
||||
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user