From 903c74e62f50bcb54c6527b8a7d0ebd2f3440937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Poyet?= Date: Mon, 2 Mar 2026 23:39:47 +0100 Subject: [PATCH] feat(settings): prepend module tabs in UserSettingsDialog --- .../views/dialogs/UserSettingsDialog.tsx | 26 ++++++++++++- src/modules/ExtrasApi.ts | 38 ++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index eb52facf2b..01300b870f 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -23,8 +23,10 @@ import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock"; import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block"; import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, type TranslationKey } from "../../../languageHandler"; import AccountUserSettingsTab from "../settings/tabs/user/AccountUserSettingsTab"; +import { useModuleSettingsTabs } from "../../../modules/ExtrasApi"; +import { ModuleApi } from "../../../modules/Api"; import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; @@ -87,12 +89,19 @@ function titleForTabID(tabId: UserTab): React.ReactNode { return _t("settings|labs_mjolnir|dialog_title", undefined, subs); case UserTab.Help: return _t("setting|help_about|dialog_title", undefined, subs); + default: { + // Module-registered tab — look up the label from the extras API + const moduleTab = ModuleApi.instance.extras.settingsTabs.get(tabId); + if (moduleTab) return moduleTab.label; + return tabId; + } } } export default function UserSettingsDialog(props: IProps): JSX.Element { const voipEnabled = useSettingValue(UIFeature.Voip); const mjolnirEnabled = useSettingValue("feature_mjolnir"); + const moduleSettingsTabs = useModuleSettingsTabs(ModuleApi.instance.extras); // store these props in state as changing tabs back and forth should clear them const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode); const [initialEncryptionState, setInitialEncryptionState] = useState(props.initialEncryptionState); @@ -120,6 +129,18 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { const getTabs = (): NonEmptyArray> => { const tabs: Tab[] = []; + // Prepend module-registered settings tabs (e.g. HealthChat "Profil") + for (const moduleTab of moduleSettingsTabs) { + tabs.push( + new Tab( + moduleTab.key as UserTab, + moduleTab.label as TranslationKey, + moduleTab.icon, + moduleTab.body(), + ), + ); + } + tabs.push( new Tab( UserTab.Account, @@ -249,7 +270,8 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { return tabs as NonEmptyArray>; }; - const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.Account, props.initialTabId); + const defaultTab = moduleSettingsTabs.length > 0 ? (moduleSettingsTabs[0].key as UserTab) : UserTab.Account; + const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), defaultTab, props.initialTabId); const setActiveTabId = (tabId: UserTab): void => { _setActiveTabId(tabId); // Clear these so switching away from the tab and back to it will not show the QR code again diff --git a/src/modules/ExtrasApi.ts b/src/modules/ExtrasApi.ts index 420b17130a..ff5f660bba 100644 --- a/src/modules/ExtrasApi.ts +++ b/src/modules/ExtrasApi.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 { useState } from "react"; +import { type JSX, useState } from "react"; import { type SpacePanelItemProps, type ExtrasApi } from "@element-hq/element-web-module-api"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; @@ -15,17 +15,27 @@ export interface ModuleSpacePanelItem extends SpacePanelItemProps { spaceKey: string; } +/** Shape of a settings tab registered by a module plugin. */ +export interface ModuleSettingsTab { + label: string; + icon: JSX.Element | string | null; + body: () => JSX.Element; +} + enum ExtrasApiEvent { SpacePanelItemsChanged = "SpacePanelItemsChanged", + SettingsTabsChanged = "SettingsTabsChanged", } interface EmittedEvents { [ExtrasApiEvent.SpacePanelItemsChanged]: () => void; + [ExtrasApiEvent.SettingsTabsChanged]: () => void; } export class ElementWebExtrasApi extends TypedEventEmitter implements ExtrasApi { public spacePanelItems = new Map(); public visibleRoomBySpaceKey = new Map string[]>(); + public settingsTabs = new Map(); public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void { this.spacePanelItems.set(spacekey, item); @@ -35,6 +45,11 @@ export class ElementWebExtrasApi extends TypedEventEmitter string[]): void { this.visibleRoomBySpaceKey.set(spaceKey, cb); } + + public registerSettingsTab(key: string, tab: ModuleSettingsTab): void { + this.settingsTabs.set(key, tab); + this.emit(ExtrasApiEvent.SettingsTabsChanged); + } } export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpacePanelItem[] { @@ -53,3 +68,24 @@ export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpaceP return items; } + +export interface ModuleSettingsTabEntry extends ModuleSettingsTab { + key: string; +} + +export function useModuleSettingsTabs(api: ElementWebExtrasApi): ModuleSettingsTabEntry[] { + const getTabs = (): ModuleSettingsTabEntry[] => { + return Array.from(api.settingsTabs.entries()).map(([key, tab]) => ({ + key, + ...tab, + })); + }; + + const [tabs, setTabs] = useState(getTabs); + + useTypedEventEmitter(api, ExtrasApiEvent.SettingsTabsChanged, () => { + setTabs(getTabs()); + }); + + return tabs; +}