import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Avatar, Box, Button, config, Icon, IconButton, Icons, IconSrc, MenuItem, Overlay, OverlayBackdrop, OverlayCenter, Text, color, } from 'folds'; import FocusTrap from 'focus-trap-react'; import { AdvancedDisplayOptions, AppearancePage, CallsPage, DataStoragePage, DesktopPage, MessagesPage, Privacy, } from './general'; import { isTauri as isTauriEnv } from '../../hooks/useTauri'; import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { Account } from './account'; import { useUserProfile } from '../../hooks/useUserProfile'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { UserAvatar } from '../../components/user-avatar'; import { nameInitials } from '../../utils/common'; import { Notifications } from './notifications'; import { Devices } from './devices'; import { EmojisStickers } from './emojis-stickers'; import { DeveloperTools } from './developer-tools'; import { About } from './about'; import { UseStateProvider } from '../../components/UseStateProvider'; import { stopPropagation } from '../../utils/keyboard'; import { LogoutDialog } from '../../components/LogoutDialog'; import { SettingsPages } from './pages'; import { SettingsSearch } from './search/SettingsSearch'; import type { SettingsEntry } from './search/settingsIndex'; export { SettingsPages }; type SettingsMenuItem = { page: SettingsPages; name: string; icon: IconSrc; }; const useSettingsMenuItems = (): SettingsMenuItem[] => useMemo( () => [ { page: SettingsPages.AccountPage, name: 'Account', icon: Icons.User }, { page: SettingsPages.AppearancePage, name: 'Appearance', icon: Icons.Sun }, { page: SettingsPages.MessagesPage, name: 'Messages', icon: Icons.Message }, { page: SettingsPages.NotificationPage, name: 'Notifications', icon: Icons.Bell }, { page: SettingsPages.CallsPage, name: 'Calls & audio', icon: Icons.Phone }, { page: SettingsPages.DevicesPage, name: 'Privacy & security', icon: Icons.ShieldUser }, { page: SettingsPages.DataStoragePage, name: 'Data & storage', icon: Icons.Download }, // [Gitea #459] Desktop-only options: nothing to show in a browser. ...(isTauriEnv() ? [{ page: SettingsPages.DesktopPage, name: 'Desktop', icon: Icons.Monitor }] : []), { page: SettingsPages.EmojisStickersPage, name: 'Emojis & stickers', icon: Icons.Smile }, { page: SettingsPages.DeveloperToolsPage, name: 'Advanced', icon: Icons.Terminal }, { page: SettingsPages.AboutPage, name: 'About', icon: Icons.Info }, ] as SettingsMenuItem[], [], ); type SettingsProps = { initialPage?: SettingsPages; requestClose: () => void; }; export function Settings({ initialPage, requestClose }: SettingsProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const userId = mx.getUserId()!; const profile = useUserProfile(userId); const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId; const avatarUrl = profile.avatarUrl ? (mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined) : undefined; const screenSize = useScreenSizeContext(); const [activePage, setActivePage] = useState(() => { if (initialPage) return initialPage; return screenSize === ScreenSize.Mobile ? undefined : SettingsPages.AccountPage; }); const menuItems = useSettingsMenuItems(); // [Gitea #463] Settings search: results replace the page list while typing; // picking one opens its page and briefly highlights the setting. const pageNames = useMemo( () => new Map(menuItems.map((item) => [item.page, item.name])), [menuItems], ); const [searching, setSearching] = useState(false); const [highlight, setHighlight] = useState(); const pickResult = useCallback((entry: SettingsEntry) => { setActivePage(entry.page); setHighlight(entry.title); }, []); useEffect(() => { if (!highlight) return undefined; let tries = 0; let flashTimer: number | undefined; const find = () => { const match = [...document.querySelectorAll('p, span, label, h1, h2, h3')].find( (el) => !el.closest('nav') && el.textContent?.trim() === highlight && el.offsetParent !== null, ); if (!match) { tries += 1; if (tries < 20) poll = window.setTimeout(find, 100); else setHighlight(undefined); return; } const target = (match.closest('[class*="SequenceCard"]') as HTMLElement | null) ?? match; target.scrollIntoView({ block: 'center', behavior: 'smooth' }); const prev = target.style.boxShadow; target.style.boxShadow = `0 0 0 2px ${color.Primary.Main}`; target.style.transition = 'box-shadow 0.3s'; flashTimer = window.setTimeout(() => { target.style.boxShadow = prev; setHighlight(undefined); }, 1800); }; let poll = window.setTimeout(find, 50); return () => { window.clearTimeout(poll); window.clearTimeout(flashTimer); }; }, [highlight, activePage]); const handlePageRequestClose = () => { if (screenSize === ScreenSize.Mobile) { setActivePage(undefined); return; } requestClose(); }; return ( {nameInitials(displayName)}} /> Settings {screenSize === ScreenSize.Mobile && ( )} {(logout, setLogout) => ( <> {logout && ( }> setLogout(false), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} > setLogout(false)} /> )} )} ) } > {activePage === SettingsPages.AppearancePage && ( )} {activePage === SettingsPages.MessagesPage && ( )} {activePage === SettingsPages.CallsPage && ( )} {activePage === SettingsPages.DataStoragePage && ( )} {activePage === SettingsPages.DesktopPage && ( )} {activePage === SettingsPages.AccountPage && ( )} {activePage === SettingsPages.NotificationPage && ( )} {activePage === SettingsPages.DevicesPage && ( } requestClose={handlePageRequestClose} /> )} {activePage === SettingsPages.EmojisStickersPage && ( )} {activePage === SettingsPages.DeveloperToolsPage && ( } requestClose={handlePageRequestClose} /> )} {activePage === SettingsPages.AboutPage && } ); }