Enable lint rules for Promise handling to discourage misuse of them.

Squashed all of Hugh's commits into one.
This commit is contained in:
Hugh Nimmo-Smith 2024-07-10 16:20:59 +01:00 committed by Timo
parent c2cc0937c1
commit 480a995be1
31 changed files with 332 additions and 198 deletions

View File

@ -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: {

View File

@ -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<AppProps> = ({ 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 = <CrashView />;

View File

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

View File

@ -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<E extends IPosthogEvent>(
public trackEvent<E extends IPosthogEvent>(
{ eventName, ...properties }: E,
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) {
// 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"),
);
});
}

View File

@ -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<void> {
return Promise.resolve();
// eslint-disable-next-line @typescript-eslint/require-await
public async shutdown(): Promise<void> {
return;
}
}

View File

@ -49,7 +49,7 @@ export function useInteractiveLogin(
const interactiveAuth = new InteractiveAuth({
matrixClient: authClient,
doRequest: (): Promise<LoginResponse> =>
doRequest: async (): Promise<LoginResponse> =>
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: "" });
},
});

View File

@ -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<RegisterResponse> =>
doRequest: async (auth): Promise<RegisterResponse> =>
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" });
},
});

View File

@ -72,7 +72,7 @@ export function useRecaptcha(sitekey?: string): {
}
}, [recaptchaId, sitekey]);
const execute = useCallback((): Promise<string> => {
const execute = useCallback(async (): Promise<string> => {
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<HTMLIFrameElement>(
'iframe[src*="recaptcha/api2/bframe"]',

View File

@ -30,17 +30,21 @@ export class Config {
return this.internalInstance.config;
}
public static init(): Promise<void> {
public static async init(): Promise<void> {
if (Config.internalInstance?.initPromise) {
return Config.internalInstance.initPromise;
}
Config.internalInstance = new Config();
Config.internalInstance.initPromise = new Promise<void>((resolve) => {
downloadConfig("../config.json").then((config) => {
Config.internalInstance.config = { ...DEFAULT_CONFIG, ...config };
resolve();
});
});
Config.internalInstance.initPromise = new Promise<void>(
(resolve, reject) => {
downloadConfig("../config.json")
.then((config) => {
Config.internalInstance.config = { ...DEFAULT_CONFIG, ...config };
resolve();
})
.catch(reject);
},
);
return Config.internalInstance.initPromise;
}

View File

@ -55,19 +55,24 @@ export class MatrixKeyProvider extends BaseKeyProvider {
}
}
private onEncryptionKeyChanged = async (
private onEncryptionKeyChanged = (
encryptionKey: Uint8Array,
encryptionKeyIndex: number,
participantId: string,
): Promise<void> => {
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,
);
});
};
}

View File

@ -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)

View File

@ -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;

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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 {

View File

@ -147,11 +147,7 @@ export const GroupCallView: FC<Props> = ({
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<Props> = ({
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<Props> = ({
if (widget && preload && skipLobby) {
// In preload mode without lobby we wait for a join action before entering
const onJoin = async (
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
await enterRTCSession(rtcSession, perParticipantE2EE);
await widget!.api.transport.reply(ev.detail, {});
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): 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<void> => {
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<Props> = ({
const history = useHistory();
const onLeave = useCallback(
async (leaveError?: Error) => {
(leaveError?: Error): void => {
setLeaveError(leaveError);
setLeft(true);
@ -227,15 +226,19 @@ export const GroupCallView: FC<Props> = ({
);
// 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<Props> = ({
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<IWidgetApiRequest>,
): Promise<void> => {
const onHangup = (ev: CustomEvent<IWidgetApiRequest>): 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<Props> = ({
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);

View File

@ -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<ActiveCallProps> = (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<InCallViewProps> = ({
);
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<InCallViewProps> = ({
rtcSession.room.roomId,
);
const toggleScreensharing = useCallback(async () => {
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
const toggleScreensharing = useCallback(() => {
void localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
audio: true,
selfBrowserSurface: "include",
surfaceSwitching: "include",

View File

@ -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,

View File

@ -165,17 +165,21 @@ export const useLoadGroupCall = (
const invitePromise = new Promise<void>((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<void> => {
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());
});

View File

@ -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(

View File

@ -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<Props> = ({ roomId }) => {
sendLogs,
rageshakeRequestId,
roomId,
}).catch((e) => {
logger.error("Failed to send feedback rageshake", e);
});
if (roomId && sendLogs) {

View File

@ -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<Props> = ({ client }) => {
// @ts-ignore
avatar: avatar && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0),
}).catch((e) => {
logger.error("Failed to save profile", e);
});
}
};

View File

@ -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<Props> = ({ description }) => {
submitRageshake({
description,
sendLogs: true,
}).catch((e) => {
logger.error("Failed to send rageshake", e);
});
}, [submitRageshake, description]);

View File

@ -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<void> {
public async connect(): Promise<void> {
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<void> => {
public flush = async (): Promise<void> => {
// 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<string> {
async function fetchLogs(id: string, maxSize: number): Promise<string> {
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<string[]> {
async function fetchLogIds(): Promise<string[]> {
// 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<void> {
async function deleteLogs(id: number): Promise<void> {
return new Promise<void>((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<T[]>} Resolves to an array of whatever you returned from
* resultMapper.
*/
function selectQuery<T>(
async function selectQuery<T>(
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<void>;
var mx_rage_initStoragePromise: Promise<void> | undefined;
}
/**
@ -481,7 +479,7 @@ declare global {
* be set up immediately for the logs.
* @return {Promise} Resolves when set up.
*/
export function init(): Promise<void> {
export async function init(): Promise<void> {
global.mx_rage_logger = new ConsoleLogger();
setLogExtension(global.mx_rage_logger.log);
@ -493,7 +491,7 @@ export function init(): Promise<void> {
* then this no-ops.
* @return {Promise} Resolves when complete.
*/
function tryInitStorage(): Promise<void> {
async function tryInitStorage(): Promise<void> {
if (global.mx_rage_initStoragePromise) {
return global.mx_rage_initStoragePromise;
}

View File

@ -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],
);

View File

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

View File

@ -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<void> => {
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);
});
}
};

View File

@ -270,11 +270,17 @@ export async function createRoom(
// Wait for the room to arrive
await new Promise<void>((resolve, reject) => {
const onRoom = async (room: Room): Promise<void> => {
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);

View File

@ -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> {
): string | undefined {
const deviceInfo = devices.find(
(d) => d.kind === kind && d.label === deviceName,
);

View File

@ -167,17 +167,15 @@ export const widget = ((): WidgetHelpers | null => {
false,
);
const clientPromise = new Promise<MatrixClient>((resolve) => {
(async (): Promise<void> => {
// 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<MatrixClient> => {
// 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");