diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..069c694784 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(ls:*)", + "Bash(test:*)", + "Bash(yarn test:*)", + "Bash(npx tsc:*)", + "Bash(yarn lint:types:*)" + ] + } +} diff --git a/res/themes/light-custom/css/_custom.pcss b/res/themes/light-custom/css/_custom.pcss index 043de30191..c66c65d36e 100644 --- a/res/themes/light-custom/css/_custom.pcss +++ b/res/themes/light-custom/css/_custom.pcss @@ -103,6 +103,7 @@ $event-highlight-bg-color: var(--timeline-highlights-color); /* redirect some variables away from their hardcoded values in the light theme */ $settings-grey-fg-color: $primary-content; +$tab-label-active-fg-color: $primary-content; /* --eventbubble colors */ $eventbubble-self-bg: var(--eventbubble-self-bg, $eventbubble-self-bg); diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index eb52facf2b..737e1b687d 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,7 +129,20 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { const getTabs = (): NonEmptyArray> => { const tabs: Tab[] = []; - tabs.push( + // 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(), + ), + ); + } + + // JP: Disabled Account tab for HealthChat (replaced by module "Profil" tab) + /*tabs.push( new Tab( UserTab.Account, _td("settings|account|title"), @@ -128,7 +150,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { , "UserSettingsGeneral", ), - ); + );*/ tabs.push( new Tab( UserTab.SessionManager, @@ -249,7 +271,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; +}