diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 64044c4c5c1..1aea3ae23bf 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -211,6 +211,10 @@ Please see LICENSE files in the repository root for full details. } } + &.mx_SpaceButton_withIcon .mx_SpaceButton_icon { + background-color: $panel-actions; + } + &.mx_SpaceButton_home .mx_SpaceButton_icon::before { mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg"); } diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts index 5eddeecb2ae..ae67b5f378e 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -31,7 +31,7 @@ const notLoggedInMap: Record, ScreenName> = { [Views.LOCK_STOLEN]: "SessionLockStolen", }; -const loggedInPageTypeMap: Record = { +const loggedInPageTypeMap: Record = { [PageType.HomePage]: "Home", [PageType.RoomView]: "Room", [PageType.UserView]: "User", @@ -48,10 +48,10 @@ export default class PosthogTrackers { } private view: Views = Views.LOADING; - private pageType?: PageType; + private pageType?: PageType | string; private override?: ScreenName; - public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void { + public trackPageChange(view: Views, pageType: PageType | string | undefined, durationMs: number): void { this.view = view; this.pageType = pageType; if (this.override) return; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 907e40dede1..009ef704b2e 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -69,6 +69,7 @@ import { type ConfigOptions } from "../../SdkConfig"; import { MatrixClientContextProvider } from "./MatrixClientContextProvider"; import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation"; import { SDKContext } from "../../contexts/SDKContext.ts"; +import ModuleApi from "../../modules/Api.ts"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -679,6 +680,10 @@ class LoggedInView extends React.Component { public render(): React.ReactNode { let pageElement; + const moduleRenderer = this.props.page_type + ? ModuleApi.navigation.locationRenderers.get(this.props.page_type) + : undefined; + switch (this.props.page_type) { case PageTypes.RoomView: pageElement = ( @@ -705,6 +710,13 @@ class LoggedInView extends React.Component { ); } break; + default: { + if (moduleRenderer) { + pageElement = moduleRenderer(); + } else { + console.warn(`Couldn't render page type "${this.props.page_type}"`); + } + } } const wrapperClasses = classNames({ @@ -746,20 +758,22 @@ class LoggedInView extends React.Component { )} {!useNewRoomList && } -
- -
+ {!moduleRenderer && ( +
+ +
+ )} - + {!moduleRenderer && }
{pageElement}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a4df7a5fe9e..ea4cc36f8d4 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -140,6 +140,7 @@ import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/ShareP import Markdown from "../../Markdown"; import { sanitizeHtmlParams } from "../../Linkify"; import { isOnlyAdmin } from "../../utils/membership"; +import ModuleApi from "../../modules/Api.ts"; // legacy export export { default as Views } from "../../Views"; @@ -175,9 +176,11 @@ interface IProps { interface IState { // the master view we are showing. view: Views; - // What the LoggedInView would be showing if visible + // What the LoggedInView would be showing if visible. + // A member of the enum for standard pages or a string for those provided by + // a module. // eslint-disable-next-line camelcase - page_type?: PageType; + page_type?: PageType | string; // The ID of the room we're viewing. This is either populated directly // in the case where we view a room by ID or by RoomView when it resolves // what ID an alias points at. @@ -1922,7 +1925,9 @@ export default class MatrixChat extends React.PureComponent { subAction: params?.action, }); } else { - logger.info(`Ignoring showScreen for '${screen}'`); + if (ModuleApi.navigation.locationRenderers.get(screen)) { + this.setState({ page_type: screen }); + } } } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 10647ee86b5..8f0870c06aa 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -44,6 +44,7 @@ import { type CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { debounce, throttle } from "lodash"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; +import { type RoomViewProps } from "@element-hq/element-web-module-api"; import shouldHideEvent from "../../shouldHideEvent"; import { _t } from "../../languageHandler"; @@ -147,7 +148,7 @@ if (DEBUG) { debuglog = logger.log.bind(console); } -interface IRoomProps { +interface IRoomProps extends RoomViewProps { threepidInvite?: IThreepidInvite; oobData?: IOOBData; diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 984b33bb12e..39b246c6980 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -69,6 +69,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation"; import { KeyboardShortcut } from "../settings/KeyboardShortcut"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement"; +import ModuleApi from "../../../modules/Api.ts"; const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => { const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { @@ -341,6 +342,7 @@ const InnerSpacePanel = React.memo( ))} {children} + {ModuleApi.extras.spacePanelItems.map((renderer) => renderer({ isPanelCollapsed }))} {shouldShowComponent(UIComponent.CreateSpaces) && ( )} diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index d03ac8a1e20..f2c41c367f6 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -52,6 +52,7 @@ type ButtonProps = Omit< className?: string; selected?: boolean; label: string; + icon?: JSX.Element; contextMenuTooltip?: string; notificationState?: NotificationState; isNarrow?: boolean; @@ -65,6 +66,7 @@ export const SpaceButton = ({ space, spaceKey: _spaceKey, className, + icon, selected, label, contextMenuTooltip, @@ -84,7 +86,7 @@ export const SpaceButton = ({ let avatar = (
-
+
{icon}
); if (space) { @@ -143,6 +145,7 @@ export const SpaceButton = ({ mx_SpaceButton_active: selected, mx_SpaceButton_hasMenuOpen: menuDisplayed, mx_SpaceButton_narrow: isNarrow, + mx_SpaceButton_withIcon: Boolean(icon), })} aria-label={label} title={!isNarrow || menuDisplayed ? undefined : label} diff --git a/src/modules/Api.ts b/src/modules/Api.ts index 49082daef2e..72c8ed9ebbf 100644 --- a/src/modules/Api.ts +++ b/src/modules/Api.ts @@ -26,6 +26,7 @@ import { WatchableProfile } from "./Profile.ts"; import { NavigationApi } from "./Navigation.ts"; import { openDialog } from "./Dialog.tsx"; import { overwriteAccountAuth } from "./Auth.ts"; +import { ElementWebExtrasApi } from "./ExtrasApi.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -79,6 +80,7 @@ export class ModuleApi implements Api { public readonly config = new ConfigApi(); public readonly i18n = new I18nApi(); public readonly customComponents = new CustomComponentsApi(); + public readonly extras = new ElementWebExtrasApi(); public readonly rootNode = document.getElementById("matrixchat")!; public createRoot(element: Element): Root { diff --git a/src/modules/BuiltinsApi.ts b/src/modules/BuiltinsApi.ts new file mode 100644 index 00000000000..20bd6343e60 --- /dev/null +++ b/src/modules/BuiltinsApi.ts @@ -0,0 +1,16 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type RoomViewProps, type BuiltinsApi } from "@element-hq/element-web-module-api"; + +import { RoomView } from "../components/structures/RoomView"; + +export class ElementWebBuiltinsApi implements BuiltinsApi { + public getRoomViewComponent(): React.ComponentType { + return RoomView; + } +} diff --git a/src/modules/ExtrasApi.ts b/src/modules/ExtrasApi.ts new file mode 100644 index 00000000000..fb98e132808 --- /dev/null +++ b/src/modules/ExtrasApi.ts @@ -0,0 +1,16 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type SpacePanelItemRenderFunction, type ExtrasApi } from "@element-hq/element-web-module-api"; + +export class ElementWebExtrasApi implements ExtrasApi { + public spacePanelItems: SpacePanelItemRenderFunction[] = []; + + public addSpacePanelItem(renderer: SpacePanelItemRenderFunction): void { + this.spacePanelItems.push(renderer); + } +} diff --git a/src/modules/Navigation.ts b/src/modules/Navigation.ts index 0e7724727d3..52bdb5aee9b 100644 --- a/src/modules/Navigation.ts +++ b/src/modules/Navigation.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type NavigationApi as INavigationApi } from "@element-hq/element-web-module-api"; +import { type LocationRenderFunction, type NavigationApi as INavigationApi } from "@element-hq/element-web-module-api"; import { navigateToPermalink } from "../utils/permalinks/navigator.ts"; import { parsePermalink } from "../utils/permalinks/Permalinks.ts"; @@ -14,6 +14,8 @@ import { Action } from "../dispatcher/actions.ts"; import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts"; export class NavigationApi implements INavigationApi { + public locationRenderers = new Map(); + public async toMatrixToLink(link: string, join = false): Promise { navigateToPermalink(link); @@ -38,4 +40,8 @@ export class NavigationApi implements INavigationApi { } } } + + public registerLocationRenderer(path: string, renderer: LocationRenderFunction): void { + this.locationRenderers.set(path, renderer); + } }