From 480a995be17ef86b60dcd8d6558fb51cdfaccf8c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 10 Jul 2024 16:20:59 +0100 Subject: [PATCH] Enable lint rules for Promise handling to discourage misuse of them. Squashed all of Hugh's commits into one. --- .eslintrc.cjs | 6 ++ src/App.tsx | 15 +++-- src/UserMenuContainer.tsx | 2 +- src/analytics/PosthogAnalytics.ts | 37 ++++++++---- src/analytics/PosthogSpanProcessor.ts | 29 +++++---- src/auth/useInteractiveLogin.ts | 4 +- src/auth/useInteractiveRegistration.ts | 27 ++++++--- src/auth/useRecaptcha.ts | 9 ++- src/config/Config.ts | 18 +++--- src/e2ee/matrixKeyProvider.ts | 25 ++++---- src/initializer.tsx | 16 +++-- src/livekit/openIDSFU.ts | 4 +- src/livekit/useECConnectionState.ts | 22 +++++-- src/livekit/useLiveKit.ts | 38 ++++++++---- src/main.tsx | 5 +- src/otel/otel.ts | 4 +- src/room/GroupCallView.tsx | 83 ++++++++++++++------------ src/room/InCallView.tsx | 25 +++++--- src/room/RoomPage.tsx | 10 +++- src/room/useLoadGroupCall.ts | 16 +++-- src/rtcSessionHelpers.ts | 5 +- src/settings/FeedbackSettingsTab.tsx | 3 + src/settings/ProfileSettingsTab.tsx | 3 + src/settings/RageshakeButton.tsx | 3 + src/settings/rageshake.ts | 42 +++++++------ src/settings/submit-rageshake.ts | 12 ++-- src/state/CallViewModel.ts | 2 +- src/useWakeLock.ts | 27 +++++---- src/utils/matrix.ts | 16 +++-- src/utils/media.ts | 4 +- src/widget.ts | 18 +++--- 31 files changed, 332 insertions(+), 198 deletions(-) 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 fc12af39..285a48ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,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"; @@ -70,11 +71,15 @@ 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((e) => { + logger.error(e); + }); }); const errorPage = ; diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index 87149c22..764b48c2 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -40,7 +40,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 05979a89..8d39d344 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -265,7 +265,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", ); } } @@ -333,7 +333,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 { @@ -382,20 +384,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).catch((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 { @@ -409,7 +418,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 59960c92..5a74aeff 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -43,16 +43,20 @@ export class PosthogSpanProcessor implements SpanProcessor { public onStart(span: Span): void { // Hack: Yield to allow attributes to be set before processing - Promise.resolve().then(() => { - switch (span.name) { - case "matrix.groupCallMembership": - this.onGroupCallMembershipStart(span); - return; - case "matrix.groupCallMembership.summaryReport": - this.onSummaryReportStart(span); - return; - } - }); + Promise.resolve() + .then(() => { + switch (span.name) { + case "matrix.groupCallMembership": + this.onGroupCallMembershipStart(span); + return; + case "matrix.groupCallMembership.summaryReport": + this.onSummaryReportStart(span); + return; + } + }) + .catch((e) => { + // noop + }); } public onEnd(span: ReadableSpan): void { @@ -157,7 +161,8 @@ export class PosthogSpanProcessor implements SpanProcessor { /** * Shutdown the processor. */ - public shutdown(): Promise { - return Promise.resolve(); + // eslint-disable-next-line @typescript-eslint/require-await + public async shutdown(): Promise { + return; } } diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index 7877b285..e2ecb438 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -49,7 +49,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", @@ -58,7 +58,7 @@ export function useInteractiveLogin( password, }), stateUpdated: (): void => {}, - requestEmailToken: (): Promise<{ sid: string }> => { + requestEmailToken: async (): Promise<{ sid: string }> => { return Promise.resolve({ sid: "" }); }, }); diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index 6b753363..d8eaf6e7 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -21,6 +21,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"; @@ -75,7 +76,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, @@ -87,17 +88,25 @@ 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 }> => { + requestEmailToken: async (): Promise<{ sid: string }> => { return Promise.resolve({ sid: "dummy" }); }, }); diff --git a/src/auth/useRecaptcha.ts b/src/auth/useRecaptcha.ts index 00daa18c..ff409a93 100644 --- a/src/auth/useRecaptcha.ts +++ b/src/auth/useRecaptcha.ts @@ -72,7 +72,7 @@ export function useRecaptcha(sitekey?: string): { } }, [recaptchaId, sitekey]); - const execute = useCallback((): Promise => { + const execute = useCallback(async (): Promise => { if (!sitekey) { return Promise.resolve(""); } @@ -104,7 +104,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 382e7eab..342bc2a6 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -30,17 +30,21 @@ export class Config { return this.internalInstance.config; } - public static init(): Promise { + public static async 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(); - }); - }); + Config.internalInstance.initPromise = new Promise( + (resolve, reject) => { + downloadConfig("../config.json") + .then((config) => { + Config.internalInstance.config = { ...DEFAULT_CONFIG, ...config }; + resolve(); + }) + .catch(reject); + }, + ); return Config.internalInstance.initPromise; } diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index 7fac8193..d8b8b1a7 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -55,19 +55,24 @@ 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}`, + ); + }) + .catch((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/initializer.tsx b/src/initializer.tsx index 42a84663..22aa5ded 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -19,6 +19,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"; @@ -82,6 +83,9 @@ export class Initializer { order: ["urlFragment", "navigator"], caches: [], }, + }) + .catch((e) => { + logger.error("Failed to initialize i18n", e); }); // Custom Themeing @@ -129,10 +133,14 @@ 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); + }) + .catch((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 949eea90..2ebf4cd3 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -54,7 +54,9 @@ export function useOpenIDSFU( ? await getSFUConfigWithOpenID(client, activeFocus) : undefined; setSFUConfig(sfuConfig); - })(); + })().catch((e) => { + logger.error("Failed to get SFU config", e); + }); }, [client, activeFocus]); return sfuConfig; diff --git a/src/livekit/useECConnectionState.ts b/src/livekit/useECConnectionState.ts index 9afb2578..d084d7d3 100644 --- a/src/livekit/useECConnectionState.ts +++ b/src/livekit/useECConnectionState.ts @@ -45,7 +45,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", } @@ -160,9 +160,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); + }); } } @@ -240,7 +244,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) @@ -257,7 +263,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 988dc0f8..33395410 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -76,9 +76,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]); @@ -121,7 +123,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]); @@ -226,7 +230,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", @@ -235,7 +239,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) { @@ -250,14 +254,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]); @@ -304,7 +319,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 ed64a09c..e44ec455 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -34,7 +34,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 14c22cb6..f8a1323e 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -99,7 +99,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 8339e749..49f2cd85 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -147,11 +147,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); @@ -167,11 +163,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); @@ -187,24 +179,31 @@ 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 => { + defaultDeviceSetup(ev.detail.data as unknown as JoinCallData) + .catch((e) => { + logger.error("Error setting up default devices", e); + }) + .then(async () => enterRTCSession(rtcSession, perParticipantE2EE)) + .then(() => widget!.api.transport.reply(ev.detail, {})) + .catch((e) => { + logger.error("Error entering 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 => { - await defaultDeviceSetup({ audioInput: null, videoInput: null }); - await enterRTCSession(rtcSession, perParticipantE2EE); - }; - // No lobby and no preload: we enter the RTC Session right away. - join(); + // No lobby and no preload: we enter the rtc session right away + defaultDeviceSetup({ audioInput: null, videoInput: null }) + .catch((e) => { + logger.error("Error setting up default devices", e); + }) + .then(async () => enterRTCSession(rtcSession, perParticipantE2EE)) + .catch((e) => { + logger.error("Error entering RTC session", e); + }); } }, [rtcSession, preload, skipLobby, perParticipantE2EE]); @@ -213,7 +212,7 @@ export const GroupCallView: FC = ({ const history = useHistory(); const onLeave = useCallback( - async (leaveError?: Error) => { + (leaveError?: Error): void => { setLeaveError(leaveError); setLeft(true); @@ -227,15 +226,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], ); @@ -243,14 +246,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 => { @@ -262,7 +267,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 ebda4271..f9083e06 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -38,6 +38,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"; @@ -109,7 +110,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 }, []); @@ -305,12 +308,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(() => { @@ -470,8 +477,8 @@ export const InCallView: FC = ({ rtcSession.room.roomId, ); - const toggleScreensharing = useCallback(async () => { - await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, { + const toggleScreensharing = useCallback(() => { + void localParticipant.setScreenShareEnabled(!isScreenShareEnabled, { audio: true, selfBrowserSurface: "include", surfaceSwitching: "include", diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 00071331..69bdabbd 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -68,9 +68,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) + .finally(() => { + setIsRegistering(false); + }) + .catch((e) => { + logger.error("Failed to register passwordless user", e); + }); } }, [ loading, diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 685b8833..e0ac4b89 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -165,17 +165,21 @@ export const useLoadGroupCall = ( const invitePromise = 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) => { + joinedRoom = room; + logger.log("Auto-joined %s", room.roomId); + resolve(); + }) + .catch((e) => reject(e)); } if (membership === KnownMembership.Ban) reject(bannedError()); if (membership === KnownMembership.Leave) @@ -317,7 +321,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 451871ea..32068758 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -36,6 +36,7 @@ export function makeActiveFocus(): LivekitFocusActive { }; } +// eslint-disable-next-line @typescript-eslint/require-await async function makePreferredLivekitFoci( rtcSession: MatrixRTCSession, livekitAlias: string, @@ -128,13 +129,13 @@ 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); + await widget.api.setAlwaysOnScreen(false); PosthogAnalytics.instance.logout(); // 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 b1babb0b..207a80fe 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -18,6 +18,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"; @@ -50,6 +51,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 a67fd853..02f05d02 100644 --- a/src/settings/ProfileSettingsTab.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -17,6 +17,7 @@ limitations under the License. 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"; @@ -70,6 +71,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 92c56a52..c06b1e4c 100644 --- a/src/settings/RageshakeButton.tsx +++ b/src/settings/RageshakeButton.tsx @@ -17,6 +17,7 @@ limitations under the License. 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"; @@ -34,6 +35,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 bf6909fb..03d631d8 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -138,13 +138,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 => { @@ -200,16 +204,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. @@ -230,7 +228,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) { @@ -239,7 +237,7 @@ class IndexedDBLogStore { } // queue up a flush to occur immediately after the pending one completes. this.flushAgainPromise = this.flushPromise - .then(() => { + .then(async () => { return this.flush(); }) .then(() => { @@ -296,7 +294,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"); @@ -326,7 +324,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") @@ -346,7 +344,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"); @@ -404,7 +402,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.`); }, @@ -442,7 +440,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, @@ -471,7 +469,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; } /** @@ -481,7 +479,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); @@ -493,7 +491,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 adae3f0d..b12bc8be 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -306,10 +306,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 144b0937..dfedb724 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -288,7 +288,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 7857c532..ff1f5e0a 100644 --- a/src/useWakeLock.ts +++ b/src/useWakeLock.ts @@ -28,19 +28,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)); + }) + .catch((e) => { + logger.warn("Can't acquire wake lock", e); + }); } }; diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index 936a69e2..612a7f4d 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -270,11 +270,17 @@ 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 onRoom = (room: Room): void => { + createPromise + .then((result) => { + if (room.roomId === result.room_id) { + resolve(); + cleanUp(); + } + }) + .catch((e) => { + logger.error("Failed to wait for the room to arrive", e); + }); }; createPromise.catch((e) => { reject(e); diff --git a/src/utils/media.ts b/src/utils/media.ts index 74e5ca33..cfd3ed88 100644 --- a/src/utils/media.ts +++ b/src/utils/media.ts @@ -20,11 +20,11 @@ limitations under the License. * @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/widget.ts b/src/widget.ts index 8ce55ace..658559f6 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -167,17 +167,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");