feat(settings): prepend module tabs in UserSettingsDialog

This commit is contained in:
Joël Poyet
2026-03-02 23:39:47 +01:00
committed by Joël
parent f0907d75f7
commit 9a9007d554
2 changed files with 61 additions and 3 deletions
@@ -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<Tab<UserTab>> => {
const tabs: Tab<UserTab>[] = [];
// 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<Tab<UserTab>>;
};
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
+37 -1
View File
@@ -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<keyof EmittedEvents, EmittedEvents> implements ExtrasApi {
public spacePanelItems = new Map<string, SpacePanelItemProps>();
public visibleRoomBySpaceKey = new Map<string, () => string[]>();
public roomHeaderButtonsCallbacks: RoomHeaderButtonsCallback[] = [];
public settingsTabs = new Map<string, ModuleSettingsTab>();
public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void {
this.spacePanelItems.set(spacekey, item);
@@ -45,6 +55,11 @@ export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents,
public addRoomHeaderButtonCallback(cb: RoomHeaderButtonsCallback): void {
this.roomHeaderButtonsCallbacks.push(cb);
}
public registerSettingsTab(key: string, tab: ModuleSettingsTab): void {
this.settingsTabs.set(key, tab);
this.emit(ExtrasApiEvent.SettingsTabsChanged);
}
}
export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpacePanelItem[] {
@@ -63,3 +78,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<ModuleSettingsTabEntry[]>(getTabs);
useTypedEventEmitter(api, ExtrasApiEvent.SettingsTabsChanged, () => {
setTabs(getTabs());
});
return tabs;
}