Conform more of the codebase to `strictNullChecks` (#10842)

pull/28788/head^2
Michael Telatynski 2023-05-11 09:56:56 +01:00 committed by GitHub
parent 5eea2c8b02
commit 82e32035fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 126 additions and 93 deletions

View File

@ -126,6 +126,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
setHighlights(highlights); setHighlights(highlights);
setResults({ ...results }); // copy to force a refresh setResults({ ...results }); // copy to force a refresh
return false;
}, },
(error) => { (error) => {
if (aborted.current) { if (aborted.current) {

View File

@ -15,14 +15,15 @@ limitations under the License.
*/ */
import React, { ComponentProps } from "react"; import React, { ComponentProps } from "react";
import { IWidget } from "matrix-widget-api";
import classNames from "classnames"; import classNames from "classnames";
import { IApp } from "../../../stores/WidgetStore"; import { IApp, isAppWidget } from "../../../stores/WidgetStore";
import BaseAvatar, { BaseAvatarType } from "./BaseAvatar"; import BaseAvatar, { BaseAvatarType } from "./BaseAvatar";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls" | "height" | "width"> { interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls" | "height" | "width"> {
app: IApp; app: IApp | IWidget;
height?: number; height?: number;
width?: number; width?: number;
} }
@ -46,7 +47,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
name={app.id} name={app.id}
className={classNames("mx_WidgetAvatar", className)} className={classNames("mx_WidgetAvatar", className)}
// MSC2765 // 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} urls={iconUrls}
width={width} width={width}
height={height} height={height}

View File

@ -15,14 +15,14 @@ limitations under the License.
*/ */
import React, { ComponentProps, useContext } from "react"; 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 { logger } from "matrix-js-sdk/src/logger";
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { ChevronFace } from "../../structures/ContextMenu"; import { ChevronFace } from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { IApp } from "../../../stores/WidgetStore"; import { isAppWidget } from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils"; import WidgetUtils from "../../../utils/WidgetUtils";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
@ -39,7 +39,7 @@ import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> { interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> {
app: IApp; app: IWidget;
userWidget?: boolean; userWidget?: boolean;
showUnpin?: boolean; showUnpin?: boolean;
// override delete handler // override delete handler
@ -155,7 +155,9 @@ export const WidgetContextMenu: React.FC<IProps> = ({
} }
const isAllowedWidget = 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(); app.creatorUserId === cli.getUserId();
const isLocalWidget = WidgetType.JITSI.matches(app.type); const isLocalWidget = WidgetType.JITSI.matches(app.type);
@ -166,9 +168,10 @@ export const WidgetContextMenu: React.FC<IProps> = ({
if (!opts.approved) { if (!opts.approved) {
const onRevokeClick = (): void => { 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); 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"); const level = SettingsStore.firstSupportedLevel("allowedWidgets");
if (!level) throw new Error("level must be defined"); if (!level) throw new Error("level must be defined");
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => { SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => {

View File

@ -20,7 +20,7 @@ limitations under the License.
import url from "url"; import url from "url";
import React, { ContextType, createRef, CSSProperties, MutableRefObject, ReactNode } from "react"; import React, { ContextType, createRef, CSSProperties, MutableRefObject, ReactNode } from "react";
import classNames from "classnames"; 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 { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
@ -40,7 +40,7 @@ import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWid
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar"; import WidgetAvatar from "../avatars/WidgetAvatar";
import LegacyCallHandler from "../../../LegacyCallHandler"; import LegacyCallHandler from "../../../LegacyCallHandler";
import { IApp } from "../../../stores/WidgetStore"; import { IApp, isAppWidget } from "../../../stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
@ -54,7 +54,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ModuleRunner } from "../../../modules/ModuleRunner";
interface IProps { interface IProps {
app: IApp; app: IWidget | IApp;
// If room is not specified then it is an account level widget // If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user // which bypasses permission prompts as it was added explicitly by that user
room?: Room; room?: Room;
@ -133,7 +133,10 @@ export default class AppTile extends React.Component<IProps, IState> {
// Tiles in miniMode are floating, and therefore not docked // Tiles in miniMode are floating, and therefore not docked
if (!this.props.miniMode) { 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 // The key used for PersistedElement
@ -169,14 +172,17 @@ export default class AppTile extends React.Component<IProps, IState> {
if (opts.approved) return true; if (opts.approved) return true;
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); 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; return allowed || props.userId === props.creatorUserId;
}; };
private onUserLeftRoom(): void { private onUserLeftRoom(): void {
const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence( const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(
this.props.app.id, this.props.app.id,
this.props.app.roomId, isAppWidget(this.props.app) ? this.props.app.roomId : null,
); );
if (isActiveWidget) { if (isActiveWidget) {
// We just left the room that the active widget was from. // We just left the room that the active widget was from.
@ -188,7 +194,10 @@ export default class AppTile extends React.Component<IProps, IState> {
this.reload(); this.reload();
} else { } else {
// Otherwise just cancel its persistence. // 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,
);
} }
} }
} }
@ -243,7 +252,10 @@ export default class AppTile extends React.Component<IProps, IState> {
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
// Force the widget to be non-persistent (able to be deleted/forgotten) // 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); PersistedElement.destroyElement(this.persistKey);
this.sgWidget?.stopMessaging(); this.sgWidget?.stopMessaging();
} }
@ -288,13 +300,21 @@ export default class AppTile extends React.Component<IProps, IState> {
this.unmounted = true; this.unmounted = true;
if (!this.props.miniMode) { 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, // Only tear down the widget if no other component is keeping it alive,
// because we support moving widgets between containers, in which case // because we support moving widgets between containers, in which case
// another component will keep it loaded throughout the transition // 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(); this.endWidgetActions();
} }
@ -395,7 +415,10 @@ export default class AppTile extends React.Component<IProps, IState> {
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this.persistKey); 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 }); this.sgWidget?.stopMessaging({ forceDestroy: true });
} }
@ -441,9 +464,10 @@ export default class AppTile extends React.Component<IProps, IState> {
private grantWidgetPermission = (): void => { private grantWidgetPermission = (): void => {
const roomId = this.props.room?.roomId; 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); 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")!; const level = SettingsStore.firstSupportedLevel("allowedWidgets")!;
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current) SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current)
.then(() => { .then(() => {
@ -550,7 +574,7 @@ export default class AppTile extends React.Component<IProps, IState> {
}; };
public render(): React.ReactNode { 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 // 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 // because that would allow the iframe to programmatically remove the sandbox attribute, but

View File

@ -47,7 +47,7 @@ interface IProps {
// the latest event in this chain of replies // the latest event in this chain of replies
parentEv: MatrixEvent; parentEv: MatrixEvent;
// called when the ReplyChain contents has changed, including EventTiles thereof // called when the ReplyChain contents has changed, including EventTiles thereof
onHeightChanged: () => void; onHeightChanged?: () => void;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
// Specifies which layout to use. // Specifies which layout to use.
layout?: Layout; layout?: Layout;
@ -104,7 +104,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
} }
public componentDidUpdate(): void { public componentDidUpdate(): void {
this.props.onHeightChanged(); this.props.onHeightChanged?.();
this.trySetExpandableQuotes(); this.trySetExpandableQuotes();
} }

View File

@ -32,7 +32,7 @@ export interface IBodyProps {
highlightLink?: string; highlightLink?: string;
/* callback called when dynamic content in events are loaded */ /* callback called when dynamic content in events are loaded */
onHeightChanged: () => void; onHeightChanged?: () => void;
showUrlPreview?: boolean; showUrlPreview?: boolean;
forExport?: boolean; forExport?: boolean;
@ -40,7 +40,7 @@ export interface IBodyProps {
replacingEventId?: string; replacingEventId?: string;
editState?: EditorStateTransfer; editState?: EditorStateTransfer;
onMessageAllowed: () => void; // TODO: Docs onMessageAllowed: () => void; // TODO: Docs
permalinkCreator: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
mediaEventHelper: MediaEventHelper; mediaEventHelper: MediaEventHelper;
/* /*

View File

@ -174,7 +174,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
private onImageLoad = (): void => { private onImageLoad = (): void => {
this.clearBlurhashTimeout(); this.clearBlurhashTimeout();
this.props.onHeightChanged(); this.props.onHeightChanged?.();
let loadedImageDimensions: IState["loadedImageDimensions"]; let loadedImageDimensions: IState["loadedImageDimensions"];

View File

@ -154,7 +154,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
decryptedThumbnailUrl: thumbnailUrl, decryptedThumbnailUrl: thumbnailUrl,
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
}); });
this.props.onHeightChanged(); this.props.onHeightChanged?.();
} else { } else {
logger.log("NOT preloading video"); logger.log("NOT preloading video");
const content = this.props.mxEvent.getContent<IMediaEventContent>(); const content = this.props.mxEvent.getContent<IMediaEventContent>();
@ -216,7 +216,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
this.videoRef.current.play(); this.videoRef.current.play();
}, },
); );
this.props.onHeightChanged(); this.props.onHeightChanged?.();
}; };
protected get showFileBody(): boolean { protected get showFileBody(): boolean {

View File

@ -173,7 +173,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
// By expanding/collapsing we changed // By expanding/collapsing we changed
// the height, therefore we call this // the height, therefore we call this
this.props.onHeightChanged(); this.props.onHeightChanged?.();
}; };
div.appendChild(button); div.appendChild(button);

View File

@ -19,6 +19,7 @@ import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Resizable } from "re-resizable"; import { Resizable } from "re-resizable";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { IWidget } from "matrix-widget-api";
import AppTile from "../elements/AppTile"; import AppTile from "../elements/AppTile";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
@ -32,7 +33,6 @@ import PercentageDistributor from "../../../resizer/distributors/percentage";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers"; import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
import { IApp } from "../../../stores/WidgetStore";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
@ -46,9 +46,9 @@ interface IProps {
interface IState { interface IState {
apps: { apps: {
[Container.Top]: IApp[]; [Container.Top]: IWidget[];
[Container.Center]: IApp[]; [Container.Center]: IWidget[];
[Container.Right]?: IApp[]; [Container.Right]?: IWidget[];
}; };
resizingVertical: boolean; // true when changing the height of the apps drawer resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
@ -147,7 +147,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
this.loadResizerPreferences(); 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 { public componentDidUpdate(prevProps: IProps, prevState: IState): void {
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) { if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
@ -210,8 +210,8 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
[Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top), [Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
[Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center), [Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
}); });
private topApps = (): IApp[] => this.state.apps[Container.Top]; private topApps = (): IWidget[] => this.state.apps[Container.Top];
private centerApps = (): IApp[] => this.state.apps[Container.Center]; private centerApps = (): IWidget[] => this.state.apps[Container.Center];
private updateApps = (): void => { private updateApps = (): void => {
if (this.unmounted) return; if (this.unmounted) return;

View File

@ -513,6 +513,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
const { permalinkCreator, mxEvent } = this.props; const { permalinkCreator, mxEvent } = this.props;
if (!permalinkCreator) return;
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId()!); const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId()!);
await copyPlaintext(matrixToUrl); await copyPlaintext(matrixToUrl);
}; };
@ -1439,7 +1440,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject<UnwrappedEventTile>) => { const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject<UnwrappedEventTile>) => {
return ( return (
<> <>
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}> <TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
<UnwrappedEventTile ref={ref} {...props} /> <UnwrappedEventTile ref={ref} {...props} />
</TileErrorBoundary> </TileErrorBoundary>
</> </>

View File

@ -32,7 +32,7 @@ interface IProps {
links: string[]; // the URLs to be previewed links: string[]; // the URLs to be previewed
mxEvent: MatrixEvent; // the Event associated with the preview mxEvent: MatrixEvent; // the Event associated with the preview
onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked
onHeightChanged(): void; // called when the preview's contents has loaded onHeightChanged?(): void; // called when the preview's contents has loaded
} }
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => { const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
@ -49,7 +49,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
); );
useEffect(() => { useEffect(() => {
onHeightChanged(); onHeightChanged?.();
}, [onHeightChanged, expanded, previews]); }, [onHeightChanged, expanded, previews]);
const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS); const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS);

View File

@ -17,6 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { IWidget } from "matrix-widget-api";
import { _t, _td } from "../../../languageHandler"; import { _t, _td } from "../../../languageHandler";
import AppTile from "../elements/AppTile"; import AppTile from "../elements/AppTile";
@ -32,7 +33,6 @@ import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingSto
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import ScalarAuthClient from "../../../ScalarAuthClient"; import ScalarAuthClient from "../../../ScalarAuthClient";
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
import { IApp } from "../../../stores/WidgetStore";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
@ -264,15 +264,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack"); stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
// FIXME: could this use the same code as other apps? // FIXME: could this use the same code as other apps?
const stickerApp: IApp = { const stickerApp: IWidget = {
id: stickerpickerWidget.id, id: stickerpickerWidget.id,
url: stickerpickerWidget.content.url, url: stickerpickerWidget.content.url,
name: stickerpickerWidget.content.name, name: stickerpickerWidget.content.name,
type: stickerpickerWidget.content.type, type: stickerpickerWidget.content.type,
data: stickerpickerWidget.content.data, data: stickerpickerWidget.content.data,
roomId: stickerpickerWidget.content.roomId,
eventId: stickerpickerWidget.content.eventId,
avatar_url: stickerpickerWidget.content.avatar_url,
creatorUserId: stickerpickerWidget.content.creatorUserId || stickerpickerWidget.sender, creatorUserId: stickerpickerWidget.content.creatorUserId || stickerpickerWidget.sender,
}; };

View File

@ -30,7 +30,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import Modal, { IHandle } from "./Modal"; import Modal, { IHandle } from "./Modal";
import { _t } from "./languageHandler"; import { _t, UserFriendlyError } from "./languageHandler";
import dis from "./dispatcher/dispatcher"; import dis from "./dispatcher/dispatcher";
import * as Rooms from "./Rooms"; import * as Rooms from "./Rooms";
import { getAddressType } from "./UserAddress"; import { getAddressType } from "./UserAddress";
@ -121,14 +121,23 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
case "mx-user-id": case "mx-user-id":
createOpts.invite = [opts.dmUserId]; createOpts.invite = [opts.dmUserId];
break; break;
case "email": case "email": {
const isUrl = MatrixClientPeg.get().getIdentityServerUrl(true);
if (!isUrl) {
throw new UserFriendlyError(
"Cannot invite user by email without an identity server. " +
'You can connect to one under "Settings".',
);
}
createOpts.invite_3pid = [ createOpts.invite_3pid = [
{ {
id_server: MatrixClientPeg.get().getIdentityServerUrl(true), id_server: isUrl,
medium: "email", medium: "email",
address: opts.dmUserId, address: opts.dmUserId,
}, },
]; ];
break;
}
} }
} }
if (opts.dmUserId && createOpts.is_direct === undefined) { if (opts.dmUserId && createOpts.is_direct === undefined) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import { ICryptoCallbacks } from "matrix-js-sdk/src/crypto";
import { IMatrixClientCreds } from "../MatrixClientPeg"; import { IMatrixClientCreds } from "../MatrixClientPeg";
import { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast"; import { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast";
@ -41,14 +41,6 @@ function getSecretStorageKey(): Uint8Array | null {
return null; return null;
} }
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function getDehydrationKey(
keyInfo: ISecretStorageKeyInfo,
checkFunc: (key: Uint8Array) => void,
): Promise<Uint8Array | null> {
return Promise.resolve(null);
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function catchAccessSecretStorageError(e: Error): void { function catchAccessSecretStorageError(e: Error): void {
// E.g. notify the user in some way // E.g. notify the user in some way
@ -70,7 +62,7 @@ export interface ISecurityCustomisations {
getSecretStorageKey?: typeof getSecretStorageKey; getSecretStorageKey?: typeof getSecretStorageKey;
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError; catchAccessSecretStorageError?: typeof catchAccessSecretStorageError;
setupEncryptionNeeded?: typeof setupEncryptionNeeded; setupEncryptionNeeded?: typeof setupEncryptionNeeded;
getDehydrationKey?: typeof getDehydrationKey; getDehydrationKey?: ICryptoCallbacks["getDehydrationKey"];
/** /**
* When false, disables the post-login UI from showing. If there's * When false, disables the post-login UI from showing. If there's

View File

@ -62,12 +62,12 @@ export interface EventTileTypeProps {
highlights?: string[]; highlights?: string[];
highlightLink?: string; highlightLink?: string;
showUrlPreview?: boolean; showUrlPreview?: boolean;
onHeightChanged: () => void; onHeightChanged?: () => void;
forExport?: boolean; forExport?: boolean;
getRelationsForEvent?: GetRelationsForEvent; getRelationsForEvent?: GetRelationsForEvent;
editState?: EditorStateTransfer; editState?: EditorStateTransfer;
replacingEventId?: string; replacingEventId?: string;
permalinkCreator: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
callEventGrouper?: LegacyCallEventGrouper; callEventGrouper?: LegacyCallEventGrouper;
isSeeingThroughMessageHiddenForModeration?: boolean; isSeeingThroughMessageHiddenForModeration?: boolean;
timestamp?: JSX.Element; timestamp?: JSX.Element;

View File

@ -23,6 +23,7 @@
"The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.", "The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
"Upload Failed": "Upload Failed", "Upload Failed": "Upload Failed",
"Cannot invite user by email without an identity server. You can connect to one under \"Settings\".": "Cannot invite user by email without an identity server. You can connect to one under \"Settings\".",
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
"The server does not support the room version specified.": "The server does not support the room version specified.", "The server does not support the room version specified.": "The server does not support the room version specified.",
"Failure to create room": "Failure to create room", "Failure to create room": "Failure to create room",
@ -754,7 +755,6 @@
"The user must be unbanned before they can be invited.": "The user must be unbanned before they can be invited.", "The user must be unbanned before they can be invited.": "The user must be unbanned before they can be invited.",
"The user's homeserver does not support the version of the space.": "The user's homeserver does not support the version of the space.", "The user's homeserver does not support the version of the space.": "The user's homeserver does not support the version of the space.",
"The user's homeserver does not support the version of the room.": "The user's homeserver does not support the version of the room.", "The user's homeserver does not support the version of the room.": "The user's homeserver does not support the version of the room.",
"Cannot invite user by email without an identity server. You can connect to one under \"Settings\".": "Cannot invite user by email without an identity server. You can connect to one under \"Settings\".",
"Unknown server error": "Unknown server error", "Unknown server error": "Unknown server error",
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases", "Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
"No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters",

View File

@ -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; 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); this.setWidgetPersistence(widgetId, roomId, false);
} }
@ -102,29 +102,29 @@ export default class ActiveWidgetStore extends EventEmitter {
// Registers the given widget as being docked somewhere in the UI (not a PiP), // Registers the given widget as being docked somewhere in the UI (not a PiP),
// to allow its lifecycle to be tracked. // to allow its lifecycle to be tracked.
public dockWidget(widgetId: string, roomId: string): void { public dockWidget(widgetId: string, roomId: string | null): void {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId); const uid = WidgetUtils.calcWidgetUid(widgetId, roomId ?? undefined);
const refs = this.dockedWidgetsByUid.get(uid) ?? 0; const refs = this.dockedWidgetsByUid.get(uid) ?? 0;
this.dockedWidgetsByUid.set(uid, refs + 1); this.dockedWidgetsByUid.set(uid, refs + 1);
if (refs === 0) this.emit(ActiveWidgetStoreEvent.Dock); if (refs === 0) this.emit(ActiveWidgetStoreEvent.Dock);
} }
public undockWidget(widgetId: string, roomId: string): void { public undockWidget(widgetId: string, roomId: string | null): void {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId); const uid = WidgetUtils.calcWidgetUid(widgetId, roomId ?? undefined);
const refs = this.dockedWidgetsByUid.get(uid); const refs = this.dockedWidgetsByUid.get(uid);
if (refs) this.dockedWidgetsByUid.set(uid, refs - 1); if (refs) this.dockedWidgetsByUid.set(uid, refs - 1);
if (refs === 1) this.emit(ActiveWidgetStoreEvent.Undock); if (refs === 1) this.emit(ActiveWidgetStoreEvent.Undock);
} }
// Determines whether the given widget is docked anywhere in the UI (not a PiP) // Determines whether the given widget is docked anywhere in the UI (not a PiP)
public isDocked(widgetId: string, roomId: string): boolean { public isDocked(widgetId: string, roomId: string | null): boolean {
const uid = WidgetUtils.calcWidgetUid(widgetId, roomId); const uid = WidgetUtils.calcWidgetUid(widgetId, roomId ?? undefined);
const refs = this.dockedWidgetsByUid.get(uid) ?? 0; const refs = this.dockedWidgetsByUid.get(uid) ?? 0;
return refs > 0; return refs > 0;
} }
// Determines whether the given widget is being kept alive in the UI, including PiPs // 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); return this.isDocked(widgetId, roomId) || this.getWidgetPersistence(widgetId, roomId);
} }
} }

View File

@ -38,6 +38,10 @@ export interface IApp extends IWidget {
avatar_url?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765 avatar_url?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
} }
export function isAppWidget(widget: IWidget | IApp): widget is IApp {
return "roomId" in widget && typeof widget.roomId === "string";
}
interface IRoomWidgets { interface IRoomWidgets {
widgets: IApp[]; widgets: IApp[];
} }

View File

@ -55,7 +55,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { ElementWidgetActions, IHangupCallApiRequest, IViewRoomApiRequest } from "./ElementWidgetActions"; import { ElementWidgetActions, IHangupCallApiRequest, IViewRoomApiRequest } from "./ElementWidgetActions";
import { ModalWidgetStore } from "../ModalWidgetStore"; import { ModalWidgetStore } from "../ModalWidgetStore";
import { IApp } from "../WidgetStore"; import { IApp, isAppWidget } from "../WidgetStore";
import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { getCustomTheme } from "../../theme"; import { getCustomTheme } from "../../theme";
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities"; import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
@ -72,7 +72,7 @@ import { SdkContextClass } from "../../contexts/SDKContext";
interface IAppTileProps { interface IAppTileProps {
// Note: these are only the props we care about // Note: these are only the props we care about
app: IApp; app: IApp | IWidget;
room?: Room; // without a room it is a user widget room?: Room; // without a room it is a user widget
userId: string; userId: string;
creatorUserId: string; creatorUserId: string;
@ -179,7 +179,7 @@ export class StopGapWidget extends EventEmitter {
this.mockWidget = new ElementWidget(app); this.mockWidget = new ElementWidget(app);
this.roomId = appTileProps.room?.roomId; this.roomId = appTileProps.room?.roomId;
this.kind = appTileProps.userWidget ? WidgetKind.Account : WidgetKind.Room; // probably 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> { private get eventListenerRoomId(): Optional<string> {

View File

@ -19,6 +19,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import { compare, MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils"; import { compare, MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils";
import { IWidget } from "matrix-widget-api";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import WidgetStore, { IApp } from "../WidgetStore"; import WidgetStore, { IApp } from "../WidgetStore";
@ -362,11 +363,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
} }
} }
public getContainerWidgets(room: Optional<Room>, container: Container): IApp[] { public getContainerWidgets(room: Optional<Room>, container: Container): IWidget[] {
return (room && this.byRoom.get(room.roomId)?.get(container)?.ordered) || []; return (room && this.byRoom.get(room.roomId)?.get(container)?.ordered) || [];
} }
public isInContainer(room: Room, widget: IApp, container: Container): boolean { public isInContainer(room: Room, widget: IWidget, container: Container): boolean {
return this.getContainerWidgets(room, container).some((w) => w.id === widget.id); return this.getContainerWidgets(room, container).some((w) => w.id === widget.id);
} }
@ -437,7 +438,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
this.updateUserLayout(room, localLayout); this.updateUserLayout(room, localLayout);
} }
public moveWithinContainer(room: Room, container: Container, widget: IApp, delta: number): void { public moveWithinContainer(room: Room, container: Container, widget: IWidget, delta: number): void {
const widgets = arrayFastClone(this.getContainerWidgets(room, container)); const widgets = arrayFastClone(this.getContainerWidgets(room, container));
const currentIdx = widgets.findIndex((w) => w.id === widget.id); const currentIdx = widgets.findIndex((w) => w.id === widget.id);
if (currentIdx < 0) return; // no change needed if (currentIdx < 0) return; // no change needed
@ -460,7 +461,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
this.updateUserLayout(room, localLayout); this.updateUserLayout(room, localLayout);
} }
public moveToContainer(room: Room, widget: IApp, toContainer: Container): void { public moveToContainer(room: Room, widget: IWidget, toContainer: Container): void {
const allWidgets = this.getAllWidgets(room); const allWidgets = this.getAllWidgets(room);
if (!allWidgets.some(([w]) => w.id === widget.id)) return; // invalid if (!allWidgets.some(([w]) => w.id === widget.id)) return; // invalid
// Prepare other containers (potentially move widgets to obey the following rules) // Prepare other containers (potentially move widgets to obey the following rules)

View File

@ -35,7 +35,7 @@ import { WidgetType } from "../widgets/WidgetType";
import { Jitsi } from "../widgets/Jitsi"; import { Jitsi } from "../widgets/Jitsi";
import { objectClone } from "./objects"; import { objectClone } from "./objects";
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
import { IApp } from "../stores/WidgetStore"; import { IApp, isAppWidget } from "../stores/WidgetStore";
// How long we wait for the state event echo to come back from the server // How long we wait for the state event echo to come back from the server
// before waitFor[Room/User]Widget rejects its promise // before waitFor[Room/User]Widget rejects its promise
@ -545,30 +545,30 @@ export default class WidgetUtils {
return url.href; return url.href;
} }
public static getWidgetName(app?: IApp): string { public static getWidgetName(app?: IWidget): string {
return app?.name?.trim() || _t("Unknown App"); return app?.name?.trim() || _t("Unknown App");
} }
public static getWidgetDataTitle(app?: IApp): string { public static getWidgetDataTitle(app?: IWidget): string {
return app?.data?.title?.trim() || ""; return app?.data?.title?.trim() || "";
} }
public static getWidgetUid(app?: IApp): string { public static getWidgetUid(app?: IApp | IWidget): string {
return app ? WidgetUtils.calcWidgetUid(app.id, app.roomId) : ""; return app ? WidgetUtils.calcWidgetUid(app.id, isAppWidget(app) ? app.roomId : undefined) : "";
} }
public static calcWidgetUid(widgetId: string, roomId?: string): string { public static calcWidgetUid(widgetId: string, roomId?: string): string {
return roomId ? `room_${roomId}_${widgetId}` : `user_${widgetId}`; return roomId ? `room_${roomId}_${widgetId}` : `user_${widgetId}`;
} }
public static editWidget(room: Room, app: IApp): void { public static editWidget(room: Room, app: IWidget): void {
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
IntegrationManagers.sharedInstance() IntegrationManagers.sharedInstance()
.getPrimaryManager() .getPrimaryManager()
?.open(room, "type_" + app.type, app.id); ?.open(room, "type_" + app.type, app.id);
} }
public static isManagedByManager(app: IApp): boolean { public static isManagedByManager(app: IWidget): boolean {
if (WidgetUtils.isScalarUrl(app.url)) { if (WidgetUtils.isScalarUrl(app.url)) {
const managers = IntegrationManagers.sharedInstance(); const managers = IntegrationManagers.sharedInstance();
if (managers.hasManager()) { if (managers.hasManager()) {

View File

@ -117,9 +117,9 @@ export abstract class Member {
/** /**
* Gets the MXC URL of this Member's avatar. For users this should be their profile's * Gets the MXC URL of this Member's avatar. For users this should be their profile's
* avatar MXC URL or null if none set. For 3PIDs this should always be null. * avatar MXC URL or null if none set. For 3PIDs this should always be undefined.
*/ */
public abstract getMxcAvatarUrl(): string | null; public abstract getMxcAvatarUrl(): string | undefined;
} }
export class DirectoryMember extends Member { export class DirectoryMember extends Member {
@ -144,8 +144,8 @@ export class DirectoryMember extends Member {
return this._userId; return this._userId;
} }
public getMxcAvatarUrl(): string | null { public getMxcAvatarUrl(): string | undefined {
return this.avatarUrl ?? null; return this.avatarUrl;
} }
} }
@ -173,8 +173,8 @@ export class ThreepidMember extends Member {
return this.id; return this.id;
} }
public getMxcAvatarUrl(): string | null { public getMxcAvatarUrl(): string | undefined {
return null; return undefined;
} }
} }

View File

@ -17,7 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { ClientWidgetApi, MatrixWidgetType } from "matrix-widget-api"; import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import { act, render, RenderResult } from "@testing-library/react"; import { act, render, RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
@ -366,7 +366,7 @@ describe("AppTile", () => {
describe("for a maximised (centered) widget", () => { describe("for a maximised (centered) widget", () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation( jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
(room: Optional<Room>, widget: IApp, container: Container) => { (room: Optional<Room>, widget: IWidget, container: Container) => {
return room === r1 && widget === app1 && container === Container.Center; return room === r1 && widget === app1 && container === Container.Center;
}, },
); );