Enable lint rules for Promise handling to discourage misuse of them. (#2607)

* Enable lint rules for Promise handling to discourage misuse of them.
Squashed all of Hugh's commits into one.

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
This commit is contained in:
Timo
2024-09-10 09:49:35 +02:00
committed by GitHub
parent c30c8ac7d6
commit c3edd3e25e
35 changed files with 369 additions and 241 deletions

View File

@@ -39,6 +39,12 @@ module.exports = {
// We should use the js-sdk logger, never console directly. // We should use the js-sdk logger, never console directly.
"no-console": ["error"], "no-console": ["error"],
"react/display-name": "error", "react/display-name": "error",
// Encourage proper usage of Promises:
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/await-thenable": "error",
}, },
settings: { settings: {
react: { react: {

View File

@@ -15,6 +15,7 @@ import {
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { History } from "history"; import { History } from "history";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { HomePage } from "./home/HomePage"; import { HomePage } from "./home/HomePage";
import { LoginPage } from "./auth/LoginPage"; import { LoginPage } from "./auth/LoginPage";
@@ -61,11 +62,13 @@ interface AppProps {
export const App: FC<AppProps> = ({ history }) => { export const App: FC<AppProps> = ({ history }) => {
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
Initializer.init()?.then(() => { Initializer.init()
if (loaded) return; ?.then(async () => {
setLoaded(true); if (loaded) return;
widget?.api.sendContentLoaded(); setLoaded(true);
}); await widget?.api.sendContentLoaded();
})
.catch(logger.error);
}); });
const errorPage = <CrashView />; const errorPage = <CrashView />;

View File

@@ -31,7 +31,7 @@ export const UserMenuContainer: FC<Props> = ({ preventNavigation = false }) => {
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab); const [settingsTab, setSettingsTab] = useState(defaultSettingsTab);
const onAction = useCallback( const onAction = useCallback(
async (value: string) => { (value: string) => {
switch (value) { switch (value) {
case "user": case "user":
setSettingsTab("profile"); setSettingsTab("profile");

View File

@@ -256,7 +256,7 @@ export class PosthogAnalytics {
this.posthog.identify(analyticsID); this.posthog.identify(analyticsID);
} else { } else {
logger.info( logger.info(
"No analyticsID is availble. Should not try to setup posthog", "No analyticsID is available. Should not try to setup posthog",
); );
} }
} }
@@ -324,7 +324,9 @@ export class PosthogAnalytics {
} }
public onLoginStatusChanged(): void { public onLoginStatusChanged(): void {
this.maybeIdentifyUser(); this.maybeIdentifyUser().catch(() =>
logger.log("Could not identify user on login status change"),
);
} }
private updateSuperProperties(): void { private updateSuperProperties(): void {
@@ -373,20 +375,27 @@ export class PosthogAnalytics {
} }
} }
public async trackEvent<E extends IPosthogEvent>( public trackEvent<E extends IPosthogEvent>(
{ eventName, ...properties }: E, { eventName, ...properties }: E,
options?: CaptureOptions, options?: CaptureOptions,
): Promise<void> { ): void {
const doCapture = (): void => {
if (
this.anonymity == Anonymity.Disabled ||
this.anonymity == Anonymity.Anonymous
)
return;
this.capture(eventName, properties, options);
};
if (this.identificationPromise) { if (this.identificationPromise) {
// only make calls to posthog after the identificaion is done // only make calls to posthog after the identification is done
await this.identificationPromise; this.identificationPromise.then(doCapture, (e) => {
logger.error("Failed to identify user for tracking", e);
});
} else {
doCapture();
} }
if (
this.anonymity == Anonymity.Disabled ||
this.anonymity == Anonymity.Anonymous
)
return;
this.capture(eventName, properties, options);
} }
private startListeningToSettingsChanges(): void { private startListeningToSettingsChanges(): void {
@@ -400,7 +409,9 @@ export class PosthogAnalytics {
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes) // won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
optInAnalytics.value.subscribe((optIn) => { optInAnalytics.value.subscribe((optIn) => {
this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled); this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled);
this.maybeIdentifyUser(); this.maybeIdentifyUser().catch(() =>
logger.log("Could not identify user"),
);
}); });
} }

View File

@@ -34,7 +34,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
public onStart(span: Span): void { public onStart(span: Span): void {
// Hack: Yield to allow attributes to be set before processing // Hack: Yield to allow attributes to be set before processing
Promise.resolve().then(() => { try {
switch (span.name) { switch (span.name) {
case "matrix.groupCallMembership": case "matrix.groupCallMembership":
this.onGroupCallMembershipStart(span); this.onGroupCallMembershipStart(span);
@@ -43,7 +43,10 @@ export class PosthogSpanProcessor implements SpanProcessor {
this.onSummaryReportStart(span); this.onSummaryReportStart(span);
return; return;
} }
}); } catch (e) {
// log to avoid tripping @typescript-eslint/no-unused-vars
logger.debug(e);
}
} }
public onEnd(span: ReadableSpan): void { public onEnd(span: ReadableSpan): void {
@@ -148,7 +151,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
/** /**
* Shutdown the processor. * Shutdown the processor.
*/ */
public shutdown(): Promise<void> { public async shutdown(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
} }

View File

@@ -40,7 +40,7 @@ export function useInteractiveLogin(
const interactiveAuth = new InteractiveAuth({ const interactiveAuth = new InteractiveAuth({
matrixClient: authClient, matrixClient: authClient,
doRequest: (): Promise<LoginResponse> => doRequest: async (): Promise<LoginResponse> =>
authClient.login("m.login.password", { authClient.login("m.login.password", {
identifier: { identifier: {
type: "m.id.user", type: "m.id.user",
@@ -49,9 +49,8 @@ export function useInteractiveLogin(
password, password,
}), }),
stateUpdated: (): void => {}, stateUpdated: (): void => {},
requestEmailToken: (): Promise<{ sid: string }> => { requestEmailToken: async (): Promise<{ sid: string }> =>
return Promise.resolve({ sid: "" }); Promise.resolve({ sid: "" }),
},
}); });
// XXX: This claims to return an IAuthData which contains none of these // XXX: This claims to return an IAuthData which contains none of these

View File

@@ -12,6 +12,7 @@ import {
MatrixClient, MatrixClient,
RegisterResponse, RegisterResponse,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { initClient } from "../utils/matrix"; import { initClient } from "../utils/matrix";
import { Session } from "../ClientContext"; import { Session } from "../ClientContext";
@@ -66,7 +67,7 @@ export const useInteractiveRegistration = (
): Promise<[MatrixClient, Session]> => { ): Promise<[MatrixClient, Session]> => {
const interactiveAuth = new InteractiveAuth({ const interactiveAuth = new InteractiveAuth({
matrixClient: authClient.current!, matrixClient: authClient.current!,
doRequest: (auth): Promise<RegisterResponse> => doRequest: async (auth): Promise<RegisterResponse> =>
authClient.current!.registerRequest({ authClient.current!.registerRequest({
username, username,
password, password,
@@ -78,19 +79,26 @@ export const useInteractiveRegistration = (
} }
if (nextStage === "m.login.terms") { if (nextStage === "m.login.terms") {
interactiveAuth.submitAuthDict({ interactiveAuth
type: "m.login.terms", .submitAuthDict({
}); type: "m.login.terms",
})
.catch((e) => {
logger.error(e);
});
} else if (nextStage === "m.login.recaptcha") { } else if (nextStage === "m.login.recaptcha") {
interactiveAuth.submitAuthDict({ interactiveAuth
type: "m.login.recaptcha", .submitAuthDict({
response: recaptchaResponse, type: "m.login.recaptcha",
}); response: recaptchaResponse,
})
.catch((e) => {
logger.error(e);
});
} }
}, },
requestEmailToken: (): Promise<{ sid: string }> => { requestEmailToken: async (): Promise<{ sid: string }> =>
return Promise.resolve({ sid: "dummy" }); Promise.resolve({ sid: "dummy" }),
},
}); });
// XXX: This claims to return an IAuthData which contains none of these // XXX: This claims to return an IAuthData which contains none of these

View File

@@ -63,7 +63,7 @@ export function useRecaptcha(sitekey?: string): {
} }
}, [recaptchaId, sitekey]); }, [recaptchaId, sitekey]);
const execute = useCallback((): Promise<string> => { const execute = useCallback(async (): Promise<string> => {
if (!sitekey) { if (!sitekey) {
return Promise.resolve(""); return Promise.resolve("");
} }
@@ -95,7 +95,12 @@ export function useRecaptcha(sitekey?: string): {
}, },
}; };
window.grecaptcha.execute(); window.grecaptcha.execute().then(
() => {}, // noop
(e) => {
logger.error("Recaptcha execution failed", e);
},
);
const iframe = document.querySelector<HTMLIFrameElement>( const iframe = document.querySelector<HTMLIFrameElement>(
'iframe[src*="recaptcha/api2/bframe"]', 'iframe[src*="recaptcha/api2/bframe"]',

View File

@@ -13,7 +13,7 @@ import {
} from "./ConfigOptions"; } from "./ConfigOptions";
export class Config { export class Config {
private static internalInstance: Config; private static internalInstance: Config | undefined;
public static get(): ConfigOptions { public static get(): ConfigOptions {
if (!this.internalInstance?.config) if (!this.internalInstance?.config)
@@ -21,23 +21,23 @@ export class Config {
return this.internalInstance.config; return this.internalInstance.config;
} }
public static init(): Promise<void> { public static async init(): Promise<void> {
if (Config.internalInstance?.initPromise) { if (!Config.internalInstance?.initPromise) {
return Config.internalInstance.initPromise; const internalInstance = new Config();
} Config.internalInstance = internalInstance;
Config.internalInstance = new Config();
Config.internalInstance.initPromise = new Promise<void>((resolve) => { Config.internalInstance.initPromise = downloadConfig(
downloadConfig("../config.json").then((config) => { "../config.json",
Config.internalInstance.config = { ...DEFAULT_CONFIG, ...config }; ).then((config) => {
resolve(); internalInstance.config = { ...DEFAULT_CONFIG, ...config };
}); });
}); }
return Config.internalInstance.initPromise; return Config.internalInstance.initPromise;
} }
/** /**
* This is a alternative initializer that does not load anything * This is a alternative initializer that does not load anything
* from a hosted config file but instead just initializes the conifg using the * from a hosted config file but instead just initializes the config using the
* default config. * default config.
* *
* It is supposed to only be used in tests. (It is executed in `vite.setup.js`) * It is supposed to only be used in tests. (It is executed in `vite.setup.js`)

View File

@@ -46,19 +46,25 @@ export class MatrixKeyProvider extends BaseKeyProvider {
} }
} }
private onEncryptionKeyChanged = async ( private onEncryptionKeyChanged = (
encryptionKey: Uint8Array, encryptionKey: Uint8Array,
encryptionKeyIndex: number, encryptionKeyIndex: number,
participantId: string, participantId: string,
): Promise<void> => { ): void => {
this.onSetEncryptionKey( createKeyMaterialFromBuffer(encryptionKey).then(
await createKeyMaterialFromBuffer(encryptionKey), (keyMaterial) => {
participantId, this.onSetEncryptionKey(keyMaterial, participantId, encryptionKeyIndex);
encryptionKeyIndex,
);
logger.debug( logger.debug(
`Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex}`, `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex}`,
);
},
(e) => {
logger.error(
`Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex}`,
e,
);
},
); );
}; };
} }

View File

@@ -32,6 +32,7 @@ import {
} from "react"; } from "react";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import classNames from "classnames"; import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import styles from "./Grid.module.css"; import styles from "./Grid.module.css";
import { useMergedRefs } from "../useMergedRefs"; import { useMergedRefs } from "../useMergedRefs";
@@ -353,7 +354,7 @@ export function Grid<
// Because we're using react-spring in imperative mode, we're responsible for // Because we're using react-spring in imperative mode, we're responsible for
// firing animations manually whenever the tiles array updates // firing animations manually whenever the tiles array updates
useEffect(() => { useEffect(() => {
springRef.start(); springRef.start().forEach((p) => void p.catch(logger.error));
}, [placedTiles, springRef]); }, [placedTiles, springRef]);
const animateDraggedTile = ( const animateDraggedTile = (
@@ -390,7 +391,8 @@ export function Grid<
((key): boolean => ((key): boolean =>
key === "zIndex" || key === "x" || key === "y"), key === "zIndex" || key === "x" || key === "y"),
}, },
); )
.catch(logger.error);
if (endOfGesture) if (endOfGesture)
callback({ callback({

View File

@@ -22,7 +22,7 @@ describe("CallList", () => {
); );
}; };
it("should show room", async () => { it("should show room", () => {
const rooms = [ const rooms = [
{ {
roomName: "Room #1", roomName: "Room #1",

View File

@@ -10,6 +10,7 @@ import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend"; import Backend from "i18next-http-backend";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { logger } from "matrix-js-sdk/src/logger";
import { getUrlParams } from "./UrlParams"; import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
@@ -73,6 +74,9 @@ export class Initializer {
order: ["urlFragment", "navigator"], order: ["urlFragment", "navigator"],
caches: [], caches: [],
}, },
})
.catch((e) => {
logger.error("Failed to initialize i18n", e);
}); });
// Custom Themeing // Custom Themeing
@@ -120,10 +124,15 @@ export class Initializer {
// config // config
if (this.loadStates.config === LoadState.None) { if (this.loadStates.config === LoadState.None) {
this.loadStates.config = LoadState.Loading; this.loadStates.config = LoadState.Loading;
Config.init().then(() => { Config.init().then(
this.loadStates.config = LoadState.Loaded; () => {
this.initStep(resolve); this.loadStates.config = LoadState.Loaded;
}); this.initStep(resolve);
},
(e) => {
logger.error("Failed to load config", e);
},
);
} }
//sentry (only initialize after the config is ready) //sentry (only initialize after the config is ready)

View File

@@ -40,12 +40,18 @@ export function useOpenIDSFU(
const activeFocus = useActiveLivekitFocus(rtcSession); const activeFocus = useActiveLivekitFocus(rtcSession);
useEffect(() => { useEffect(() => {
(async (): Promise<void> => { if (activeFocus) {
const sfuConfig = activeFocus getSFUConfigWithOpenID(client, activeFocus).then(
? await getSFUConfigWithOpenID(client, activeFocus) (sfuConfig) => {
: undefined; setSFUConfig(sfuConfig);
setSFUConfig(sfuConfig); },
})(); (e) => {
logger.error("Failed to get SFU config", e);
},
);
} else {
setSFUConfig(undefined);
}
}, [client, activeFocus]); }, [client, activeFocus]);
return sfuConfig; return sfuConfig;

View File

@@ -36,7 +36,7 @@ export enum ECAddonConnectionState {
// We are switching from one focus to another (or between livekit room aliases on the same focus) // We are switching from one focus to another (or between livekit room aliases on the same focus)
ECSwitchingFocus = "ec_switching_focus", ECSwitchingFocus = "ec_switching_focus",
// The call has just been initialised and is waiting for credentials to arrive before attempting // The call has just been initialised and is waiting for credentials to arrive before attempting
// to connect. This distinguishes from the 'Disconected' state which is now just for when livekit // to connect. This distinguishes from the 'Disconnected' state which is now just for when livekit
// gives up on connectivity and we consider the call to have failed. // gives up on connectivity and we consider the call to have failed.
ECWaiting = "ec_waiting", ECWaiting = "ec_waiting",
} }
@@ -151,9 +151,13 @@ async function connectAndPublish(
`Publishing ${screenshareTracks.length} precreated screenshare tracks`, `Publishing ${screenshareTracks.length} precreated screenshare tracks`,
); );
for (const st of screenshareTracks) { for (const st of screenshareTracks) {
livekitRoom.localParticipant.publishTrack(st, { livekitRoom.localParticipant
source: Track.Source.ScreenShare, .publishTrack(st, {
}); source: Track.Source.ScreenShare,
})
.catch((e) => {
logger.error("Failed to publish screenshare track", e);
});
} }
} }
@@ -231,7 +235,9 @@ export function useECConnectionState(
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`, `SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`,
); );
doFocusSwitch(); doFocusSwitch().catch((e) => {
logger.error("Failed to switch focus", e);
});
} else if ( } else if (
!sfuConfigValid(currentSFUConfig.current) && !sfuConfigValid(currentSFUConfig.current) &&
sfuConfigValid(sfuConfig) sfuConfigValid(sfuConfig)
@@ -248,7 +254,11 @@ export function useECConnectionState(
sfuConfig!, sfuConfig!,
initialAudioEnabled, initialAudioEnabled,
initialAudioOptions, initialAudioOptions,
).finally(() => setIsInDoConnect(false)); )
.catch((e) => {
logger.error("Failed to connect to SFU", e);
})
.finally(() => setIsInDoConnect(false));
} }
currentSFUConfig.current = Object.assign({}, sfuConfig); currentSFUConfig.current = Object.assign({}, sfuConfig);

View File

@@ -67,9 +67,11 @@ export function useLiveKit(
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) { if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
(e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession); (e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession);
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) { } else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( (e2eeOptions.keyProvider as ExternalE2EEKeyProvider)
e2eeSystem.secret, .setKey(e2eeSystem.secret)
); .catch((e) => {
logger.error("Failed to set shared key for E2EE", e);
});
} }
}, [e2eeOptions, e2eeSystem, rtcSession]); }, [e2eeOptions, e2eeSystem, rtcSession]);
@@ -112,7 +114,9 @@ export function useLiveKit(
// useEffect() with an argument that references itself, if E2EE is enabled // useEffect() with an argument that references itself, if E2EE is enabled
const room = useMemo(() => { const room = useMemo(() => {
const r = new Room(roomOptions); const r = new Room(roomOptions);
r.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE); r.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE).catch((e) => {
logger.error("Failed to set E2EE enabled on room", e);
});
return r; return r;
}, [roomOptions, e2eeSystem]); }, [roomOptions, e2eeSystem]);
@@ -217,7 +221,7 @@ export function useLiveKit(
// itself we need might need to update the mute state right away. // itself we need might need to update the mute state right away.
// This async recursion makes sure that setCamera/MicrophoneEnabled is // This async recursion makes sure that setCamera/MicrophoneEnabled is
// called as little times as possible. // called as little times as possible.
syncMuteState(iterCount + 1, type); await syncMuteState(iterCount + 1, type);
} else { } else {
throw new Error( throw new Error(
"track with new mute state could not be published", "track with new mute state could not be published",
@@ -226,7 +230,7 @@ export function useLiveKit(
} catch (e) { } catch (e) {
if ((e as DOMException).name === "NotAllowedError") { if ((e as DOMException).name === "NotAllowedError") {
logger.error( logger.error(
"Fatal errror while syncing mute state: resetting", "Fatal error while syncing mute state: resetting",
e, e,
); );
if (type === MuteDevice.Microphone) { if (type === MuteDevice.Microphone) {
@@ -241,14 +245,25 @@ export function useLiveKit(
"Failed to sync audio mute state with LiveKit (will retry to sync in 1s):", "Failed to sync audio mute state with LiveKit (will retry to sync in 1s):",
e, e,
); );
setTimeout(() => syncMuteState(iterCount + 1, type), 1000); setTimeout(() => {
syncMuteState(iterCount + 1, type).catch((e) => {
logger.error(
`Failed to sync ${MuteDevice[type]} mute state with LiveKit iterCount=${iterCount + 1}`,
e,
);
});
}, 1000);
} }
} }
} }
}; };
syncMuteState(0, MuteDevice.Microphone); syncMuteState(0, MuteDevice.Microphone).catch((e) => {
syncMuteState(0, MuteDevice.Camera); logger.error("Failed to sync audio mute state with LiveKit", e);
});
syncMuteState(0, MuteDevice.Camera).catch((e) => {
logger.error("Failed to sync video mute state with LiveKit", e);
});
} }
}, [room, muteStates, connectionState]); }, [room, muteStates, connectionState]);
@@ -295,7 +310,10 @@ export function useLiveKit(
// the deviceId hasn't changed (was & still is default). // the deviceId hasn't changed (was & still is default).
room.localParticipant room.localParticipant
.getTrackPublication(Track.Source.Microphone) .getTrackPublication(Track.Source.Microphone)
?.audioTrack?.restartTrack(); ?.audioTrack?.restartTrack()
.catch((e) => {
logger.error(`Failed to restart audio device track`, e);
});
} }
} else { } else {
if (id !== undefined && room.getActiveDevice(kind) !== id) { if (id !== undefined && room.getActiveDevice(kind) !== id) {

View File

@@ -25,7 +25,10 @@ import { App } from "./App";
import { init as initRageshake } from "./settings/rageshake"; import { init as initRageshake } from "./settings/rageshake";
import { Initializer } from "./initializer"; import { Initializer } from "./initializer";
initRageshake(); initRageshake().catch((e) => {
logger.error("Failed to initialize rageshake", e);
});
setLogLevel("debug"); setLogLevel("debug");
setLKLogExtension(global.mx_rage_logger.log); setLKLogExtension(global.mx_rage_logger.log);

View File

@@ -90,7 +90,9 @@ export class ElementCallOpenTelemetry {
public dispose(): void { public dispose(): void {
opentelemetry.trace.disable(); opentelemetry.trace.disable();
this._provider?.shutdown(); this._provider?.shutdown().catch((e) => {
logger.error("Failed to shutdown OpenTelemetry", e);
});
} }
public get isOtlpEnabled(): boolean { public get isOtlpEnabled(): boolean {

View File

@@ -138,11 +138,7 @@ export const GroupCallView: FC<Props> = ({
if (audioInput === null) { if (audioInput === null) {
latestMuteStates.current!.audio.setEnabled?.(false); latestMuteStates.current!.audio.setEnabled?.(false);
} else { } else {
const deviceId = await findDeviceByName( const deviceId = findDeviceByName(audioInput, "audioinput", devices);
audioInput,
"audioinput",
devices,
);
if (!deviceId) { if (!deviceId) {
logger.warn("Unknown audio input: " + audioInput); logger.warn("Unknown audio input: " + audioInput);
latestMuteStates.current!.audio.setEnabled?.(false); latestMuteStates.current!.audio.setEnabled?.(false);
@@ -158,11 +154,7 @@ export const GroupCallView: FC<Props> = ({
if (videoInput === null) { if (videoInput === null) {
latestMuteStates.current!.video.setEnabled?.(false); latestMuteStates.current!.video.setEnabled?.(false);
} else { } else {
const deviceId = await findDeviceByName( const deviceId = findDeviceByName(videoInput, "videoinput", devices);
videoInput,
"videoinput",
devices,
);
if (!deviceId) { if (!deviceId) {
logger.warn("Unknown video input: " + videoInput); logger.warn("Unknown video input: " + videoInput);
latestMuteStates.current!.video.setEnabled?.(false); latestMuteStates.current!.video.setEnabled?.(false);
@@ -178,24 +170,27 @@ export const GroupCallView: FC<Props> = ({
if (widget && preload && skipLobby) { if (widget && preload && skipLobby) {
// In preload mode without lobby we wait for a join action before entering // In preload mode without lobby we wait for a join action before entering
const onJoin = async ( const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
ev: CustomEvent<IWidgetApiRequest>, (async (): Promise<void> => {
): Promise<void> => { await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData); await enterRTCSession(rtcSession, perParticipantE2EE);
await enterRTCSession(rtcSession, perParticipantE2EE); widget!.api.transport.reply(ev.detail, {});
await widget!.api.transport.reply(ev.detail, {}); })().catch((e) => {
logger.error("Error joining RTC session", e);
});
}; };
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin); widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
return (): void => { return (): void => {
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
}; };
} else if (widget && !preload && skipLobby) { } else if (widget && !preload && skipLobby) {
const join = async (): Promise<void> => { // No lobby and no preload: we enter the rtc session right away
(async (): Promise<void> => {
await defaultDeviceSetup({ audioInput: null, videoInput: null }); await defaultDeviceSetup({ audioInput: null, videoInput: null });
await enterRTCSession(rtcSession, perParticipantE2EE); await enterRTCSession(rtcSession, perParticipantE2EE);
}; })().catch((e) => {
// No lobby and no preload: we enter the RTC Session right away. logger.error("Error joining RTC session", e);
join(); });
} }
}, [rtcSession, preload, skipLobby, perParticipantE2EE]); }, [rtcSession, preload, skipLobby, perParticipantE2EE]);
@@ -204,7 +199,7 @@ export const GroupCallView: FC<Props> = ({
const history = useHistory(); const history = useHistory();
const onLeave = useCallback( const onLeave = useCallback(
async (leaveError?: Error) => { (leaveError?: Error): void => {
setLeaveError(leaveError); setLeaveError(leaveError);
setLeft(true); setLeft(true);
@@ -218,15 +213,19 @@ export const GroupCallView: FC<Props> = ({
); );
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts. // Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
await leaveRTCSession(rtcSession); leaveRTCSession(rtcSession)
.then(() => {
if ( if (
!isPasswordlessUser && !isPasswordlessUser &&
!confineToRoom && !confineToRoom &&
!PosthogAnalytics.instance.isEnabled() !PosthogAnalytics.instance.isEnabled()
) { ) {
history.push("/"); history.push("/");
} }
})
.catch((e) => {
logger.error("Error leaving RTC session", e);
});
}, },
[rtcSession, isPasswordlessUser, confineToRoom, history], [rtcSession, isPasswordlessUser, confineToRoom, history],
); );
@@ -234,14 +233,16 @@ export const GroupCallView: FC<Props> = ({
useEffect(() => { useEffect(() => {
if (widget && isJoined) { if (widget && isJoined) {
// set widget to sticky once joined. // set widget to sticky once joined.
widget!.api.setAlwaysOnScreen(true); widget!.api.setAlwaysOnScreen(true).catch((e) => {
logger.error("Error calling setAlwaysOnScreen(true)", e);
});
const onHangup = async ( const onHangup = (ev: CustomEvent<IWidgetApiRequest>): void => {
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
widget!.api.transport.reply(ev.detail, {}); widget!.api.transport.reply(ev.detail, {});
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts. // Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
await leaveRTCSession(rtcSession); leaveRTCSession(rtcSession).catch((e) => {
logger.error("Failed to leave RTC session", e);
});
}; };
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup); widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
return (): void => { return (): void => {
@@ -253,7 +254,9 @@ export const GroupCallView: FC<Props> = ({
const onReconnect = useCallback(() => { const onReconnect = useCallback(() => {
setLeft(false); setLeft(false);
setLeaveError(undefined); setLeaveError(undefined);
enterRTCSession(rtcSession, perParticipantE2EE); enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
logger.error("Error re-entering RTC session on reconnect", e);
});
}, [rtcSession, perParticipantE2EE]); }, [rtcSession, perParticipantE2EE]);
const joinRule = useJoinRule(rtcSession.room); const joinRule = useJoinRule(rtcSession.room);

View File

@@ -29,6 +29,7 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import classNames from "classnames"; import classNames from "classnames";
import { BehaviorSubject, of } from "rxjs"; import { BehaviorSubject, of } from "rxjs";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import { logger } from "matrix-js-sdk/src/logger";
import LogoMark from "../icons/LogoMark.svg?react"; import LogoMark from "../icons/LogoMark.svg?react";
import LogoType from "../icons/LogoType.svg?react"; import LogoType from "../icons/LogoType.svg?react";
@@ -100,7 +101,9 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
useEffect(() => { useEffect(() => {
return (): void => { return (): void => {
livekitRoom?.disconnect(); livekitRoom?.disconnect().catch((e) => {
logger.error("Failed to disconnect from livekit room", e);
});
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -296,12 +299,16 @@ export const InCallView: FC<InCallViewProps> = ({
); );
useEffect(() => { useEffect(() => {
widget?.api.transport.send( widget?.api.transport
gridMode === "grid" .send(
? ElementWidgetActions.TileLayout gridMode === "grid"
: ElementWidgetActions.SpotlightLayout, ? ElementWidgetActions.TileLayout
{}, : ElementWidgetActions.SpotlightLayout,
); {},
)
.catch((e) => {
logger.error("Failed to send layout change to widget API", e);
});
}, [gridMode]); }, [gridMode]);
useEffect(() => { useEffect(() => {
@@ -461,13 +468,15 @@ export const InCallView: FC<InCallViewProps> = ({
rtcSession.room.roomId, rtcSession.room.roomId,
); );
const toggleScreensharing = useCallback(async () => { const toggleScreensharing = useCallback(() => {
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, { localParticipant
audio: true, .setScreenShareEnabled(!isScreenShareEnabled, {
selfBrowserSurface: "include", audio: true,
surfaceSwitching: "include", selfBrowserSurface: "include",
systemAudio: "include", surfaceSwitching: "include",
}); systemAudio: "include",
})
.catch(logger.error);
}, [localParticipant, isScreenShareEnabled]); }, [localParticipant, isScreenShareEnabled]);
let footer: JSX.Element | null; let footer: JSX.Element | null;

View File

@@ -13,6 +13,7 @@ import {
useMemo, useMemo,
} from "react"; } from "react";
import { IWidgetApiRequest } from "matrix-widget-api"; import { IWidgetApiRequest } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger";
import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext"; import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
@@ -74,10 +75,14 @@ export function useMuteStates(): MuteStates {
const video = useMuteState(devices.videoInput, () => true); const video = useMuteState(devices.videoInput, () => true);
useEffect(() => { useEffect(() => {
widget?.api.transport.send(ElementWidgetActions.DeviceMute, { widget?.api.transport
audio_enabled: audio.enabled, .send(ElementWidgetActions.DeviceMute, {
video_enabled: video.enabled, audio_enabled: audio.enabled,
}); video_enabled: video.enabled,
})
.catch((e) =>
logger.warn("Could not send DeviceMute action to widget", e),
);
}, [audio, video]); }, [audio, video]);
const onMuteStateChangeRequest = useCallback( const onMuteStateChangeRequest = useCallback(

View File

@@ -59,9 +59,13 @@ export const RoomPage: FC = () => {
// a URL param, automatically register a passwordless user // a URL param, automatically register a passwordless user
if (!loading && !authenticated && displayName && !widget) { if (!loading && !authenticated && displayName && !widget) {
setIsRegistering(true); setIsRegistering(true);
registerPasswordlessUser(displayName).finally(() => { registerPasswordlessUser(displayName)
setIsRegistering(false); .catch((e) => {
}); logger.error("Failed to register passwordless user", e);
})
.finally(() => {
setIsRegistering(false);
});
} }
}, [ }, [
loading, loading,

View File

@@ -150,23 +150,22 @@ export const useLoadGroupCall = (
viaServers: string[], viaServers: string[],
onKnockSent: () => void, onKnockSent: () => void,
): Promise<Room> => { ): Promise<Room> => {
let joinedRoom: Room | null = null;
await client.knockRoom(roomId, { viaServers }); await client.knockRoom(roomId, { viaServers });
onKnockSent(); onKnockSent();
const invitePromise = new Promise<void>((resolve, reject) => { return await new Promise<Room>((resolve, reject) => {
client.on( client.on(
RoomEvent.MyMembership, RoomEvent.MyMembership,
async (room, membership, prevMembership) => { (room, membership, prevMembership): void => {
if (roomId !== room.roomId) return; if (roomId !== room.roomId) return;
activeRoom.current = room; activeRoom.current = room;
if ( if (
membership === KnownMembership.Invite && membership === KnownMembership.Invite &&
prevMembership === KnownMembership.Knock prevMembership === KnownMembership.Knock
) { ) {
await client.joinRoom(room.roomId, { viaServers }); client.joinRoom(room.roomId, { viaServers }).then((room) => {
joinedRoom = room; logger.log("Auto-joined %s", room.roomId);
logger.log("Auto-joined %s", room.roomId); resolve(room);
resolve(); }, reject);
} }
if (membership === KnownMembership.Ban) reject(bannedError()); if (membership === KnownMembership.Ban) reject(bannedError());
if (membership === KnownMembership.Leave) if (membership === KnownMembership.Leave)
@@ -174,11 +173,6 @@ export const useLoadGroupCall = (
}, },
); );
}); });
await invitePromise;
if (!joinedRoom) {
throw new Error("Failed to join room after knocking.");
}
return joinedRoom;
}; };
const fetchOrCreateRoom = async (): Promise<Room> => { const fetchOrCreateRoom = async (): Promise<Room> => {
@@ -308,7 +302,7 @@ export const useLoadGroupCall = (
const observeMyMembership = async (): Promise<void> => { const observeMyMembership = async (): Promise<void> => {
await new Promise((_, reject) => { await new Promise((_, reject) => {
client.on(RoomEvent.MyMembership, async (_, membership) => { client.on(RoomEvent.MyMembership, (_, membership) => {
if (membership === KnownMembership.Leave) reject(removeNoticeError()); if (membership === KnownMembership.Leave) reject(removeNoticeError());
if (membership === KnownMembership.Ban) reject(bannedError()); if (membership === KnownMembership.Ban) reject(bannedError());
}); });

View File

@@ -74,8 +74,7 @@ async function makePreferredLivekitFoci(
`No livekit_service_url is configured so we could not create a focus. `No livekit_service_url is configured so we could not create a focus.
Currently we skip computing a focus based on other users in the room.`, Currently we skip computing a focus based on other users in the room.`,
); );
return Promise.resolve(preferredFoci);
return preferredFoci;
// TODO: we want to do something like this: // TODO: we want to do something like this:
// //
@@ -119,13 +118,18 @@ const widgetPostHangupProcedure = async (
// we need to wait until the callEnded event is tracked on posthog. // we need to wait until the callEnded event is tracked on posthog.
// Otherwise the iFrame gets killed before the callEnded event got tracked. // Otherwise the iFrame gets killed before the callEnded event got tracked.
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
widget.api.setAlwaysOnScreen(false);
PosthogAnalytics.instance.logout(); PosthogAnalytics.instance.logout();
try {
await widget.api.setAlwaysOnScreen(false);
} catch (e) {
logger.error("Failed to set call widget `alwaysOnScreen` to false", e);
}
// We send the hangup event after the memberships have been updated // We send the hangup event after the memberships have been updated
// calling leaveRTCSession. // calling leaveRTCSession.
// We need to wait because this makes the client hosting this widget killing the IFrame. // We need to wait because this makes the client hosting this widget killing the IFrame.
widget.api.transport.send(ElementWidgetActions.HangupCall, {}); await widget.api.transport.send(ElementWidgetActions.HangupCall, {});
}; };
export async function leaveRTCSession( export async function leaveRTCSession(

View File

@@ -9,6 +9,7 @@ import { FC, useCallback } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake";
@@ -41,6 +42,8 @@ export const FeedbackSettingsTab: FC<Props> = ({ roomId }) => {
sendLogs, sendLogs,
rageshakeRequestId, rageshakeRequestId,
roomId, roomId,
}).catch((e) => {
logger.error("Failed to send feedback rageshake", e);
}); });
if (roomId && sendLogs) { if (roomId && sendLogs) {

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import { FC, useCallback, useEffect, useMemo, useRef } from "react"; import { FC, useCallback, useEffect, useMemo, useRef } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
@@ -61,6 +62,8 @@ export const ProfileSettingsTab: FC<Props> = ({ client }) => {
// @ts-ignore // @ts-ignore
avatar: avatar && avatarSize > 0 ? avatar : undefined, avatar: avatar && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0), removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0),
}).catch((e) => {
logger.error("Failed to save profile", e);
}); });
} }
}; };

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FC, useCallback } from "react"; import { FC, useCallback } from "react";
import { Button } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import styles from "./RageshakeButton.module.css"; import styles from "./RageshakeButton.module.css";
@@ -25,6 +26,8 @@ export const RageshakeButton: FC<Props> = ({ description }) => {
submitRageshake({ submitRageshake({
description, description,
sendLogs: true, sendLogs: true,
}).catch((e) => {
logger.error("Failed to send rageshake", e);
}); });
}, [submitRageshake, description]); }, [submitRageshake, description]);

View File

@@ -128,13 +128,17 @@ class IndexedDBLogStore {
this.id = "instance-" + randomString(16); this.id = "instance-" + randomString(16);
loggerInstance.on(ConsoleLoggerEvent.Log, this.onLoggerLog); loggerInstance.on(ConsoleLoggerEvent.Log, this.onLoggerLog);
window.addEventListener("beforeunload", this.flush); window.addEventListener("beforeunload", () => {
this.flush().catch((e) =>
logger.error("Failed to flush logs before unload", e),
);
});
} }
/** /**
* @return {Promise} Resolves when the store is ready. * @return {Promise} Resolves when the store is ready.
*/ */
public connect(): Promise<void> { public async connect(): Promise<void> {
const req = this.indexedDB.open("logs"); const req = this.indexedDB.open("logs");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.onsuccess = (): void => { req.onsuccess = (): void => {
@@ -190,16 +194,10 @@ class IndexedDBLogStore {
// Throttled function to flush logs. We use throttle rather // Throttled function to flush logs. We use throttle rather
// than debounce as we want logs to be written regularly, otherwise // than debounce as we want logs to be written regularly, otherwise
// if there's a constant stream of logging, we'd never write anything. // if there's a constant stream of logging, we'd never write anything.
private throttledFlush = throttle( private throttledFlush = throttle(() => this.flush, MAX_FLUSH_INTERVAL_MS, {
() => { leading: false,
this.flush(); trailing: true,
}, });
MAX_FLUSH_INTERVAL_MS,
{
leading: false,
trailing: true,
},
);
/** /**
* Flush logs to disk. * Flush logs to disk.
@@ -220,7 +218,7 @@ class IndexedDBLogStore {
* *
* @return {Promise} Resolved when the logs have been flushed. * @return {Promise} Resolved when the logs have been flushed.
*/ */
public flush = (): Promise<void> => { public flush = async (): Promise<void> => {
// check if a flush() operation is ongoing // check if a flush() operation is ongoing
if (this.flushPromise) { if (this.flushPromise) {
if (this.flushAgainPromise) { if (this.flushAgainPromise) {
@@ -228,13 +226,9 @@ class IndexedDBLogStore {
return this.flushAgainPromise; return this.flushAgainPromise;
} }
// queue up a flush to occur immediately after the pending one completes. // queue up a flush to occur immediately after the pending one completes.
this.flushAgainPromise = this.flushPromise this.flushAgainPromise = this.flushPromise.then(this.flush).then(() => {
.then(() => { this.flushAgainPromise = undefined;
return this.flush(); });
})
.then(() => {
this.flushAgainPromise = undefined;
});
return this.flushAgainPromise; return this.flushAgainPromise;
} }
// there is no flush promise or there was but it has finished, so do // there is no flush promise or there was but it has finished, so do
@@ -286,7 +280,7 @@ class IndexedDBLogStore {
// Returns: a string representing the concatenated logs for this ID. // Returns: a string representing the concatenated logs for this ID.
// Stops adding log fragments when the size exceeds maxSize // Stops adding log fragments when the size exceeds maxSize
function fetchLogs(id: string, maxSize: number): Promise<string> { async function fetchLogs(id: string, maxSize: number): Promise<string> {
const objectStore = db! const objectStore = db!
.transaction("logs", "readonly") .transaction("logs", "readonly")
.objectStore("logs"); .objectStore("logs");
@@ -316,7 +310,7 @@ class IndexedDBLogStore {
} }
// Returns: A sorted array of log IDs. (newest first) // Returns: A sorted array of log IDs. (newest first)
function fetchLogIds(): Promise<string[]> { async function fetchLogIds(): Promise<string[]> {
// To gather all the log IDs, query for all records in logslastmod. // To gather all the log IDs, query for all records in logslastmod.
const o = db! const o = db!
.transaction("logslastmod", "readonly") .transaction("logslastmod", "readonly")
@@ -336,7 +330,7 @@ class IndexedDBLogStore {
}); });
} }
function deleteLogs(id: number): Promise<void> { async function deleteLogs(id: number): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const txn = db!.transaction(["logs", "logslastmod"], "readwrite"); const txn = db!.transaction(["logs", "logslastmod"], "readwrite");
const o = txn.objectStore("logs"); const o = txn.objectStore("logs");
@@ -394,7 +388,7 @@ class IndexedDBLogStore {
logger.log("Removing logs: ", removeLogIds); logger.log("Removing logs: ", removeLogIds);
// Don't await this because it's non-fatal if we can't clean up // Don't await this because it's non-fatal if we can't clean up
// logs. // logs.
Promise.all(removeLogIds.map((id) => deleteLogs(id))).then( Promise.all(removeLogIds.map(async (id) => deleteLogs(id))).then(
() => { () => {
logger.log(`Removed ${removeLogIds.length} old logs.`); logger.log(`Removed ${removeLogIds.length} old logs.`);
}, },
@@ -432,7 +426,7 @@ class IndexedDBLogStore {
* @return {Promise<T[]>} Resolves to an array of whatever you returned from * @return {Promise<T[]>} Resolves to an array of whatever you returned from
* resultMapper. * resultMapper.
*/ */
function selectQuery<T>( async function selectQuery<T>(
store: IDBObjectStore, store: IDBObjectStore,
keyRange: IDBKeyRange | undefined, keyRange: IDBKeyRange | undefined,
resultMapper: (cursor: IDBCursorWithValue) => T, resultMapper: (cursor: IDBCursorWithValue) => T,
@@ -461,7 +455,7 @@ declare global {
// eslint-disable-next-line no-var, camelcase // eslint-disable-next-line no-var, camelcase
var mx_rage_logger: ConsoleLogger; var mx_rage_logger: ConsoleLogger;
// eslint-disable-next-line no-var, camelcase // eslint-disable-next-line no-var, camelcase
var mx_rage_initStoragePromise: Promise<void>; var mx_rage_initStoragePromise: Promise<void> | undefined;
} }
/** /**
@@ -471,7 +465,7 @@ declare global {
* be set up immediately for the logs. * be set up immediately for the logs.
* @return {Promise} Resolves when set up. * @return {Promise} Resolves when set up.
*/ */
export function init(): Promise<void> { export async function init(): Promise<void> {
global.mx_rage_logger = new ConsoleLogger(); global.mx_rage_logger = new ConsoleLogger();
setLogExtension(global.mx_rage_logger.log); setLogExtension(global.mx_rage_logger.log);
@@ -483,7 +477,7 @@ export function init(): Promise<void> {
* then this no-ops. * then this no-ops.
* @return {Promise} Resolves when complete. * @return {Promise} Resolves when complete.
*/ */
function tryInitStorage(): Promise<void> { async function tryInitStorage(): Promise<void> {
if (global.mx_rage_initStoragePromise) { if (global.mx_rage_initStoragePromise) {
return global.mx_rage_initStoragePromise; return global.mx_rage_initStoragePromise;
} }

View File

@@ -300,10 +300,14 @@ export function useRageshakeRequest(): (
const sendRageshakeRequest = useCallback( const sendRageshakeRequest = useCallback(
(roomId: string, rageshakeRequestId: string) => { (roomId: string, rageshakeRequestId: string) => {
// @ts-expect-error - org.matrix.rageshake_request is not part of `keyof TimelineEvents` but it is okay to sent a custom event. client!
client!.sendEvent(roomId, "org.matrix.rageshake_request", { // @ts-expect-error - org.matrix.rageshake_request is not part of `keyof TimelineEvents` but it is okay to sent a custom event.
request_id: rageshakeRequestId, .sendEvent(roomId, "org.matrix.rageshake_request", {
}); request_id: rageshakeRequestId,
})
.catch((e) => {
logger.error("Failed to send org.matrix.rageshake_request event", e);
});
}, },
[client], [client],
); );

View File

@@ -279,7 +279,7 @@ export class CallViewModel extends ViewModel {
}); });
}), }),
// Then unhold them // Then unhold them
]).then(() => Promise.resolve({ unhold: ps })), ]).then(() => ({ unhold: ps })),
); );
} else { } else {
return EMPTY; return EMPTY;

View File

@@ -19,19 +19,22 @@ export function useWakeLock(): void {
// The lock is automatically released whenever the window goes invisible, // The lock is automatically released whenever the window goes invisible,
// so we need to reacquire it on visibility changes // so we need to reacquire it on visibility changes
const onVisibilityChange = async (): Promise<void> => { const onVisibilityChange = (): void => {
if (document.visibilityState === "visible") { if (document.visibilityState === "visible") {
try { navigator.wakeLock.request("screen").then(
lock = await navigator.wakeLock.request("screen"); (newLock) => {
// Handle the edge case where this component unmounts before the lock = newLock;
// promise resolves // Handle the edge case where this component unmounts before the
if (!mounted) // promise resolves
lock if (!mounted)
.release() lock
.catch((e) => logger.warn("Can't release wake lock", e)); .release()
} catch (e) { .catch((e) => logger.warn("Can't release wake lock", e));
logger.warn("Can't acquire wake lock", e); },
} (e) => {
logger.warn("Can't acquire wake lock", e);
},
);
} }
}; };

View File

@@ -260,34 +260,40 @@ export async function createRoom(
}); });
// Wait for the room to arrive // Wait for the room to arrive
await new Promise<void>((resolve, reject) => { const roomId = await new Promise<string>((resolve, reject) => {
const onRoom = async (room: Room): Promise<void> => {
if (room.roomId === (await createPromise).room_id) {
resolve();
cleanUp();
}
};
createPromise.catch((e) => { createPromise.catch((e) => {
reject(e); reject(e);
cleanUp(); cleanUp();
}); });
const onRoom = (room: Room): void => {
createPromise.then(
(result) => {
if (room.roomId === result.room_id) {
resolve(room.roomId);
cleanUp();
}
},
(e) => {
logger.error("Failed to wait for the room to arrive", e);
},
);
};
const cleanUp = (): void => { const cleanUp = (): void => {
client.off(ClientEvent.Room, onRoom); client.off(ClientEvent.Room, onRoom);
}; };
client.on(ClientEvent.Room, onRoom); client.on(ClientEvent.Room, onRoom);
}); });
const result = await createPromise; let password: string | undefined;
let password;
if (e2ee == E2eeType.SHARED_KEY) { if (e2ee == E2eeType.SHARED_KEY) {
password = secureRandomBase64Url(16); password = secureRandomBase64Url(16);
saveKeyForRoom(result.room_id, password); saveKeyForRoom(roomId, password);
} }
return { return {
roomId: result.room_id, roomId,
alias: e2ee ? undefined : fullAliasFromRoomName(name, client), alias: e2ee ? undefined : fullAliasFromRoomName(name, client),
password, password,
}; };

View File

@@ -11,11 +11,11 @@ Please see LICENSE in the repository root for full details.
* @param devices The list of devices to search * @param devices The list of devices to search
* @returns A matching media device or undefined if no matching device was found * @returns A matching media device or undefined if no matching device was found
*/ */
export async function findDeviceByName( export function findDeviceByName(
deviceName: string, deviceName: string,
kind: MediaDeviceKind, kind: MediaDeviceKind,
devices: MediaDeviceInfo[], devices: MediaDeviceInfo[],
): Promise<string | undefined> { ): string | undefined {
const deviceInfo = devices.find( const deviceInfo = devices.find(
(d) => d.kind === kind && d.label === deviceName, (d) => d.kind === kind && d.label === deviceName,
); );

View File

@@ -11,17 +11,21 @@ import posthog from "posthog-js";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import { afterEach, beforeEach } from "vitest"; import { afterEach, beforeEach } from "vitest";
import { cleanup } from "@testing-library/react"; import { cleanup } from "@testing-library/react";
import { logger } from "matrix-js-sdk/src/logger";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
// Bare-minimum i18n config // Bare-minimum i18n config
i18n.use(initReactI18next).init({ i18n
lng: "en-GB", .use(initReactI18next)
fallbackLng: "en-GB", .init({
interpolation: { lng: "en-GB",
escapeValue: false, // React has built-in XSS protections fallbackLng: "en-GB",
}, interpolation: {
}); escapeValue: false, // React has built-in XSS protections
},
})
.catch((e) => logger.warn("Failed to init i18n for testing", e));
Config.initDefault(); Config.initDefault();
posthog.opt_out_capturing(); posthog.opt_out_capturing();

View File

@@ -158,17 +158,15 @@ export const widget = ((): WidgetHelpers | null => {
false, false,
); );
const clientPromise = new Promise<MatrixClient>((resolve) => { const clientPromise = async (): Promise<MatrixClient> => {
(async (): Promise<void> => { // Wait for the config file to be ready (we load very early on so it might not
// Wait for the config file to be ready (we load very early on so it might not // be otherwise)
// be otherwise) await Config.init();
await Config.init(); await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 }); return client;
resolve(client); };
})();
});
return { api, lazyActions, client: clientPromise }; return { api, lazyActions, client: clientPromise() };
} else { } else {
if (import.meta.env.MODE !== "test") if (import.meta.env.MODE !== "test")
logger.info("No widget API available"); logger.info("No widget API available");