diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index c1faab885a..1249f4c47e 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -126,6 +126,7 @@ export const RoomSearchView = forwardRef( setHighlights(highlights); setResults({ ...results }); // copy to force a refresh + return false; }, (error) => { if (aborted.current) { diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx index e02e924365..31d3a988c5 100644 --- a/src/components/views/avatars/WidgetAvatar.tsx +++ b/src/components/views/avatars/WidgetAvatar.tsx @@ -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, "name" | "url" | "urls" | "height" | "width"> { - app: IApp; + app: IApp | IWidget; height?: number; width?: number; } @@ -46,7 +47,7 @@ const WidgetAvatar: React.FC = ({ 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} diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index a70f07a0b1..146a54fb05 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -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"; @@ -39,7 +39,7 @@ import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; interface IProps extends Omit, "children"> { - app: IApp; + app: IWidget; userWidget?: boolean; showUnpin?: boolean; // override delete handler @@ -155,7 +155,9 @@ export const WidgetContextMenu: React.FC = ({ } 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); @@ -166,9 +168,10 @@ export const WidgetContextMenu: React.FC = ({ 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) => { diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 2749836ee1..1fa1fba752 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -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"; @@ -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"; @@ -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; @@ -133,7 +133,10 @@ export default class AppTile extends React.Component { // 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 @@ -169,14 +172,17 @@ export default class AppTile extends React.Component { 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. @@ -188,7 +194,10 @@ export default class AppTile extends React.Component { 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, + ); } } } @@ -243,7 +252,10 @@ export default class AppTile extends React.Component { 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(); } @@ -288,13 +300,21 @@ export default class AppTile extends React.Component { 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(); } @@ -395,7 +415,10 @@ export default class AppTile extends React.Component { // 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 }); } @@ -441,9 +464,10 @@ export default class AppTile extends React.Component { 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(() => { @@ -550,7 +574,7 @@ export default class AppTile extends React.Component { }; 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 diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 6409464701..3df327b2a2 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -47,7 +47,7 @@ interface IProps { // the latest event in this chain of replies parentEv: MatrixEvent; // called when the ReplyChain contents has changed, including EventTiles thereof - onHeightChanged: () => void; + onHeightChanged?: () => void; permalinkCreator?: RoomPermalinkCreator; // Specifies which layout to use. layout?: Layout; @@ -104,7 +104,7 @@ export default class ReplyChain extends React.Component { } public componentDidUpdate(): void { - this.props.onHeightChanged(); + this.props.onHeightChanged?.(); this.trySetExpandableQuotes(); } diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts index e16de3ccd8..e9b853621a 100644 --- a/src/components/views/messages/IBodyProps.ts +++ b/src/components/views/messages/IBodyProps.ts @@ -32,7 +32,7 @@ export interface IBodyProps { highlightLink?: string; /* callback called when dynamic content in events are loaded */ - onHeightChanged: () => void; + onHeightChanged?: () => void; showUrlPreview?: boolean; forExport?: boolean; @@ -40,7 +40,7 @@ export interface IBodyProps { replacingEventId?: string; editState?: EditorStateTransfer; onMessageAllowed: () => void; // TODO: Docs - permalinkCreator: RoomPermalinkCreator; + permalinkCreator?: RoomPermalinkCreator; mediaEventHelper: MediaEventHelper; /* diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index e7c0964d16..917c5d62eb 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -174,7 +174,7 @@ export default class MImageBody extends React.Component { private onImageLoad = (): void => { this.clearBlurhashTimeout(); - this.props.onHeightChanged(); + this.props.onHeightChanged?.(); let loadedImageDimensions: IState["loadedImageDimensions"]; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 5c2cd78fa8..5ecf74a8b6 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -154,7 +154,7 @@ export default class MVideoBody extends React.PureComponent decryptedThumbnailUrl: thumbnailUrl, decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, }); - this.props.onHeightChanged(); + this.props.onHeightChanged?.(); } else { logger.log("NOT preloading video"); const content = this.props.mxEvent.getContent(); @@ -216,7 +216,7 @@ export default class MVideoBody extends React.PureComponent this.videoRef.current.play(); }, ); - this.props.onHeightChanged(); + this.props.onHeightChanged?.(); }; protected get showFileBody(): boolean { diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index fa4008e144..904432b2e6 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -173,7 +173,7 @@ export default class TextualBody extends React.Component { // By expanding/collapsing we changed // the height, therefore we call this - this.props.onHeightChanged(); + this.props.onHeightChanged?.(); }; div.appendChild(button); diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index 2014455768..b9e2132f89 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -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"; @@ -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"; @@ -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 @@ -147,7 +147,7 @@ export default class AppsDrawer extends React.Component { 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) { @@ -210,8 +210,8 @@ export default class AppsDrawer extends React.Component { [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; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 925611ab80..13a8e6b006 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -513,6 +513,7 @@ export class UnwrappedEventTile extends React.Component evt.preventDefault(); evt.stopPropagation(); const { permalinkCreator, mxEvent } = this.props; + if (!permalinkCreator) return; const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId()!); await copyPlaintext(matrixToUrl); }; @@ -1439,7 +1440,7 @@ export class UnwrappedEventTile extends React.Component const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject) => { return ( <> - + diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx index e4a3df8c1d..6cd2ce8db8 100644 --- a/src/components/views/rooms/LinkPreviewGroup.tsx +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -32,7 +32,7 @@ interface IProps { links: string[]; // the URLs to be previewed mxEvent: MatrixEvent; // the Event associated with the preview 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 = ({ links, mxEvent, onCancelClick, onHeightChanged }) => { @@ -49,7 +49,7 @@ const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick, onH ); useEffect(() => { - onHeightChanged(); + onHeightChanged?.(); }, [onHeightChanged, expanded, previews]); const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS); diff --git a/src/components/views/rooms/Stickerpicker.tsx b/src/components/views/rooms/Stickerpicker.tsx index 1e14164620..92b7ddd650 100644 --- a/src/components/views/rooms/Stickerpicker.tsx +++ b/src/components/views/rooms/Stickerpicker.tsx @@ -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"; @@ -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"; @@ -264,15 +264,12 @@ export default class Stickerpicker extends React.PureComponent { 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, }; diff --git a/src/createRoom.ts b/src/createRoom.ts index cf090b06ca..1b600657cf 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -30,7 +30,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "./MatrixClientPeg"; import Modal, { IHandle } from "./Modal"; -import { _t } from "./languageHandler"; +import { _t, UserFriendlyError } from "./languageHandler"; import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; import { getAddressType } from "./UserAddress"; @@ -121,14 +121,23 @@ export default async function createRoom(opts: IOpts): Promise { case "mx-user-id": createOpts.invite = [opts.dmUserId]; 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 = [ { - id_server: MatrixClientPeg.get().getIdentityServerUrl(true), + id_server: isUrl, medium: "email", address: opts.dmUserId, }, ]; + break; + } } } if (opts.dmUserId && createOpts.is_direct === undefined) { diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts index 2d83a564f1..ae5a905f56 100644 --- a/src/customisations/Security.ts +++ b/src/customisations/Security.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and 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 { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast"; @@ -41,14 +41,6 @@ function getSecretStorageKey(): Uint8Array | null { return null; } -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function getDehydrationKey( - keyInfo: ISecretStorageKeyInfo, - checkFunc: (key: Uint8Array) => void, -): Promise { - return Promise.resolve(null); -} - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function catchAccessSecretStorageError(e: Error): void { // E.g. notify the user in some way @@ -70,7 +62,7 @@ export interface ISecurityCustomisations { getSecretStorageKey?: typeof getSecretStorageKey; catchAccessSecretStorageError?: typeof catchAccessSecretStorageError; setupEncryptionNeeded?: typeof setupEncryptionNeeded; - getDehydrationKey?: typeof getDehydrationKey; + getDehydrationKey?: ICryptoCallbacks["getDehydrationKey"]; /** * When false, disables the post-login UI from showing. If there's diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 8bb9c28c44..1e87810d44 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -62,12 +62,12 @@ export interface EventTileTypeProps { highlights?: string[]; highlightLink?: string; showUrlPreview?: boolean; - onHeightChanged: () => void; + onHeightChanged?: () => void; forExport?: boolean; getRelationsForEvent?: GetRelationsForEvent; editState?: EditorStateTransfer; replacingEventId?: string; - permalinkCreator: RoomPermalinkCreator; + permalinkCreator?: RoomPermalinkCreator; callEventGrouper?: LegacyCallEventGrouper; isSeeingThroughMessageHiddenForModeration?: boolean; timestamp?: JSX.Element; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9a6b076cfd..3847915e76 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -23,6 +23,7 @@ "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", "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.", "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", @@ -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'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.", - "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", "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", diff --git a/src/stores/ActiveWidgetStore.ts b/src/stores/ActiveWidgetStore.ts index 42e8d739ac..90e349e668 100644 --- a/src/stores/ActiveWidgetStore.ts +++ b/src/stores/ActiveWidgetStore.ts @@ -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); } @@ -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); } } diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 2dd2ca9bdc..f58e792f3e 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -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 "roomId" in widget && typeof widget.roomId === "string"; +} + interface IRoomWidgets { widgets: IApp[]; } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 90f1d32ff1..781b152cea 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -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"; @@ -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; @@ -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 { diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index 2bfd555ea3..1942048a6b 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -19,6 +19,7 @@ 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, MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils"; +import { IWidget } from "matrix-widget-api"; import SettingsStore from "../../settings/SettingsStore"; import WidgetStore, { IApp } from "../WidgetStore"; @@ -362,11 +363,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore { } } - public getContainerWidgets(room: Optional, container: Container): IApp[] { + public getContainerWidgets(room: Optional, container: Container): IWidget[] { 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); } @@ -437,7 +438,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { 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 currentIdx = widgets.findIndex((w) => w.id === widget.id); if (currentIdx < 0) return; // no change needed @@ -460,7 +461,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { 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); if (!allWidgets.some(([w]) => w.id === widget.id)) return; // invalid // Prepare other containers (potentially move widgets to obey the following rules) diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 2c4f01b011..74d5e0bd54 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -35,7 +35,7 @@ import { WidgetType } from "../widgets/WidgetType"; import { Jitsi } from "../widgets/Jitsi"; import { objectClone } from "./objects"; 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 // before waitFor[Room/User]Widget rejects its promise @@ -545,30 +545,30 @@ export default class WidgetUtils { return url.href; } - public static getWidgetName(app?: IApp): string { + public static getWidgetName(app?: IWidget): string { return app?.name?.trim() || _t("Unknown App"); } - public static getWidgetDataTitle(app?: IApp): string { + public static getWidgetDataTitle(app?: IWidget): string { return app?.data?.title?.trim() || ""; } - public static getWidgetUid(app?: IApp): string { - return app ? WidgetUtils.calcWidgetUid(app.id, app.roomId) : ""; + public static getWidgetUid(app?: IApp | IWidget): string { + return app ? WidgetUtils.calcWidgetUid(app.id, isAppWidget(app) ? app.roomId : undefined) : ""; } public static calcWidgetUid(widgetId: string, roomId?: string): string { 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 IntegrationManagers.sharedInstance() .getPrimaryManager() ?.open(room, "type_" + app.type, app.id); } - public static isManagedByManager(app: IApp): boolean { + public static isManagedByManager(app: IWidget): boolean { if (WidgetUtils.isScalarUrl(app.url)) { const managers = IntegrationManagers.sharedInstance(); if (managers.hasManager()) { diff --git a/src/utils/direct-messages.ts b/src/utils/direct-messages.ts index 9174defdfa..070685d2f2 100644 --- a/src/utils/direct-messages.ts +++ b/src/utils/direct-messages.ts @@ -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 - * 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 { @@ -144,8 +144,8 @@ export class DirectoryMember extends Member { return this._userId; } - public getMxcAvatarUrl(): string | null { - return this.avatarUrl ?? null; + public getMxcAvatarUrl(): string | undefined { + return this.avatarUrl; } } @@ -173,8 +173,8 @@ export class ThreepidMember extends Member { return this.id; } - public getMxcAvatarUrl(): string | null { - return null; + public getMxcAvatarUrl(): string | undefined { + return undefined; } } diff --git a/test/components/views/elements/AppTile-test.tsx b/test/components/views/elements/AppTile-test.tsx index 27ff5a1503..135ba7f9bf 100644 --- a/test/components/views/elements/AppTile-test.tsx +++ b/test/components/views/elements/AppTile-test.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { jest } from "@jest/globals"; 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 { act, render, RenderResult } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -366,7 +366,7 @@ describe("AppTile", () => { describe("for a maximised (centered) widget", () => { beforeEach(() => { jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation( - (room: Optional, widget: IApp, container: Container) => { + (room: Optional, widget: IWidget, container: Container) => { return room === r1 && widget === app1 && container === Container.Center; }, );