diff --git a/apps/web/src/components/views/dialogs/UserSettingsDialog.tsx b/apps/web/src/components/views/dialogs/UserSettingsDialog.tsx index 2bc3714920..024adf18a1 100644 --- a/apps/web/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/apps/web/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/apps/web/src/modules/ExtrasApi.ts b/apps/web/src/modules/ExtrasApi.ts index c82925889c..f646947cb9 100644 --- a/apps/web/src/modules/ExtrasApi.ts +++ b/apps/web/src/modules/ExtrasApi.ts @@ -6,7 +6,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, @@ -20,18 +20,28 @@ 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 roomHeaderButtonsCallbacks: RoomHeaderButtonsCallback[] = []; + public settingsTabs = new Map(); public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void { this.spacePanelItems.set(spacekey, item); @@ -45,6 +55,11 @@ export class ElementWebExtrasApi extends TypedEventEmitter { + return Array.from(api.settingsTabs.entries()).map(([key, tab]) => ({ + key, + ...tab, + })); + }; + + const [tabs, setTabs] = useState(getTabs); + + useTypedEventEmitter(api, ExtrasApiEvent.SettingsTabsChanged, () => { + setTabs(getTabs()); + }); + + return tabs; +}