2026-10-25 12:36:21 -04:00
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
2025-02-10 16:49:47 +11:00
|
|
|
import {
|
|
|
|
|
Avatar,
|
|
|
|
|
Box,
|
|
|
|
|
Button,
|
|
|
|
|
config,
|
|
|
|
|
Icon,
|
|
|
|
|
IconButton,
|
|
|
|
|
Icons,
|
|
|
|
|
IconSrc,
|
|
|
|
|
MenuItem,
|
|
|
|
|
Overlay,
|
|
|
|
|
OverlayBackdrop,
|
|
|
|
|
OverlayCenter,
|
|
|
|
|
Text,
|
2026-10-25 12:36:21 -04:00
|
|
|
color,
|
2025-02-10 16:49:47 +11:00
|
|
|
} from 'folds';
|
|
|
|
|
import FocusTrap from 'focus-trap-react';
|
2026-10-25 02:07:23 -04:00
|
|
|
import {
|
|
|
|
|
AdvancedDisplayOptions,
|
|
|
|
|
AppearancePage,
|
|
|
|
|
CallsPage,
|
|
|
|
|
DataStoragePage,
|
|
|
|
|
DesktopPage,
|
|
|
|
|
MessagesPage,
|
|
|
|
|
Privacy,
|
|
|
|
|
} from './general';
|
|
|
|
|
import { isTauri as isTauriEnv } from '../../hooks/useTauri';
|
2025-02-10 16:49:47 +11:00
|
|
|
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';
|
2026-10-19 00:00:19 -04:00
|
|
|
import { SettingsPages } from './pages';
|
2026-10-25 12:36:21 -04:00
|
|
|
import { SettingsSearch } from './search/SettingsSearch';
|
|
|
|
|
import type { SettingsEntry } from './search/settingsIndex';
|
2025-02-10 16:49:47 +11:00
|
|
|
|
2026-10-19 00:00:19 -04:00
|
|
|
export { SettingsPages };
|
2025-02-10 16:49:47 +11:00
|
|
|
|
|
|
|
|
type SettingsMenuItem = {
|
|
|
|
|
page: SettingsPages;
|
|
|
|
|
name: string;
|
|
|
|
|
icon: IconSrc;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const useSettingsMenuItems = (): SettingsMenuItem[] =>
|
|
|
|
|
useMemo(
|
2026-10-25 02:07:23 -04:00
|
|
|
() =>
|
|
|
|
|
[
|
|
|
|
|
{ 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[],
|
2026-05-21 23:30:50 -04:00
|
|
|
[],
|
2025-02-10 16:49:47 +11:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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
|
2026-05-21 23:30:50 -04:00
|
|
|
? (mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined)
|
2025-02-10 16:49:47 +11:00
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const screenSize = useScreenSizeContext();
|
|
|
|
|
const [activePage, setActivePage] = useState<SettingsPages | undefined>(() => {
|
|
|
|
|
if (initialPage) return initialPage;
|
2026-10-25 02:07:23 -04:00
|
|
|
return screenSize === ScreenSize.Mobile ? undefined : SettingsPages.AccountPage;
|
2025-02-10 16:49:47 +11:00
|
|
|
});
|
|
|
|
|
const menuItems = useSettingsMenuItems();
|
2026-10-25 12:36:21 -04:00
|
|
|
// [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<string>();
|
|
|
|
|
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<HTMLElement>('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]);
|
2025-02-10 16:49:47 +11:00
|
|
|
|
|
|
|
|
const handlePageRequestClose = () => {
|
|
|
|
|
if (screenSize === ScreenSize.Mobile) {
|
|
|
|
|
setActivePage(undefined);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
requestClose();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<PageRoot
|
|
|
|
|
nav={
|
|
|
|
|
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
|
|
|
|
|
<PageNav size="300">
|
|
|
|
|
<PageNavHeader outlined={false}>
|
|
|
|
|
<Box grow="Yes" gap="200">
|
|
|
|
|
<Avatar size="200" radii="300">
|
|
|
|
|
<UserAvatar
|
|
|
|
|
userId={userId}
|
|
|
|
|
src={avatarUrl}
|
|
|
|
|
renderFallback={() => <Text size="H6">{nameInitials(displayName)}</Text>}
|
|
|
|
|
/>
|
|
|
|
|
</Avatar>
|
|
|
|
|
<Text size="H4" truncate>
|
|
|
|
|
Settings
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box shrink="No">
|
|
|
|
|
{screenSize === ScreenSize.Mobile && (
|
2026-05-21 20:49:33 -04:00
|
|
|
<IconButton
|
|
|
|
|
onClick={requestClose}
|
|
|
|
|
variant="Background"
|
|
|
|
|
aria-label="Close settings"
|
|
|
|
|
>
|
2025-02-10 16:49:47 +11:00
|
|
|
<Icon src={Icons.Cross} />
|
|
|
|
|
</IconButton>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
</PageNavHeader>
|
|
|
|
|
<Box grow="Yes" direction="Column">
|
2026-10-25 12:36:21 -04:00
|
|
|
<SettingsSearch
|
|
|
|
|
pageNames={pageNames}
|
|
|
|
|
onActiveChange={setSearching}
|
|
|
|
|
onPick={pickResult}
|
|
|
|
|
/>
|
2025-02-10 16:49:47 +11:00
|
|
|
<PageNavContent>
|
2026-10-25 12:36:21 -04:00
|
|
|
<nav aria-label="Settings sections" style={{ flexGrow: 1 }} hidden={searching}>
|
2025-02-10 16:49:47 +11:00
|
|
|
{menuItems.map((item) => (
|
|
|
|
|
<MenuItem
|
|
|
|
|
key={item.name}
|
|
|
|
|
variant="Background"
|
|
|
|
|
radii="400"
|
|
|
|
|
aria-pressed={activePage === item.page}
|
|
|
|
|
before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
|
|
|
|
|
onClick={() => setActivePage(item.page)}
|
|
|
|
|
>
|
|
|
|
|
<Text
|
|
|
|
|
style={{
|
|
|
|
|
fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
|
|
|
|
|
}}
|
|
|
|
|
size="T300"
|
|
|
|
|
truncate
|
|
|
|
|
>
|
|
|
|
|
{item.name}
|
|
|
|
|
</Text>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
))}
|
2026-09-20 13:25:57 -04:00
|
|
|
</nav>
|
2025-02-10 16:49:47 +11:00
|
|
|
</PageNavContent>
|
|
|
|
|
<Box style={{ padding: config.space.S200 }} shrink="No" direction="Column">
|
|
|
|
|
<UseStateProvider initial={false}>
|
|
|
|
|
{(logout, setLogout) => (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
size="300"
|
|
|
|
|
variant="Critical"
|
|
|
|
|
fill="None"
|
|
|
|
|
radii="Pill"
|
|
|
|
|
before={<Icon src={Icons.Power} size="100" />}
|
|
|
|
|
onClick={() => setLogout(true)}
|
|
|
|
|
>
|
|
|
|
|
<Text size="B400">Logout</Text>
|
|
|
|
|
</Button>
|
|
|
|
|
{logout && (
|
|
|
|
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
|
|
|
|
<OverlayCenter>
|
|
|
|
|
<FocusTrap
|
|
|
|
|
focusTrapOptions={{
|
|
|
|
|
onDeactivate: () => setLogout(false),
|
|
|
|
|
clickOutsideDeactivates: true,
|
|
|
|
|
escapeDeactivates: stopPropagation,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<LogoutDialog handleClose={() => setLogout(false)} />
|
|
|
|
|
</FocusTrap>
|
|
|
|
|
</OverlayCenter>
|
|
|
|
|
</Overlay>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</UseStateProvider>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</PageNav>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-10-25 02:07:23 -04:00
|
|
|
{activePage === SettingsPages.AppearancePage && (
|
|
|
|
|
<AppearancePage requestClose={handlePageRequestClose} />
|
|
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.MessagesPage && (
|
|
|
|
|
<MessagesPage requestClose={handlePageRequestClose} />
|
|
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.CallsPage && (
|
|
|
|
|
<CallsPage requestClose={handlePageRequestClose} />
|
|
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.DataStoragePage && (
|
|
|
|
|
<DataStoragePage requestClose={handlePageRequestClose} />
|
|
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.DesktopPage && (
|
|
|
|
|
<DesktopPage requestClose={handlePageRequestClose} />
|
2025-02-10 16:49:47 +11:00
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.AccountPage && (
|
|
|
|
|
<Account requestClose={handlePageRequestClose} />
|
|
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.NotificationPage && (
|
|
|
|
|
<Notifications requestClose={handlePageRequestClose} />
|
|
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.DevicesPage && (
|
2026-10-25 02:07:23 -04:00
|
|
|
<Devices
|
|
|
|
|
title="Privacy & security"
|
|
|
|
|
before={<Privacy />}
|
|
|
|
|
requestClose={handlePageRequestClose}
|
|
|
|
|
/>
|
2025-02-10 16:49:47 +11:00
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.EmojisStickersPage && (
|
|
|
|
|
<EmojisStickers requestClose={handlePageRequestClose} />
|
|
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.DeveloperToolsPage && (
|
2026-10-25 02:07:23 -04:00
|
|
|
<DeveloperTools
|
|
|
|
|
title="Advanced"
|
|
|
|
|
before={<AdvancedDisplayOptions />}
|
|
|
|
|
requestClose={handlePageRequestClose}
|
|
|
|
|
/>
|
2025-02-10 16:49:47 +11:00
|
|
|
)}
|
|
|
|
|
{activePage === SettingsPages.AboutPage && <About requestClose={handlePageRequestClose} />}
|
|
|
|
|
</PageRoot>
|
|
|
|
|
);
|
|
|
|
|
}
|