Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Conform more of the codebase to strictNullChecks #10842

Merged
merged 10 commits into from
May 11, 2023
7 changes: 4 additions & 3 deletions src/components/views/avatars/WidgetAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ limitations under the License.
*/

import React, { ComponentProps } from "react";
import { IWidget } from "matrix-widget-api";
import classNames from "classnames";

import { IApp } from "../../../stores/WidgetStore";
import { IApp, isAppWidget } from "../../../stores/WidgetStore";
import BaseAvatar, { BaseAvatarType } from "./BaseAvatar";
import { mediaFromMxc } from "../../../customisations/Media";

interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls" | "height" | "width"> {
app: IApp;
app: IApp | IWidget;
height?: number;
width?: number;
}
Expand All @@ -46,7 +47,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
name={app.id}
className={classNames("mx_WidgetAvatar", className)}
// MSC2765
url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : null}
url={isAppWidget(app) && app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : null}
urls={iconUrls}
width={width}
height={height}
Expand Down
15 changes: 9 additions & 6 deletions src/components/views/context_menus/WidgetContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ limitations under the License.
*/

import React, { ComponentProps, useContext } from "react";
import { MatrixCapabilities } from "matrix-widget-api";
import { IWidget, MatrixCapabilities } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger";
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";

import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { ChevronFace } from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler";
import { IApp } from "../../../stores/WidgetStore";
import { isAppWidget } from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import RoomContext from "../../../contexts/RoomContext";
Expand All @@ -39,7 +39,7 @@ import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";

interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> {
app: IApp;
app: IWidget;
userWidget?: boolean;
showUnpin?: boolean;
// override delete handler
Expand Down Expand Up @@ -155,7 +155,9 @@ export const WidgetContextMenu: React.FC<IProps> = ({
}

const isAllowedWidget =
(app.eventId !== undefined && (SettingsStore.getValue("allowedWidgets", roomId)[app.eventId] ?? false)) ||
(isAppWidget(app) &&
app.eventId !== undefined &&
(SettingsStore.getValue("allowedWidgets", roomId)[app.eventId] ?? false)) ||
app.creatorUserId === cli.getUserId();

const isLocalWidget = WidgetType.JITSI.matches(app.type);
Expand All @@ -166,9 +168,10 @@ export const WidgetContextMenu: React.FC<IProps> = ({

if (!opts.approved) {
const onRevokeClick = (): void => {
logger.info("Revoking permission for widget to load: " + app.eventId);
const eventId = isAppWidget(app) ? app.eventId : undefined;
logger.info("Revoking permission for widget to load: " + eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
if (app.eventId !== undefined) current[app.eventId] = false;
if (eventId !== undefined) current[eventId] = false;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
if (!level) throw new Error("level must be defined");
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => {
Expand Down
52 changes: 38 additions & 14 deletions src/components/views/elements/AppTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ limitations under the License.
import url from "url";
import React, { ContextType, createRef, CSSProperties, MutableRefObject, ReactNode } from "react";
import classNames from "classnames";
import { MatrixCapabilities } from "matrix-widget-api";
import { IWidget, MatrixCapabilities } from "matrix-widget-api";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
Expand All @@ -40,7 +40,7 @@ import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWid
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar";
import LegacyCallHandler from "../../../LegacyCallHandler";
import { IApp } from "../../../stores/WidgetStore";
import { IApp, isAppWidget } from "../../../stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
Expand All @@ -54,7 +54,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { ModuleRunner } from "../../../modules/ModuleRunner";

interface IProps {
app: IApp;
app: IWidget | IApp;
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
room?: Room;
Expand Down Expand Up @@ -133,7 +133,10 @@ export default class AppTile extends React.Component<IProps, IState> {

// Tiles in miniMode are floating, and therefore not docked
if (!this.props.miniMode) {
ActiveWidgetStore.instance.dockWidget(this.props.app.id, this.props.app.roomId);
ActiveWidgetStore.instance.dockWidget(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);
}

// The key used for PersistedElement
Expand Down Expand Up @@ -169,14 +172,17 @@ export default class AppTile extends React.Component<IProps, IState> {
if (opts.approved) return true;

const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false);
const allowed =
isAppWidget(props.app) &&
props.app.eventId !== undefined &&
(currentlyAllowedWidgets[props.app.eventId] ?? false);
return allowed || props.userId === props.creatorUserId;
};

private onUserLeftRoom(): void {
const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(
this.props.app.id,
this.props.app.roomId,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);
if (isActiveWidget) {
// We just left the room that the active widget was from.
Expand All @@ -188,7 +194,10 @@ export default class AppTile extends React.Component<IProps, IState> {
this.reload();
} else {
// Otherwise just cancel its persistence.
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id, this.props.app.roomId);
ActiveWidgetStore.instance.destroyPersistentWidget(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);
}
}
}
Expand Down Expand Up @@ -243,7 +252,10 @@ export default class AppTile extends React.Component<IProps, IState> {

if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id, this.props.app.roomId);
ActiveWidgetStore.instance.destroyPersistentWidget(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);
PersistedElement.destroyElement(this.persistKey);
this.sgWidget?.stopMessaging();
}
Expand Down Expand Up @@ -288,13 +300,21 @@ export default class AppTile extends React.Component<IProps, IState> {
this.unmounted = true;

if (!this.props.miniMode) {
ActiveWidgetStore.instance.undockWidget(this.props.app.id, this.props.app.roomId);
ActiveWidgetStore.instance.undockWidget(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);
}

// Only tear down the widget if no other component is keeping it alive,
// because we support moving widgets between containers, in which case
// another component will keep it loaded throughout the transition
if (!ActiveWidgetStore.instance.isLive(this.props.app.id, this.props.app.roomId)) {
if (
!ActiveWidgetStore.instance.isLive(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
)
) {
this.endWidgetActions();
}

Expand Down Expand Up @@ -395,7 +415,10 @@ export default class AppTile extends React.Component<IProps, IState> {

// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this.persistKey);
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id, this.props.app.roomId);
ActiveWidgetStore.instance.destroyPersistentWidget(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);

this.sgWidget?.stopMessaging({ forceDestroy: true });
}
Expand Down Expand Up @@ -441,9 +464,10 @@ export default class AppTile extends React.Component<IProps, IState> {

private grantWidgetPermission = (): void => {
const roomId = this.props.room?.roomId;
logger.info("Granting permission for widget to load: " + this.props.app.eventId);
const eventId = isAppWidget(this.props.app) ? this.props.app.eventId : undefined;
logger.info("Granting permission for widget to load: " + eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
if (this.props.app.eventId !== undefined) current[this.props.app.eventId] = true;
if (eventId !== undefined) current[eventId] = true;
const level = SettingsStore.firstSupportedLevel("allowedWidgets")!;
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current)
.then(() => {
Expand Down Expand Up @@ -550,7 +574,7 @@ export default class AppTile extends React.Component<IProps, IState> {
};

public render(): React.ReactNode {
let appTileBody;
let appTileBody: JSX.Element;

// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to programmatically remove the sandbox attribute, but
Expand Down
14 changes: 7 additions & 7 deletions src/components/views/rooms/AppsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import React from "react";
import classNames from "classnames";
import { Resizable } from "re-resizable";
import { Room } from "matrix-js-sdk/src/models/room";
import { IWidget } from "matrix-widget-api";

import AppTile from "../elements/AppTile";
import dis from "../../../dispatcher/dispatcher";
Expand All @@ -32,7 +33,6 @@ import PercentageDistributor from "../../../resizer/distributors/percentage";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
import UIStore from "../../../stores/UIStore";
import { IApp } from "../../../stores/WidgetStore";
import { ActionPayload } from "../../../dispatcher/payloads";
import Spinner from "../elements/Spinner";

Expand All @@ -46,9 +46,9 @@ interface IProps {

interface IState {
apps: {
[Container.Top]: IApp[];
[Container.Center]: IApp[];
[Container.Right]?: IApp[];
[Container.Top]: IWidget[];
[Container.Center]: IWidget[];
[Container.Right]?: IWidget[];
};
resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
Expand Down Expand Up @@ -147,7 +147,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
this.loadResizerPreferences();
};

private getAppsHash = (apps: IApp[]): string => apps.map((app) => app.id).join("~");
private getAppsHash = (apps: IWidget[]): string => apps.map((app) => app.id).join("~");

public componentDidUpdate(prevProps: IProps, prevState: IState): void {
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
Expand Down Expand Up @@ -210,8 +210,8 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
[Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
[Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
});
private topApps = (): IApp[] => this.state.apps[Container.Top];
private centerApps = (): IApp[] => this.state.apps[Container.Center];
private topApps = (): IWidget[] => this.state.apps[Container.Top];
private centerApps = (): IWidget[] => this.state.apps[Container.Center];

private updateApps = (): void => {
if (this.unmounted) return;
Expand Down
7 changes: 2 additions & 5 deletions src/components/views/rooms/Stickerpicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
import React from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { IWidget } from "matrix-widget-api";

import { _t, _td } from "../../../languageHandler";
import AppTile from "../elements/AppTile";
Expand All @@ -32,7 +33,6 @@ import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingSto
import { ActionPayload } from "../../../dispatcher/payloads";
import ScalarAuthClient from "../../../ScalarAuthClient";
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
import { IApp } from "../../../stores/WidgetStore";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";

Expand Down Expand Up @@ -264,15 +264,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");

// FIXME: could this use the same code as other apps?
const stickerApp: IApp = {
const stickerApp: IWidget = {
id: stickerpickerWidget.id,
url: stickerpickerWidget.content.url,
name: stickerpickerWidget.content.name,
type: stickerpickerWidget.content.type,
data: stickerpickerWidget.content.data,
roomId: stickerpickerWidget.content.roomId,
eventId: stickerpickerWidget.content.eventId,
avatar_url: stickerpickerWidget.content.avatar_url,
creatorUserId: stickerpickerWidget.content.creatorUserId || stickerpickerWidget.sender,
};

Expand Down
18 changes: 9 additions & 9 deletions src/stores/ActiveWidgetStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ export default class ActiveWidgetStore extends EventEmitter {
}
};

public destroyPersistentWidget(widgetId: string, roomId: string): void {
public destroyPersistentWidget(widgetId: string, roomId: string | null): void {
if (!this.getWidgetPersistence(widgetId, roomId)) return;
WidgetMessagingStore.instance.stopMessagingByUid(WidgetUtils.calcWidgetUid(widgetId, roomId));
WidgetMessagingStore.instance.stopMessagingByUid(WidgetUtils.calcWidgetUid(widgetId, roomId ?? undefined));
this.setWidgetPersistence(widgetId, roomId, false);
}

Expand Down Expand Up @@ -102,29 +102,29 @@ export default class ActiveWidgetStore extends EventEmitter {

// Registers the given widget as being docked somewhere in the UI (not a PiP),
// to allow its lifecycle to be tracked.
public dockWidget(widgetId: string, roomId: string): void {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId);
public dockWidget(widgetId: string, roomId: string | null): void {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId ?? undefined);
const refs = this.dockedWidgetsByUid.get(uid) ?? 0;
this.dockedWidgetsByUid.set(uid, refs + 1);
if (refs === 0) this.emit(ActiveWidgetStoreEvent.Dock);
}

public undockWidget(widgetId: string, roomId: string): void {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId);
public undockWidget(widgetId: string, roomId: string | null): void {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId ?? undefined);
const refs = this.dockedWidgetsByUid.get(uid);
if (refs) this.dockedWidgetsByUid.set(uid, refs - 1);
if (refs === 1) this.emit(ActiveWidgetStoreEvent.Undock);
}

// Determines whether the given widget is docked anywhere in the UI (not a PiP)
public isDocked(widgetId: string, roomId: string): boolean {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId);
public isDocked(widgetId: string, roomId: string | null): boolean {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId ?? undefined);
const refs = this.dockedWidgetsByUid.get(uid) ?? 0;
return refs > 0;
}

// Determines whether the given widget is being kept alive in the UI, including PiPs
public isLive(widgetId: string, roomId: string): boolean {
public isLive(widgetId: string, roomId: string | null): boolean {
return this.isDocked(widgetId, roomId) || this.getWidgetPersistence(widgetId, roomId);
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/stores/WidgetStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export interface IApp extends IWidget {
avatar_url?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
}

export function isAppWidget(widget: IWidget | IApp): widget is IApp {
return typeof widget["roomId"] === "string";
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a piece of work I started a Typeguards.ts file in the src folder. I'm still not entirely sure if it would be better to put typeguards in one file, or always keep them closer to what they're guarding for, like here.

What do you think? Better to keep here or consolidate in a single typeguards file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My personal preference is closer to the object, in you case I think it makes sense for generic ones which don't apply to a specific type, like null

interface IRoomWidgets {
widgets: IApp[];
}
Expand Down
6 changes: 3 additions & 3 deletions src/stores/widgets/StopGapWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import { ElementWidgetActions, IHangupCallApiRequest, IViewRoomApiRequest } from "./ElementWidgetActions";
import { ModalWidgetStore } from "../ModalWidgetStore";
import { IApp } from "../WidgetStore";
import { IApp, isAppWidget } from "../WidgetStore";
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { getCustomTheme } from "../../theme";
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
Expand All @@ -72,7 +72,7 @@ import { SdkContextClass } from "../../contexts/SDKContext";

interface IAppTileProps {
// Note: these are only the props we care about
app: IApp;
app: IApp | IWidget;
room?: Room; // without a room it is a user widget
userId: string;
creatorUserId: string;
Expand Down Expand Up @@ -179,7 +179,7 @@ export class StopGapWidget extends EventEmitter {
this.mockWidget = new ElementWidget(app);
this.roomId = appTileProps.room?.roomId;
this.kind = appTileProps.userWidget ? WidgetKind.Account : WidgetKind.Room; // probably
this.virtual = app.eventId === undefined;
this.virtual = isAppWidget(app) && app.eventId === undefined;
}

private get eventListenerRoomId(): Optional<string> {
Expand Down
Loading