diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 708db7a32d..99cd063de0 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -458,7 +458,7 @@ export default class LegacyCallHandler extends EventEmitter { public getAllActiveCallsForPip(roomId: string): MatrixCall[] { const room = MatrixClientPeg.get().getRoom(roomId); - if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) { + if (room && WidgetLayoutStore.instance.hasMaximisedWidget(room)) { // This checks if there is space for the call view in the aux panel // If there is no space any call should be displayed in PiP return this.getAllActiveCalls(); diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 2cc0e094c3..6d6d4a9c75 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -67,7 +67,7 @@ export const WidgetContextMenu: React.FC = ({ if (getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type)) { const onStreamAudioClick = async (): Promise => { try { - await startJitsiAudioLivestream(widgetMessaging, roomId); + await startJitsiAudioLivestream(widgetMessaging!, roomId); } catch (err) { logger.error("Failed to start livestream", err); // XXX: won't i18n well, but looks like widget api only support 'message'? @@ -84,7 +84,7 @@ export const WidgetContextMenu: React.FC = ({ ); } - const pinnedWidgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top); + const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top) : []; const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id); let editButton; @@ -196,6 +196,7 @@ export const WidgetContextMenu: React.FC = ({ let moveRightButton; if (showUnpin && widgetIndex < pinnedWidgets.length - 1) { const onClick = (): void => { + if (!room) throw new Error("room must be defined"); WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1); onFinished(); }; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index e6bf53426b..01629c5d5c 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -99,7 +99,7 @@ interface IState { hasPermissionToLoad: boolean; // Wait for user profile load to display correct name isUserProfileReady: boolean; - error: Error; + error: Error | null; menuDisplayed: boolean; requiresClient: boolean; } @@ -124,7 +124,7 @@ export default class AppTile extends React.Component { private iframe: HTMLIFrameElement; // ref to the iframe (callback style) private allowedWidgetsWatchRef: string; private persistKey: string; - private sgWidget: StopGapWidget; + private sgWidget: StopGapWidget | null; private dispatcherRef: string; private unmounted: boolean; @@ -202,7 +202,7 @@ export default class AppTile extends React.Component { private determineInitialRequiresClientState(): boolean { try { const mockWidget = new ElementWidget(this.props.app); - const widgetApi = WidgetMessagingStore.instance.getMessaging(mockWidget, this.props.room.roomId); + const widgetApi = WidgetMessagingStore.instance.getMessaging(mockWidget, this.props.room?.roomId); if (widgetApi) { // Load value from existing API to prevent resetting the requiresClient value on layout changes. return widgetApi.hasCapability(ElementWidgetCapabilities.RequiresClient); @@ -310,9 +310,9 @@ export default class AppTile extends React.Component { } private setupSgListeners(): void { - this.sgWidget.on("preparing", this.onWidgetPreparing); + this.sgWidget?.on("preparing", this.onWidgetPreparing); // emits when the capabilities have been set up or changed - this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified); + this.sgWidget?.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified); } private stopSgListeners(): void { @@ -336,7 +336,7 @@ export default class AppTile extends React.Component { } private startWidget(): void { - this.sgWidget.prepare().then(() => { + this.sgWidget?.prepare().then(() => { if (this.unmounted) return; this.setState({ initialising: false }); }); @@ -406,7 +406,7 @@ export default class AppTile extends React.Component { private onWidgetCapabilitiesNotified = (): void => { this.setState({ - requiresClient: this.sgWidget.widgetApi.hasCapability(ElementWidgetCapabilities.RequiresClient), + requiresClient: !!this.sgWidget?.widgetApi?.hasCapability(ElementWidgetCapabilities.RequiresClient), }); }; @@ -415,7 +415,7 @@ export default class AppTile extends React.Component { case "m.sticker": if ( payload.widgetId === this.props.app.id && - this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending) + this.sgWidget?.widgetApi?.hasCapability(MatrixCapabilities.StickerSending) ) { dis.dispatch({ action: "post_sticker_message", @@ -444,8 +444,8 @@ export default class AppTile extends React.Component { logger.info("Granting permission for widget to load: " + this.props.app.eventId); const current = SettingsStore.getValue("allowedWidgets", roomId); if (this.props.app.eventId !== undefined) current[this.props.app.eventId] = true; - const level = SettingsStore.firstSupportedLevel("allowedWidgets"); - SettingsStore.setValue("allowedWidgets", roomId, level, current) + const level = SettingsStore.firstSupportedLevel("allowedWidgets")!; + SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current) .then(() => { this.setState({ hasPermissionToLoad: true }); @@ -501,7 +501,7 @@ export default class AppTile extends React.Component { this.resetWidget(this.props); this.startMessaging(); - if (this.iframe) { + if (this.iframe && this.sgWidget) { // Reload iframe this.iframe.src = this.sgWidget.embedUrl; } @@ -519,7 +519,7 @@ export default class AppTile extends React.Component { // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement("a"), { target: "_blank", - href: this.sgWidget.popoutUrl, + href: this.sgWidget?.popoutUrl, rel: "noreferrer noopener", }).click(); }; @@ -676,7 +676,7 @@ export default class AppTile extends React.Component { if (this.state.menuDisplayed) { contextMenu = ( this.emit("preparing")); this.messaging.on("ready", () => { - WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.roomId, this.messaging); + WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.roomId, this.messaging!); this.emit("ready"); }); this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); @@ -347,7 +347,7 @@ export class StopGapWidget extends EventEmitter { if (this.messaging?.hasCapability(MatrixCapabilities.AlwaysOnScreen)) { ActiveWidgetStore.instance.setWidgetPersistence( this.mockWidget.id, - this.roomId, + this.roomId ?? null, ev.detail.data.value, ); ev.preventDefault(); @@ -393,14 +393,12 @@ export class StopGapWidget extends EventEmitter { const integType = data?.integType as string; const integId = data?.integId; + const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); + const room = roomId ? this.client.getRoom(roomId) : undefined; + if (!room) return; + // noinspection JSIgnoredPromiseFromCall - IntegrationManagers.sharedInstance() - .getPrimaryManager() - .open( - this.client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()), - `type_${integType}`, - integId, - ); + IntegrationManagers.sharedInstance()?.getPrimaryManager()?.open(room, `type_${integType}`, integId); }, ); } @@ -434,7 +432,7 @@ export class StopGapWidget extends EventEmitter { if (managers.hasManager()) { // TODO: Pick the right manager for the widget const defaultManager = managers.getPrimaryManager(); - if (WidgetUtils.isScalarUrl(defaultManager.apiUrl)) { + if (defaultManager && WidgetUtils.isScalarUrl(defaultManager.apiUrl)) { const scalar = defaultManager.getScalarClient(); this.scalarToken = await scalar.getScalarToken(); } @@ -452,7 +450,10 @@ export class StopGapWidget extends EventEmitter { * @param opts */ public stopMessaging(opts = { forceDestroy: false }): void { - if (!opts?.forceDestroy && ActiveWidgetStore.instance.getWidgetPersistence(this.mockWidget.id, this.roomId)) { + if ( + !opts?.forceDestroy && + ActiveWidgetStore.instance.getWidgetPersistence(this.mockWidget.id, this.roomId ?? null) + ) { logger.log("Skipping destroy - persistent widget"); return; } @@ -500,9 +501,11 @@ export class StopGapWidget extends EventEmitter { let isBeforeMark = true; + const room = this.client.getRoom(ev.getRoomId()!); + if (!room) return; // Timelines are most recent last, so reverse the order and limit ourselves to 100 events // to avoid overusing the CPU. - const timeline = this.client.getRoom(ev.getRoomId()!).getLiveTimeline(); + const timeline = room.getLiveTimeline(); const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); for (const timelineEvent of events) { @@ -533,7 +536,7 @@ export class StopGapWidget extends EventEmitter { } const raw = ev.getEffectiveEvent(); - this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId).catch((e) => { + this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => { logger.error("Error sending event to widget: ", e); }); } diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 8e892a05e6..d38dc55288 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -113,7 +113,7 @@ export class StopGapWidgetDriver extends WidgetDriver { this.allowedCapabilities.add("visibility"); } else if ( virtual && - new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url).origin === this.forWidget.origin + new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url!).origin === this.forWidget.origin ) { // This is a trusted Element Call widget that we control this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen); @@ -202,8 +202,8 @@ export class StopGapWidgetDriver extends WidgetDriver { widget: this.forWidget, widgetKind: this.forWidgetKind, }).finished; - (result.approved || []).forEach((cap) => allowedSoFar.add(cap)); - rememberApproved = result.remember; + result?.approved?.forEach((cap) => allowedSoFar.add(cap)); + rememberApproved = !!result?.remember; } catch (e) { logger.error("Non-fatal error getting capabilities: ", e); } @@ -267,7 +267,7 @@ export class StopGapWidgetDriver extends WidgetDriver { const client = MatrixClientPeg.get(); if (encrypted) { - const deviceInfoMap = await client.crypto.deviceList.downloadKeys(Object.keys(contentMap), false); + const deviceInfoMap = await client.crypto!.deviceList.downloadKeys(Object.keys(contentMap), false); await Promise.all( Object.entries(contentMap).flatMap(([userId, userContentMap]) => @@ -313,7 +313,7 @@ export class StopGapWidgetDriver extends WidgetDriver { ? roomIds.includes(Symbols.AnyRoom) ? client.getVisibleRooms() : roomIds.map((r) => client.getRoom(r)) - : [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId())]; + : [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()!)]; return targetRooms.filter((r) => !!r) as Room[]; } @@ -356,7 +356,7 @@ export class StopGapWidgetDriver extends WidgetDriver { const allResults: IRoomEvent[] = []; for (const room of rooms) { const results: MatrixEvent[] = []; - const state: Map = room.currentState.events.get(eventType); + const state = room.currentState.events.get(eventType); if (state) { if (stateKey === "" || !!stateKey) { const forKey = state.get(stateKey); diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index 74a6d6c3ee..311c7e7c2a 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -17,7 +17,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { Optional } from "matrix-events-sdk"; import { compare } from "matrix-js-sdk/src/utils"; import SettingsStore from "../../settings/SettingsStore"; @@ -63,7 +62,7 @@ export interface IStoredLayout { // this only applies to the top container currently, and that container // will take the highest value among widgets in the container. Clamped // to 0-100 and may have minimums imposed on it. - height?: number; + height?: number | null; // TODO: [Deferred] Maximizing (fullscreen) widgets by default. } @@ -147,6 +146,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { } private updateAllRooms = (): void => { + if (!this.matrixClient) return; this.byRoom = {}; for (const room of this.matrixClient.getVisibleRooms()) { this.recalculateRoom(room); @@ -267,7 +267,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { const userWidgetLayout = userLayout?.widgets?.[widget.id]; if (Number.isFinite(userWidgetLayout?.width) || Number.isFinite(widgetLayout?.width)) { - const val = userWidgetLayout?.width || widgetLayout?.width; + const val = (userWidgetLayout?.width || widgetLayout?.width)!; const normalized = clamp(val, MIN_WIDGET_WIDTH_PCT, 100); widths.push(normalized); doAutobalance = false; // a manual width was specified @@ -278,7 +278,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { if (widgetLayout?.height || userWidgetLayout?.height) { const defRoomHeight = defaultNumber(widgetLayout?.height, MIN_WIDGET_HEIGHT_PCT); const h = defaultNumber(userWidgetLayout?.height, defRoomHeight); - maxHeight = Math.max(maxHeight, clamp(h, MIN_WIDGET_HEIGHT_PCT, 100)); + maxHeight = Math.max(maxHeight ?? 0, clamp(h, MIN_WIDGET_HEIGHT_PCT, 100)); } } if (doAutobalance) { @@ -346,11 +346,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore { } } - public getContainerWidgets(room: Optional, container: Container): IApp[] { - return this.byRoom[room?.roomId]?.[container]?.ordered || []; + public getContainerWidgets(room: Room, container: Container): IApp[] { + return this.byRoom[room.roomId]?.[container]?.ordered || []; } - public isInContainer(room: Optional, widget: IApp, container: Container): boolean { + public isInContainer(room: Room, widget: IApp, container: Container): boolean { return this.getContainerWidgets(room, container).some((w) => w.id === widget.id); } @@ -406,7 +406,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { return this.byRoom[room.roomId]?.[container]?.height ?? null; // let the default get returned if needed } - public setContainerHeight(room: Room, container: Container, height?: number): void { + public setContainerHeight(room: Room, container: Container, height?: number | null): void { const widgets = this.getContainerWidgets(room, container); const widths = this.byRoom[room.roomId]?.[container]?.distributions; const localLayout: Record = {}; @@ -438,7 +438,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { container: container, width: widths?.[i], index: i, - height: height, + height, }; }); this.updateUserLayout(room, localLayout); @@ -477,7 +477,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { this.updateUserLayout(room, newLayout); } - public hasMaximisedWidget(room?: Room): boolean { + public hasMaximisedWidget(room: Room): boolean { return this.getContainerWidgets(room, Container.Center).length > 0; } @@ -517,7 +517,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { const ret: [IApp, Container][] = []; for (const container in containers) { - const widgets = containers[container as Container].ordered; + const widgets = containers[container as Container]!.ordered; for (const widget of widgets) { ret.push([widget, container as Container]); } diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts index 45a10f393c..b269a6f759 100644 --- a/src/stores/widgets/WidgetMessagingStore.ts +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -58,7 +58,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<{}> { this.widgetMap.clear(); } - public storeMessaging(widget: Widget, roomId: string, widgetApi: ClientWidgetApi): void { + public storeMessaging(widget: Widget, roomId: string | undefined, widgetApi: ClientWidgetApi): void { this.stopMessaging(widget, roomId); const uid = WidgetUtils.calcWidgetUid(widget.id, roomId); this.widgetMap.set(uid, widgetApi); @@ -66,11 +66,11 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<{}> { this.emit(WidgetMessagingStoreEvent.StoreMessaging, uid, widgetApi); } - public stopMessaging(widget: Widget, roomId: string): void { + public stopMessaging(widget: Widget, roomId: string | undefined): void { this.stopMessagingByUid(WidgetUtils.calcWidgetUid(widget.id, roomId)); } - public getMessaging(widget: Widget, roomId: string): ClientWidgetApi { + public getMessaging(widget: Widget, roomId: string | undefined): ClientWidgetApi | undefined { return this.widgetMap.get(WidgetUtils.calcWidgetUid(widget.id, roomId)); } @@ -88,7 +88,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<{}> { * @param {string} widgetUid The widget UID. * @returns {ClientWidgetApi} The widget API, or a falsy value if not found. */ - public getMessagingForUid(widgetUid: string): ClientWidgetApi { + public getMessagingForUid(widgetUid: string): ClientWidgetApi | undefined { return this.widgetMap.get(widgetUid); } } diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 3d4dca2fe2..3fce951b35 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -57,7 +57,7 @@ export default class WidgetUtils { * @return Boolean -- true if the user can modify widgets in this room * @throws Error -- specifies the error reason */ - public static canUserModifyWidgets(roomId: string): boolean { + public static canUserModifyWidgets(roomId?: string): boolean { if (!roomId) { logger.warn("No room ID specified"); return false; diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index 9cf306a4b0..35cb5d200c 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -541,7 +541,7 @@ describe("RoomHeader", () => { await withCall(async (call) => { await call.connect(); - const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget)); + const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget))!; renderHeader({ viewingCall: true, activeCall: call }); // Should start with Freedom selected