diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5970790f..3ef03c68 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -39,6 +39,12 @@ module.exports = { // We should use the js-sdk logger, never console directly. "no-console": ["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: { react: { diff --git a/src/App.tsx b/src/App.tsx index 0f87498f..8d841dba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import { import * as Sentry from "@sentry/react"; import { History } from "history"; import { TooltipProvider } from "@vector-im/compound-web"; +import { logger } from "matrix-js-sdk/src/logger"; import { HomePage } from "./home/HomePage"; import { LoginPage } from "./auth/LoginPage"; @@ -61,11 +62,13 @@ interface AppProps { export const App: FC = ({ history }) => { const [loaded, setLoaded] = useState(false); useEffect(() => { - Initializer.init()?.then(() => { - if (loaded) return; - setLoaded(true); - widget?.api.sendContentLoaded(); - }); + Initializer.init() + ?.then(async () => { + if (loaded) return; + setLoaded(true); + await widget?.api.sendContentLoaded(); + }) + .catch(logger.error); }); const errorPage = ; diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index 8a16301d..e73f2780 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -31,7 +31,7 @@ export const UserMenuContainer: FC = ({ preventNavigation = false }) => { const [settingsTab, setSettingsTab] = useState(defaultSettingsTab); const onAction = useCallback( - async (value: string) => { + (value: string) => { switch (value) { case "user": setSettingsTab("profile"); diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 643fb7eb..f66a5ab7 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -256,7 +256,7 @@ export class PosthogAnalytics { this.posthog.identify(analyticsID); } else { 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 { - this.maybeIdentifyUser(); + this.maybeIdentifyUser().catch(() => + logger.log("Could not identify user on login status change"), + ); } private updateSuperProperties(): void { @@ -373,20 +375,27 @@ export class PosthogAnalytics { } } - public async trackEvent( + public trackEvent( { eventName, ...properties }: E, options?: CaptureOptions, - ): Promise { + ): void { + const doCapture = (): void => { + if ( + this.anonymity == Anonymity.Disabled || + this.anonymity == Anonymity.Anonymous + ) + return; + this.capture(eventName, properties, options); + }; + if (this.identificationPromise) { - // only make calls to posthog after the identificaion is done - await this.identificationPromise; + // only make calls to posthog after the identification is done + 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 { @@ -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) optInAnalytics.value.subscribe((optIn) => { this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled); - this.maybeIdentifyUser(); + this.maybeIdentifyUser().catch(() => + logger.log("Could not identify user"), + ); }); } diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index 072e85df..102de159 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -34,7 +34,7 @@ export class PosthogSpanProcessor implements SpanProcessor { public onStart(span: Span): void { // Hack: Yield to allow attributes to be set before processing - Promise.resolve().then(() => { + try { switch (span.name) { case "matrix.groupCallMembership": this.onGroupCallMembershipStart(span); @@ -43,7 +43,10 @@ export class PosthogSpanProcessor implements SpanProcessor { this.onSummaryReportStart(span); return; } - }); + } catch (e) { + // log to avoid tripping @typescript-eslint/no-unused-vars + logger.debug(e); + } } public onEnd(span: ReadableSpan): void { @@ -148,7 +151,7 @@ export class PosthogSpanProcessor implements SpanProcessor { /** * Shutdown the processor. */ - public shutdown(): Promise { + public async shutdown(): Promise { return Promise.resolve(); } } diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index 55b5fa0e..2bd15acb 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -40,7 +40,7 @@ export function useInteractiveLogin( const interactiveAuth = new InteractiveAuth({ matrixClient: authClient, - doRequest: (): Promise => + doRequest: async (): Promise => authClient.login("m.login.password", { identifier: { type: "m.id.user", @@ -49,9 +49,8 @@ export function useInteractiveLogin( password, }), stateUpdated: (): void => {}, - requestEmailToken: (): Promise<{ sid: string }> => { - return Promise.resolve({ sid: "" }); - }, + requestEmailToken: async (): Promise<{ sid: string }> => + Promise.resolve({ sid: "" }), }); // XXX: This claims to return an IAuthData which contains none of these diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index 95b1fd30..2c272cb1 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -12,6 +12,7 @@ import { MatrixClient, RegisterResponse, } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import { initClient } from "../utils/matrix"; import { Session } from "../ClientContext"; @@ -66,7 +67,7 @@ export const useInteractiveRegistration = ( ): Promise<[MatrixClient, Session]> => { const interactiveAuth = new InteractiveAuth({ matrixClient: authClient.current!, - doRequest: (auth): Promise => + doRequest: async (auth): Promise => authClient.current!.registerRequest({ username, password, @@ -78,19 +79,26 @@ export const useInteractiveRegistration = ( } if (nextStage === "m.login.terms") { - interactiveAuth.submitAuthDict({ - type: "m.login.terms", - }); + interactiveAuth + .submitAuthDict({ + type: "m.login.terms", + }) + .catch((e) => { + logger.error(e); + }); } else if (nextStage === "m.login.recaptcha") { - interactiveAuth.submitAuthDict({ - type: "m.login.recaptcha", - response: recaptchaResponse, - }); + interactiveAuth + .submitAuthDict({ + type: "m.login.recaptcha", + response: recaptchaResponse, + }) + .catch((e) => { + logger.error(e); + }); } }, - requestEmailToken: (): Promise<{ sid: string }> => { - return Promise.resolve({ sid: "dummy" }); - }, + requestEmailToken: async (): Promise<{ sid: string }> => + Promise.resolve({ sid: "dummy" }), }); // XXX: This claims to return an IAuthData which contains none of these diff --git a/src/auth/useRecaptcha.ts b/src/auth/useRecaptcha.ts index 7dddf3f9..f6b07c69 100644 --- a/src/auth/useRecaptcha.ts +++ b/src/auth/useRecaptcha.ts @@ -63,7 +63,7 @@ export function useRecaptcha(sitekey?: string): { } }, [recaptchaId, sitekey]); - const execute = useCallback((): Promise => { + const execute = useCallback(async (): Promise => { if (!sitekey) { 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( 'iframe[src*="recaptcha/api2/bframe"]', diff --git a/src/config/Config.ts b/src/config/Config.ts index e21bbe28..941ffc82 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -13,7 +13,7 @@ import { } from "./ConfigOptions"; export class Config { - private static internalInstance: Config; + private static internalInstance: Config | undefined; public static get(): ConfigOptions { if (!this.internalInstance?.config) @@ -21,23 +21,23 @@ export class Config { return this.internalInstance.config; } - public static init(): Promise { - if (Config.internalInstance?.initPromise) { - return Config.internalInstance.initPromise; - } - Config.internalInstance = new Config(); - Config.internalInstance.initPromise = new Promise((resolve) => { - downloadConfig("../config.json").then((config) => { - Config.internalInstance.config = { ...DEFAULT_CONFIG, ...config }; - resolve(); + public static async init(): Promise { + if (!Config.internalInstance?.initPromise) { + const internalInstance = new Config(); + Config.internalInstance = internalInstance; + + Config.internalInstance.initPromise = downloadConfig( + "../config.json", + ).then((config) => { + internalInstance.config = { ...DEFAULT_CONFIG, ...config }; }); - }); + } return Config.internalInstance.initPromise; } /** * 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. * * It is supposed to only be used in tests. (It is executed in `vite.setup.js`) diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index ba2bc37d..d84c3684 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -46,19 +46,25 @@ export class MatrixKeyProvider extends BaseKeyProvider { } } - private onEncryptionKeyChanged = async ( + private onEncryptionKeyChanged = ( encryptionKey: Uint8Array, encryptionKeyIndex: number, participantId: string, - ): Promise => { - this.onSetEncryptionKey( - await createKeyMaterialFromBuffer(encryptionKey), - participantId, - encryptionKeyIndex, - ); + ): void => { + createKeyMaterialFromBuffer(encryptionKey).then( + (keyMaterial) => { + this.onSetEncryptionKey(keyMaterial, participantId, encryptionKeyIndex); - logger.debug( - `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex}`, + logger.debug( + `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, + ); + }, ); }; } diff --git a/src/grid/Grid.tsx b/src/grid/Grid.tsx index 45cb9252..51d258e3 100644 --- a/src/grid/Grid.tsx +++ b/src/grid/Grid.tsx @@ -32,6 +32,7 @@ import { } from "react"; import useMeasure from "react-use-measure"; import classNames from "classnames"; +import { logger } from "matrix-js-sdk/src/logger"; import styles from "./Grid.module.css"; import { useMergedRefs } from "../useMergedRefs"; @@ -353,7 +354,7 @@ export function Grid< // Because we're using react-spring in imperative mode, we're responsible for // firing animations manually whenever the tiles array updates useEffect(() => { - springRef.start(); + springRef.start().forEach((p) => void p.catch(logger.error)); }, [placedTiles, springRef]); const animateDraggedTile = ( @@ -390,7 +391,8 @@ export function Grid< ((key): boolean => key === "zIndex" || key === "x" || key === "y"), }, - ); + ) + .catch(logger.error); if (endOfGesture) callback({ diff --git a/src/home/CallList.test.tsx b/src/home/CallList.test.tsx index 8a853d27..cd9e38d1 100644 --- a/src/home/CallList.test.tsx +++ b/src/home/CallList.test.tsx @@ -22,7 +22,7 @@ describe("CallList", () => { ); }; - it("should show room", async () => { + it("should show room", () => { const rooms = [ { roomName: "Room #1", diff --git a/src/initializer.tsx b/src/initializer.tsx index 3b3b4de5..4bc1dc9f 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -10,6 +10,7 @@ import { initReactI18next } from "react-i18next"; 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 { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; @@ -73,6 +74,9 @@ export class Initializer { order: ["urlFragment", "navigator"], caches: [], }, + }) + .catch((e) => { + logger.error("Failed to initialize i18n", e); }); // Custom Themeing @@ -120,10 +124,15 @@ export class Initializer { // config if (this.loadStates.config === LoadState.None) { this.loadStates.config = LoadState.Loading; - Config.init().then(() => { - this.loadStates.config = LoadState.Loaded; - this.initStep(resolve); - }); + Config.init().then( + () => { + this.loadStates.config = LoadState.Loaded; + this.initStep(resolve); + }, + (e) => { + logger.error("Failed to load config", e); + }, + ); } //sentry (only initialize after the config is ready) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 47ff8321..3c77db2f 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -40,12 +40,18 @@ export function useOpenIDSFU( const activeFocus = useActiveLivekitFocus(rtcSession); useEffect(() => { - (async (): Promise => { - const sfuConfig = activeFocus - ? await getSFUConfigWithOpenID(client, activeFocus) - : undefined; - setSFUConfig(sfuConfig); - })(); + if (activeFocus) { + getSFUConfigWithOpenID(client, activeFocus).then( + (sfuConfig) => { + setSFUConfig(sfuConfig); + }, + (e) => { + logger.error("Failed to get SFU config", e); + }, + ); + } else { + setSFUConfig(undefined); + } }, [client, activeFocus]); return sfuConfig; diff --git a/src/livekit/useECConnectionState.ts b/src/livekit/useECConnectionState.ts index 6fcb4386..60c5b9bb 100644 --- a/src/livekit/useECConnectionState.ts +++ b/src/livekit/useECConnectionState.ts @@ -36,7 +36,7 @@ export enum ECAddonConnectionState { // We are switching from one focus to another (or between livekit room aliases on the same focus) ECSwitchingFocus = "ec_switching_focus", // 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. ECWaiting = "ec_waiting", } @@ -151,9 +151,13 @@ async function connectAndPublish( `Publishing ${screenshareTracks.length} precreated screenshare tracks`, ); for (const st of screenshareTracks) { - livekitRoom.localParticipant.publishTrack(st, { - source: Track.Source.ScreenShare, - }); + livekitRoom.localParticipant + .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}`, ); - doFocusSwitch(); + doFocusSwitch().catch((e) => { + logger.error("Failed to switch focus", e); + }); } else if ( !sfuConfigValid(currentSFUConfig.current) && sfuConfigValid(sfuConfig) @@ -248,7 +254,11 @@ export function useECConnectionState( sfuConfig!, initialAudioEnabled, initialAudioOptions, - ).finally(() => setIsInDoConnect(false)); + ) + .catch((e) => { + logger.error("Failed to connect to SFU", e); + }) + .finally(() => setIsInDoConnect(false)); } currentSFUConfig.current = Object.assign({}, sfuConfig); diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 68ec0be2..4645fab7 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -67,9 +67,11 @@ export function useLiveKit( if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) { (e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession); } else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) { - (e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( - e2eeSystem.secret, - ); + (e2eeOptions.keyProvider as ExternalE2EEKeyProvider) + .setKey(e2eeSystem.secret) + .catch((e) => { + logger.error("Failed to set shared key for E2EE", e); + }); } }, [e2eeOptions, e2eeSystem, rtcSession]); @@ -112,7 +114,9 @@ export function useLiveKit( // useEffect() with an argument that references itself, if E2EE is enabled const room = useMemo(() => { 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; }, [roomOptions, e2eeSystem]); @@ -217,7 +221,7 @@ export function useLiveKit( // itself we need might need to update the mute state right away. // This async recursion makes sure that setCamera/MicrophoneEnabled is // called as little times as possible. - syncMuteState(iterCount + 1, type); + await syncMuteState(iterCount + 1, type); } else { throw new Error( "track with new mute state could not be published", @@ -226,7 +230,7 @@ export function useLiveKit( } catch (e) { if ((e as DOMException).name === "NotAllowedError") { logger.error( - "Fatal errror while syncing mute state: resetting", + "Fatal error while syncing mute state: resetting", e, ); 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):", 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.Camera); + syncMuteState(0, MuteDevice.Microphone).catch((e) => { + 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]); @@ -295,7 +310,10 @@ export function useLiveKit( // the deviceId hasn't changed (was & still is default). room.localParticipant .getTrackPublication(Track.Source.Microphone) - ?.audioTrack?.restartTrack(); + ?.audioTrack?.restartTrack() + .catch((e) => { + logger.error(`Failed to restart audio device track`, e); + }); } } else { if (id !== undefined && room.getActiveDevice(kind) !== id) { diff --git a/src/main.tsx b/src/main.tsx index 263619c9..b3e10985 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -25,7 +25,10 @@ import { App } from "./App"; import { init as initRageshake } from "./settings/rageshake"; import { Initializer } from "./initializer"; -initRageshake(); +initRageshake().catch((e) => { + logger.error("Failed to initialize rageshake", e); +}); + setLogLevel("debug"); setLKLogExtension(global.mx_rage_logger.log); diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 78f7807a..14d31ec8 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -90,7 +90,9 @@ export class ElementCallOpenTelemetry { public dispose(): void { opentelemetry.trace.disable(); - this._provider?.shutdown(); + this._provider?.shutdown().catch((e) => { + logger.error("Failed to shutdown OpenTelemetry", e); + }); } public get isOtlpEnabled(): boolean { diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 25eb7dce..f1986d4b 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -138,11 +138,7 @@ export const GroupCallView: FC = ({ if (audioInput === null) { latestMuteStates.current!.audio.setEnabled?.(false); } else { - const deviceId = await findDeviceByName( - audioInput, - "audioinput", - devices, - ); + const deviceId = findDeviceByName(audioInput, "audioinput", devices); if (!deviceId) { logger.warn("Unknown audio input: " + audioInput); latestMuteStates.current!.audio.setEnabled?.(false); @@ -158,11 +154,7 @@ export const GroupCallView: FC = ({ if (videoInput === null) { latestMuteStates.current!.video.setEnabled?.(false); } else { - const deviceId = await findDeviceByName( - videoInput, - "videoinput", - devices, - ); + const deviceId = findDeviceByName(videoInput, "videoinput", devices); if (!deviceId) { logger.warn("Unknown video input: " + videoInput); latestMuteStates.current!.video.setEnabled?.(false); @@ -178,24 +170,27 @@ export const GroupCallView: FC = ({ if (widget && preload && skipLobby) { // In preload mode without lobby we wait for a join action before entering - const onJoin = async ( - ev: CustomEvent, - ): Promise => { - await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData); - await enterRTCSession(rtcSession, perParticipantE2EE); - await widget!.api.transport.reply(ev.detail, {}); + const onJoin = (ev: CustomEvent): void => { + (async (): Promise => { + 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) { - const join = async (): Promise => { + // No lobby and no preload: we enter the rtc session right away + (async (): Promise => { await defaultDeviceSetup({ audioInput: null, videoInput: null }); await enterRTCSession(rtcSession, perParticipantE2EE); - }; - // No lobby and no preload: we enter the RTC Session right away. - join(); + })().catch((e) => { + logger.error("Error joining RTC session", e); + }); } }, [rtcSession, preload, skipLobby, perParticipantE2EE]); @@ -204,7 +199,7 @@ export const GroupCallView: FC = ({ const history = useHistory(); const onLeave = useCallback( - async (leaveError?: Error) => { + (leaveError?: Error): void => { setLeaveError(leaveError); setLeft(true); @@ -218,15 +213,19 @@ export const GroupCallView: FC = ({ ); // Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts. - await leaveRTCSession(rtcSession); - - if ( - !isPasswordlessUser && - !confineToRoom && - !PosthogAnalytics.instance.isEnabled() - ) { - history.push("/"); - } + leaveRTCSession(rtcSession) + .then(() => { + if ( + !isPasswordlessUser && + !confineToRoom && + !PosthogAnalytics.instance.isEnabled() + ) { + history.push("/"); + } + }) + .catch((e) => { + logger.error("Error leaving RTC session", e); + }); }, [rtcSession, isPasswordlessUser, confineToRoom, history], ); @@ -234,14 +233,16 @@ export const GroupCallView: FC = ({ useEffect(() => { if (widget && isJoined) { // 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 ( - ev: CustomEvent, - ): Promise => { + const onHangup = (ev: CustomEvent): void => { widget!.api.transport.reply(ev.detail, {}); // 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); return (): void => { @@ -253,7 +254,9 @@ export const GroupCallView: FC = ({ const onReconnect = useCallback(() => { setLeft(false); setLeaveError(undefined); - enterRTCSession(rtcSession, perParticipantE2EE); + enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => { + logger.error("Error re-entering RTC session on reconnect", e); + }); }, [rtcSession, perParticipantE2EE]); const joinRule = useJoinRule(rtcSession.room); diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 4191c17f..d50be3c9 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -29,6 +29,7 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import classNames from "classnames"; import { BehaviorSubject, of } from "rxjs"; import { useObservableEagerState } from "observable-hooks"; +import { logger } from "matrix-js-sdk/src/logger"; import LogoMark from "../icons/LogoMark.svg?react"; import LogoType from "../icons/LogoType.svg?react"; @@ -100,7 +101,9 @@ export const ActiveCall: FC = (props) => { useEffect(() => { 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 }, []); @@ -296,12 +299,16 @@ export const InCallView: FC = ({ ); useEffect(() => { - widget?.api.transport.send( - gridMode === "grid" - ? ElementWidgetActions.TileLayout - : ElementWidgetActions.SpotlightLayout, - {}, - ); + widget?.api.transport + .send( + gridMode === "grid" + ? ElementWidgetActions.TileLayout + : ElementWidgetActions.SpotlightLayout, + {}, + ) + .catch((e) => { + logger.error("Failed to send layout change to widget API", e); + }); }, [gridMode]); useEffect(() => { @@ -461,13 +468,15 @@ export const InCallView: FC = ({ rtcSession.room.roomId, ); - const toggleScreensharing = useCallback(async () => { - await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, { - audio: true, - selfBrowserSurface: "include", - surfaceSwitching: "include", - systemAudio: "include", - }); + const toggleScreensharing = useCallback(() => { + localParticipant + .setScreenShareEnabled(!isScreenShareEnabled, { + audio: true, + selfBrowserSurface: "include", + surfaceSwitching: "include", + systemAudio: "include", + }) + .catch(logger.error); }, [localParticipant, isScreenShareEnabled]); let footer: JSX.Element | null; diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index be569aef..80723f01 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -13,6 +13,7 @@ import { useMemo, } from "react"; import { IWidgetApiRequest } from "matrix-widget-api"; +import { logger } from "matrix-js-sdk/src/logger"; import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; @@ -74,10 +75,14 @@ export function useMuteStates(): MuteStates { const video = useMuteState(devices.videoInput, () => true); useEffect(() => { - widget?.api.transport.send(ElementWidgetActions.DeviceMute, { - audio_enabled: audio.enabled, - video_enabled: video.enabled, - }); + widget?.api.transport + .send(ElementWidgetActions.DeviceMute, { + audio_enabled: audio.enabled, + video_enabled: video.enabled, + }) + .catch((e) => + logger.warn("Could not send DeviceMute action to widget", e), + ); }, [audio, video]); const onMuteStateChangeRequest = useCallback( diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 29e44df0..bb32040d 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -59,9 +59,13 @@ export const RoomPage: FC = () => { // a URL param, automatically register a passwordless user if (!loading && !authenticated && displayName && !widget) { setIsRegistering(true); - registerPasswordlessUser(displayName).finally(() => { - setIsRegistering(false); - }); + registerPasswordlessUser(displayName) + .catch((e) => { + logger.error("Failed to register passwordless user", e); + }) + .finally(() => { + setIsRegistering(false); + }); } }, [ loading, diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 03e82cb5..8b758726 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -150,23 +150,22 @@ export const useLoadGroupCall = ( viaServers: string[], onKnockSent: () => void, ): Promise => { - let joinedRoom: Room | null = null; await client.knockRoom(roomId, { viaServers }); onKnockSent(); - const invitePromise = new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { client.on( RoomEvent.MyMembership, - async (room, membership, prevMembership) => { + (room, membership, prevMembership): void => { if (roomId !== room.roomId) return; activeRoom.current = room; if ( membership === KnownMembership.Invite && prevMembership === KnownMembership.Knock ) { - await client.joinRoom(room.roomId, { viaServers }); - joinedRoom = room; - logger.log("Auto-joined %s", room.roomId); - resolve(); + client.joinRoom(room.roomId, { viaServers }).then((room) => { + logger.log("Auto-joined %s", room.roomId); + resolve(room); + }, reject); } if (membership === KnownMembership.Ban) reject(bannedError()); 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 => { @@ -308,7 +302,7 @@ export const useLoadGroupCall = ( const observeMyMembership = async (): Promise => { await new Promise((_, reject) => { - client.on(RoomEvent.MyMembership, async (_, membership) => { + client.on(RoomEvent.MyMembership, (_, membership) => { if (membership === KnownMembership.Leave) reject(removeNoticeError()); if (membership === KnownMembership.Ban) reject(bannedError()); }); diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 0ff00f6c..d7f93ad2 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -74,8 +74,7 @@ async function makePreferredLivekitFoci( `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.`, ); - - return preferredFoci; + return Promise.resolve(preferredFoci); // 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. // Otherwise the iFrame gets killed before the callEnded event got tracked. await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms - widget.api.setAlwaysOnScreen(false); 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 // calling leaveRTCSession. // 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( diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx index bea36884..f47d10ac 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -9,6 +9,7 @@ import { FC, useCallback } from "react"; import { randomString } from "matrix-js-sdk/src/randomstring"; import { useTranslation } from "react-i18next"; import { Button } from "@vector-im/compound-web"; +import { logger } from "matrix-js-sdk/src/logger"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; @@ -41,6 +42,8 @@ export const FeedbackSettingsTab: FC = ({ roomId }) => { sendLogs, rageshakeRequestId, roomId, + }).catch((e) => { + logger.error("Failed to send feedback rageshake", e); }); if (roomId && sendLogs) { diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx index 3c54164b..4eb5b0d9 100644 --- a/src/settings/ProfileSettingsTab.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details. import { FC, useCallback, useEffect, useMemo, useRef } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; +import { logger } from "matrix-js-sdk/src/logger"; import { useProfile } from "../profile/useProfile"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; @@ -61,6 +62,8 @@ export const ProfileSettingsTab: FC = ({ client }) => { // @ts-ignore avatar: avatar && avatarSize > 0 ? avatar : undefined, removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0), + }).catch((e) => { + logger.error("Failed to save profile", e); }); } }; diff --git a/src/settings/RageshakeButton.tsx b/src/settings/RageshakeButton.tsx index 2bbc1d0b..0854da6e 100644 --- a/src/settings/RageshakeButton.tsx +++ b/src/settings/RageshakeButton.tsx @@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details. import { useTranslation } from "react-i18next"; import { FC, useCallback } from "react"; import { Button } from "@vector-im/compound-web"; +import { logger } from "matrix-js-sdk/src/logger"; import { Config } from "../config/Config"; import styles from "./RageshakeButton.module.css"; @@ -25,6 +26,8 @@ export const RageshakeButton: FC = ({ description }) => { submitRageshake({ description, sendLogs: true, + }).catch((e) => { + logger.error("Failed to send rageshake", e); }); }, [submitRageshake, description]); diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index 3d5103ac..e0868b9e 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -128,13 +128,17 @@ class IndexedDBLogStore { this.id = "instance-" + randomString(16); 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. */ - public connect(): Promise { + public async connect(): Promise { const req = this.indexedDB.open("logs"); return new Promise((resolve, reject) => { req.onsuccess = (): void => { @@ -190,16 +194,10 @@ class IndexedDBLogStore { // Throttled function to flush logs. We use throttle rather // than debounce as we want logs to be written regularly, otherwise // if there's a constant stream of logging, we'd never write anything. - private throttledFlush = throttle( - () => { - this.flush(); - }, - MAX_FLUSH_INTERVAL_MS, - { - leading: false, - trailing: true, - }, - ); + private throttledFlush = throttle(() => this.flush, MAX_FLUSH_INTERVAL_MS, { + leading: false, + trailing: true, + }); /** * Flush logs to disk. @@ -220,7 +218,7 @@ class IndexedDBLogStore { * * @return {Promise} Resolved when the logs have been flushed. */ - public flush = (): Promise => { + public flush = async (): Promise => { // check if a flush() operation is ongoing if (this.flushPromise) { if (this.flushAgainPromise) { @@ -228,13 +226,9 @@ class IndexedDBLogStore { return this.flushAgainPromise; } // queue up a flush to occur immediately after the pending one completes. - this.flushAgainPromise = this.flushPromise - .then(() => { - return this.flush(); - }) - .then(() => { - this.flushAgainPromise = undefined; - }); + this.flushAgainPromise = this.flushPromise.then(this.flush).then(() => { + this.flushAgainPromise = undefined; + }); return this.flushAgainPromise; } // 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. // Stops adding log fragments when the size exceeds maxSize - function fetchLogs(id: string, maxSize: number): Promise { + async function fetchLogs(id: string, maxSize: number): Promise { const objectStore = db! .transaction("logs", "readonly") .objectStore("logs"); @@ -316,7 +310,7 @@ class IndexedDBLogStore { } // Returns: A sorted array of log IDs. (newest first) - function fetchLogIds(): Promise { + async function fetchLogIds(): Promise { // To gather all the log IDs, query for all records in logslastmod. const o = db! .transaction("logslastmod", "readonly") @@ -336,7 +330,7 @@ class IndexedDBLogStore { }); } - function deleteLogs(id: number): Promise { + async function deleteLogs(id: number): Promise { return new Promise((resolve, reject) => { const txn = db!.transaction(["logs", "logslastmod"], "readwrite"); const o = txn.objectStore("logs"); @@ -394,7 +388,7 @@ class IndexedDBLogStore { logger.log("Removing logs: ", removeLogIds); // Don't await this because it's non-fatal if we can't clean up // 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.`); }, @@ -432,7 +426,7 @@ class IndexedDBLogStore { * @return {Promise} Resolves to an array of whatever you returned from * resultMapper. */ -function selectQuery( +async function selectQuery( store: IDBObjectStore, keyRange: IDBKeyRange | undefined, resultMapper: (cursor: IDBCursorWithValue) => T, @@ -461,7 +455,7 @@ declare global { // eslint-disable-next-line no-var, camelcase var mx_rage_logger: ConsoleLogger; // eslint-disable-next-line no-var, camelcase - var mx_rage_initStoragePromise: Promise; + var mx_rage_initStoragePromise: Promise | undefined; } /** @@ -471,7 +465,7 @@ declare global { * be set up immediately for the logs. * @return {Promise} Resolves when set up. */ -export function init(): Promise { +export async function init(): Promise { global.mx_rage_logger = new ConsoleLogger(); setLogExtension(global.mx_rage_logger.log); @@ -483,7 +477,7 @@ export function init(): Promise { * then this no-ops. * @return {Promise} Resolves when complete. */ -function tryInitStorage(): Promise { +async function tryInitStorage(): Promise { if (global.mx_rage_initStoragePromise) { return global.mx_rage_initStoragePromise; } diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index f9e10aba..16b81d0b 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -300,10 +300,14 @@ export function useRageshakeRequest(): ( const sendRageshakeRequest = useCallback( (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!.sendEvent(roomId, "org.matrix.rageshake_request", { - request_id: rageshakeRequestId, - }); + client! + // @ts-expect-error - org.matrix.rageshake_request is not part of `keyof TimelineEvents` but it is okay to sent a custom event. + .sendEvent(roomId, "org.matrix.rageshake_request", { + request_id: rageshakeRequestId, + }) + .catch((e) => { + logger.error("Failed to send org.matrix.rageshake_request event", e); + }); }, [client], ); diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 63dd7bde..219df600 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -279,7 +279,7 @@ export class CallViewModel extends ViewModel { }); }), // Then unhold them - ]).then(() => Promise.resolve({ unhold: ps })), + ]).then(() => ({ unhold: ps })), ); } else { return EMPTY; diff --git a/src/useWakeLock.ts b/src/useWakeLock.ts index 9ed73dc9..f22f7c1d 100644 --- a/src/useWakeLock.ts +++ b/src/useWakeLock.ts @@ -19,19 +19,22 @@ export function useWakeLock(): void { // The lock is automatically released whenever the window goes invisible, // so we need to reacquire it on visibility changes - const onVisibilityChange = async (): Promise => { + const onVisibilityChange = (): void => { if (document.visibilityState === "visible") { - try { - lock = await navigator.wakeLock.request("screen"); - // Handle the edge case where this component unmounts before the - // promise resolves - if (!mounted) - lock - .release() - .catch((e) => logger.warn("Can't release wake lock", e)); - } catch (e) { - logger.warn("Can't acquire wake lock", e); - } + navigator.wakeLock.request("screen").then( + (newLock) => { + lock = newLock; + // Handle the edge case where this component unmounts before the + // promise resolves + if (!mounted) + lock + .release() + .catch((e) => logger.warn("Can't release wake lock", e)); + }, + (e) => { + logger.warn("Can't acquire wake lock", e); + }, + ); } }; diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index 056606d8..d3821a3f 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -260,34 +260,40 @@ export async function createRoom( }); // Wait for the room to arrive - await new Promise((resolve, reject) => { - const onRoom = async (room: Room): Promise => { - if (room.roomId === (await createPromise).room_id) { - resolve(); - cleanUp(); - } - }; + const roomId = await new Promise((resolve, reject) => { createPromise.catch((e) => { reject(e); 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 => { client.off(ClientEvent.Room, onRoom); }; client.on(ClientEvent.Room, onRoom); }); - const result = await createPromise; - - let password; + let password: string | undefined; if (e2ee == E2eeType.SHARED_KEY) { password = secureRandomBase64Url(16); - saveKeyForRoom(result.room_id, password); + saveKeyForRoom(roomId, password); } return { - roomId: result.room_id, + roomId, alias: e2ee ? undefined : fullAliasFromRoomName(name, client), password, }; diff --git a/src/utils/media.ts b/src/utils/media.ts index 4a3103eb..1aedc5f3 100644 --- a/src/utils/media.ts +++ b/src/utils/media.ts @@ -11,11 +11,11 @@ Please see LICENSE in the repository root for full details. * @param devices The list of devices to search * @returns A matching media device or undefined if no matching device was found */ -export async function findDeviceByName( +export function findDeviceByName( deviceName: string, kind: MediaDeviceKind, devices: MediaDeviceInfo[], -): Promise { +): string | undefined { const deviceInfo = devices.find( (d) => d.kind === kind && d.label === deviceName, ); diff --git a/src/vitest.setup.ts b/src/vitest.setup.ts index ff6ce268..9b0a1f96 100644 --- a/src/vitest.setup.ts +++ b/src/vitest.setup.ts @@ -11,17 +11,21 @@ import posthog from "posthog-js"; import { initReactI18next } from "react-i18next"; import { afterEach, beforeEach } from "vitest"; import { cleanup } from "@testing-library/react"; +import { logger } from "matrix-js-sdk/src/logger"; import { Config } from "./config/Config"; // Bare-minimum i18n config -i18n.use(initReactI18next).init({ - lng: "en-GB", - fallbackLng: "en-GB", - interpolation: { - escapeValue: false, // React has built-in XSS protections - }, -}); +i18n + .use(initReactI18next) + .init({ + lng: "en-GB", + 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(); posthog.opt_out_capturing(); diff --git a/src/widget.ts b/src/widget.ts index 9bb8e031..f08968b6 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -158,17 +158,15 @@ export const widget = ((): WidgetHelpers | null => { false, ); - const clientPromise = new Promise((resolve) => { - (async (): Promise => { - // Wait for the config file to be ready (we load very early on so it might not - // be otherwise) - await Config.init(); - await client.startClient({ clientWellKnownPollPeriod: 60 * 10 }); - resolve(client); - })(); - }); + const clientPromise = async (): Promise => { + // Wait for the config file to be ready (we load very early on so it might not + // be otherwise) + await Config.init(); + await client.startClient({ clientWellKnownPollPeriod: 60 * 10 }); + return client; + }; - return { api, lazyActions, client: clientPromise }; + return { api, lazyActions, client: clientPromise() }; } else { if (import.meta.env.MODE !== "test") logger.info("No widget API available");