feat(settings): redesign settings as a grouped Dawn list and drop the Developer Tools tab
This commit is contained in:
parent
331366cf40
commit
a3dbe0df78
32 changed files with 865 additions and 1410 deletions
|
|
@ -88,7 +88,6 @@
|
|||
"menu_notifications": "Notifications",
|
||||
"menu_devices": "Devices",
|
||||
"menu_emojis_stickers": "Emojis & Stickers",
|
||||
"menu_developer_tools": "Developer Tools",
|
||||
"menu_about": "About",
|
||||
"drag_to_close": "Drag down to close",
|
||||
"close": "Close",
|
||||
|
|
@ -105,7 +104,6 @@
|
|||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"theme": "Theme",
|
||||
"monochrome_mode": "Monochrome Mode",
|
||||
"twitter_emoji": "Twitter Emoji",
|
||||
"page_zoom": "Page Zoom",
|
||||
"save": "Save",
|
||||
|
|
@ -116,12 +114,13 @@
|
|||
"hide_activity": "Hide Typing & Read Receipts",
|
||||
"hide_activity_desc": "Turn off both typing status and read receipts to keep your activity private.",
|
||||
"messages": "Messages",
|
||||
"hide_membership": "Hide Membership Change",
|
||||
"hide_profile": "Hide Profile Change",
|
||||
"hide_service_events": "Hide Service Events",
|
||||
"hide_service_events_desc": "Hide joins, leaves, name and avatar changes from the timeline.",
|
||||
"disable_media_auto_load": "Disable Media Auto Load",
|
||||
"url_preview": "Url Preview",
|
||||
"url_preview_encrypted": "Url Preview in Encrypted Room",
|
||||
"show_hidden_events": "Show Hidden Events",
|
||||
"advanced": "Advanced",
|
||||
"developer_mode": "Developer Mode",
|
||||
"developer_mode_desc": "Unlock technical tools, like viewing the source of messages.",
|
||||
"account_title": "Account",
|
||||
"profile": "Profile",
|
||||
"avatar": "Avatar",
|
||||
|
|
@ -139,7 +138,6 @@
|
|||
"select_user": "Select User",
|
||||
"select_user_desc": "Prevent receiving messages or invites from user by adding their userId.",
|
||||
"block": "Block",
|
||||
"users": "Users",
|
||||
"notifications_title": "Notifications",
|
||||
"block_messages": "Block Messages",
|
||||
"block_messages_moved": "This option has been moved to \"Account > Block Users\" section.",
|
||||
|
|
@ -166,7 +164,6 @@
|
|||
"email_send_notif_to": "Send notification to your email. (\"{{email}}\")",
|
||||
"unexpected_error": "Unexpected Error!",
|
||||
"all_messages": "All Messages",
|
||||
"badge": "Badge: ",
|
||||
"one_to_one": "1-to-1 Chats",
|
||||
"one_to_one_encrypted": "1-to-1 Chats (Encrypted)",
|
||||
"rooms": "Rooms",
|
||||
|
|
@ -256,26 +253,15 @@
|
|||
"apply_ready": "Changes saved! Apply when ready.",
|
||||
"apply_changes": "Apply Changes",
|
||||
"about_title": "About",
|
||||
"about_tagline": "Yet another matrix client.",
|
||||
"options": "Options",
|
||||
"about_tagline": "A messenger for everyone.",
|
||||
"about_connected": "Connected to",
|
||||
"clear_cache_title": "Clear Cache & Reload",
|
||||
"clear_cache_desc": "Clear all your locally stored data and reload from server.",
|
||||
"clear_cache": "Clear Cache",
|
||||
"legal": "Legal",
|
||||
"privacy_policy_title": "Privacy Policy",
|
||||
"privacy_policy_desc": "How your data is handled.",
|
||||
"privacy_policy_open": "Open",
|
||||
"credits": "Credits",
|
||||
"devtools_title": "Developer Tools",
|
||||
"enable_devtools": "Enable Developer Tools",
|
||||
"access_token": "Access Token",
|
||||
"access_token_desc": "Copy access token to clipboard.",
|
||||
"account_data": "Account Data",
|
||||
"account_data_global": "Global",
|
||||
"account_data_desc": "Data stored in your global account data.",
|
||||
"events": "Events",
|
||||
"total": "Total: {{count}}",
|
||||
"add_new": "Add New"
|
||||
"about_credits": "Vojo is built on open-source software — including matrix-js-sdk (Apache 2.0), Twemoji (CC-BY 4.0) and Material Design sounds (CC-BY 4.0)."
|
||||
},
|
||||
"Search": {
|
||||
"search": "Search",
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@
|
|||
"menu_notifications": "Уведомления",
|
||||
"menu_devices": "Устройства",
|
||||
"menu_emojis_stickers": "Эмодзи и стикеры",
|
||||
"menu_developer_tools": "Инструменты разработчика",
|
||||
"menu_about": "О приложении",
|
||||
"drag_to_close": "Потянуть вниз чтобы закрыть",
|
||||
"close": "Закрыть",
|
||||
|
|
@ -105,7 +104,6 @@
|
|||
"theme_light": "Светлая",
|
||||
"theme_dark": "Тёмная",
|
||||
"theme": "Тема",
|
||||
"monochrome_mode": "Монохромный режим",
|
||||
"twitter_emoji": "Эмодзи Twitter",
|
||||
"page_zoom": "Масштаб страницы",
|
||||
"save": "Сохранить",
|
||||
|
|
@ -116,12 +114,13 @@
|
|||
"hide_activity": "Скрыть набор текста и уведомления о прочтении",
|
||||
"hide_activity_desc": "Отключить статус набора и отчёты о прочтении для сохранения приватности.",
|
||||
"messages": "Сообщения",
|
||||
"hide_membership": "Скрыть изменения участников",
|
||||
"hide_profile": "Скрыть изменения профиля",
|
||||
"hide_service_events": "Скрывать служебные сообщения",
|
||||
"hide_service_events_desc": "Скрывать вступления, выходы и смену имени или аватара в ленте.",
|
||||
"disable_media_auto_load": "Отключить автозагрузку медиа",
|
||||
"url_preview": "Предпросмотр ссылок",
|
||||
"url_preview_encrypted": "Предпросмотр ссылок в зашифрованных комнатах",
|
||||
"show_hidden_events": "Показывать скрытые события",
|
||||
"advanced": "Дополнительно",
|
||||
"developer_mode": "Режим разработчика",
|
||||
"developer_mode_desc": "Открывает технические функции, например исходный код сообщений.",
|
||||
"account_title": "Аккаунт",
|
||||
"profile": "Профиль",
|
||||
"avatar": "Аватар",
|
||||
|
|
@ -139,7 +138,6 @@
|
|||
"select_user": "Выбрать пользователя",
|
||||
"select_user_desc": "Заблокируйте получение сообщений и приглашений от пользователя, добавив его идентификатор.",
|
||||
"block": "Заблокировать",
|
||||
"users": "Пользователи",
|
||||
"notifications_title": "Уведомления",
|
||||
"block_messages": "Блокировка сообщений",
|
||||
"block_messages_moved": "Эта опция перенесена в раздел «Аккаунт > Заблокированные пользователи».",
|
||||
|
|
@ -166,7 +164,6 @@
|
|||
"email_send_notif_to": "Отправлять уведомления на вашу почту. (\"{{email}}\")",
|
||||
"unexpected_error": "Непредвиденная ошибка!",
|
||||
"all_messages": "Все сообщения",
|
||||
"badge": "Значок: ",
|
||||
"one_to_one": "Личные чаты",
|
||||
"one_to_one_encrypted": "Личные чаты (зашифрованные)",
|
||||
"rooms": "Комнаты",
|
||||
|
|
@ -256,26 +253,15 @@
|
|||
"apply_ready": "Изменения сохранены! Примените, когда будете готовы.",
|
||||
"apply_changes": "Применить изменения",
|
||||
"about_title": "О приложении",
|
||||
"about_tagline": "Ещё один клиент для Matrix.",
|
||||
"options": "Параметры",
|
||||
"about_tagline": "Вседоступный мессенджер.",
|
||||
"about_connected": "Подключено к",
|
||||
"clear_cache_title": "Очистить кэш и перезагрузить",
|
||||
"clear_cache_desc": "Удалить все локально сохранённые данные и загрузить заново с сервера.",
|
||||
"clear_cache": "Очистить кэш",
|
||||
"legal": "Юридическое",
|
||||
"privacy_policy_title": "Политика конфиденциальности",
|
||||
"privacy_policy_desc": "Как обрабатываются ваши данные.",
|
||||
"privacy_policy_open": "Открыть",
|
||||
"credits": "Благодарности",
|
||||
"devtools_title": "Инструменты разработчика",
|
||||
"enable_devtools": "Включить инструменты разработчика",
|
||||
"access_token": "Токен доступа",
|
||||
"access_token_desc": "Скопировать токен доступа в буфер обмена.",
|
||||
"account_data": "Данные аккаунта",
|
||||
"account_data_global": "Глобальные",
|
||||
"account_data_desc": "Данные, хранящиеся в глобальных данных вашего аккаунта.",
|
||||
"events": "События",
|
||||
"total": "Всего: {{count}}",
|
||||
"add_new": "Добавить"
|
||||
"about_credits": "Vojo создан на открытом ПО — включая matrix-js-sdk (Apache 2.0), Twemoji (CC-BY 4.0) и звуки Material Design (CC-BY 4.0)."
|
||||
},
|
||||
"Search": {
|
||||
"search": "Поиск",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { Account } from './account';
|
|||
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';
|
||||
|
|
@ -35,7 +34,6 @@ export enum SettingsPages {
|
|||
NotificationPage,
|
||||
DevicesPage,
|
||||
EmojisStickersPage,
|
||||
DeveloperToolsPage,
|
||||
AboutPage,
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +54,6 @@ export const SETTINGS_PAGE_PARAM = {
|
|||
notifications: SettingsPages.NotificationPage,
|
||||
devices: SettingsPages.DevicesPage,
|
||||
emojis: SettingsPages.EmojisStickersPage,
|
||||
'developer-tools': SettingsPages.DeveloperToolsPage,
|
||||
about: SettingsPages.AboutPage,
|
||||
} as const satisfies Record<string, SettingsPages>;
|
||||
export const SETTINGS_PARAM_DEVICES = 'devices';
|
||||
|
|
@ -87,11 +84,6 @@ const SETTINGS_MENU_ITEMS: SettingsMenuItem[] = [
|
|||
nameKey: 'Settings.menu_emojis_stickers',
|
||||
icon: Icons.Smile,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.DeveloperToolsPage,
|
||||
nameKey: 'Settings.menu_developer_tools',
|
||||
icon: Icons.Terminal,
|
||||
},
|
||||
{ page: SettingsPages.AboutPage, nameKey: 'Settings.menu_about', icon: Icons.Info },
|
||||
];
|
||||
|
||||
|
|
@ -148,9 +140,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||
const target = e.target as HTMLElement | null;
|
||||
if (
|
||||
target &&
|
||||
(target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable)
|
||||
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -301,9 +291,6 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||
{activePage === SettingsPages.EmojisStickersPage && (
|
||||
<EmojisStickers requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SettingsPages.DeveloperToolsPage && (
|
||||
<DeveloperTools requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SettingsPages.AboutPage && <About requestClose={handlePageRequestClose} />}
|
||||
</PageRoot>
|
||||
);
|
||||
|
|
|
|||
51
src/app/features/settings/SettingsPage.tsx
Normal file
51
src/app/features/settings/SettingsPage.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../components/page';
|
||||
|
||||
type SettingsPageProps = {
|
||||
title: ReactNode;
|
||||
requestClose: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared chrome for every settings sub-page — Dawn `SurfaceVariant` surface, a
|
||||
* title header with the close button, and a hover-scrolled content column.
|
||||
* Sections inside are spaced with a single rhythm (gap 500) so grouped panels
|
||||
* read as a consistent vertical stack.
|
||||
*/
|
||||
export function SettingsPage({ title, requestClose, children }: SettingsPageProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200" alignItems="Center">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No" alignItems="Center" gap="200">
|
||||
<IconButton
|
||||
onClick={requestClose}
|
||||
variant="SurfaceVariant"
|
||||
aria-label={t('Settings.close')}
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="500">
|
||||
{children}
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
@ -102,9 +102,7 @@ export function SettingsScreen() {
|
|||
const target = e.target as HTMLElement | null;
|
||||
if (
|
||||
target &&
|
||||
(target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable)
|
||||
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
68
src/app/features/settings/SettingsSection.tsx
Normal file
68
src/app/features/settings/SettingsSection.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import React, { Children, ReactNode } from 'react';
|
||||
import { Box, Text } from 'folds';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { ContainerColorVariants } from '../../styles/ContainerColor.css';
|
||||
import { SectionFootnote, SectionLabel, SettingRow } from './styles.css';
|
||||
|
||||
type SettingsSectionProps = {
|
||||
// Uppercase muted heading above the panel. Omit for an unlabelled panel.
|
||||
label?: ReactNode;
|
||||
// Small caption under the label (rare — e.g. a privacy note).
|
||||
footnote?: ReactNode;
|
||||
// Surface tone for the rows. Default `Background` = the inset darker panel
|
||||
// (#0d0e11) on the SurfaceVariant page (#181a20).
|
||||
variant?: NonNullable<ContainerColorVariants>['variant'];
|
||||
// Each direct child becomes one row of the grouped panel. Falsy children
|
||||
// (conditional rows) are dropped so dividers/rounding stay correct.
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Dawn grouped settings panel: an uppercase muted label over a single
|
||||
* outlined card whose rows are separated by hairline dividers (via
|
||||
* `SequenceCard`'s `mergeBorder` + first/last auto-rounding). Replaces the
|
||||
* stock-Cinny "one floating card per setting" pattern.
|
||||
*/
|
||||
export function SettingsSection({
|
||||
label,
|
||||
footnote,
|
||||
variant = 'Background',
|
||||
children,
|
||||
}: SettingsSectionProps) {
|
||||
const rows = Children.toArray(children).filter(Boolean);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
{label && (
|
||||
<Box direction="Column">
|
||||
<Text as="span" className={SectionLabel}>
|
||||
{label}
|
||||
</Text>
|
||||
{footnote && (
|
||||
<Text as="span" className={SectionFootnote}>
|
||||
{footnote}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<Box direction="Column">
|
||||
{rows.map((row, index) => (
|
||||
<SequenceCard
|
||||
// Rows are positional and have no stable id; index key is correct
|
||||
// here — the list never reorders, only conditionally includes rows.
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
variant={variant}
|
||||
outlined
|
||||
mergeBorder
|
||||
direction="Column"
|
||||
className={SettingRow}
|
||||
>
|
||||
{row}
|
||||
</SequenceCard>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll, Button, config, toRem } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { Box, Text, Button, Chip, color, config, toRem } from 'folds';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import VojoSVG from '../../../../../public/res/svg/vojo.svg';
|
||||
import { clearCacheAndReload } from '../../../../client/initMatrix';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { SettingsPage } from '../SettingsPage';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { Mono } from '../styles.css';
|
||||
|
||||
type AboutProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -15,232 +16,122 @@ type AboutProps = {
|
|||
export function About({ requestClose }: AboutProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const domain = mx.getDomain() ?? 'vojo.chat';
|
||||
const version = __APP_VERSION__;
|
||||
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{t('Settings.about_title')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<SettingsPage title={t('Settings.about_title')} requestClose={requestClose}>
|
||||
{/* Brand hero — centered logo tile, wordmark, tagline, and a copyable
|
||||
Fleet-tinted version chip. Deliberately not the stock logo-left row. */}
|
||||
<Box direction="Column" alignItems="Center" gap="300" style={{ paddingTop: toRem(8) }}>
|
||||
<Box
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
shrink="No"
|
||||
style={{
|
||||
width: toRem(88),
|
||||
height: toRem(88),
|
||||
borderRadius: toRem(22),
|
||||
backgroundColor: color.Background.Container,
|
||||
border: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
|
||||
}}
|
||||
>
|
||||
<img src={VojoSVG} alt="Vojo" style={{ width: toRem(62), height: toRem(62) }} />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="700">
|
||||
<Box gap="400">
|
||||
<Box shrink="No">
|
||||
<img
|
||||
style={{ width: toRem(60), height: toRem(60) }}
|
||||
src={VojoSVG}
|
||||
alt="Vojo logo"
|
||||
/>
|
||||
</Box>
|
||||
<Box direction="Column" gap="300">
|
||||
<Box direction="Column" gap="100">
|
||||
<Box gap="100" alignItems="End">
|
||||
<Text size="H3">Vojo</Text>
|
||||
<Text size="T200">{__APP_VERSION__}</Text>
|
||||
</Box>
|
||||
<Text>{t('Settings.about_tagline')}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.options')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.clear_cache_title')}
|
||||
description={t('Settings.clear_cache_desc')}
|
||||
after={
|
||||
<Button
|
||||
onClick={() => clearCacheAndReload(mx)}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
>
|
||||
<Text size="B300">{t('Settings.clear_cache')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.legal')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.privacy_policy_title')}
|
||||
description={t('Settings.privacy_policy_desc')}
|
||||
after={
|
||||
<Button
|
||||
as="a"
|
||||
href="https://vojo.chat/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
>
|
||||
<Text size="B300">{t('Settings.privacy_policy_open')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.credits')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<Box
|
||||
as="ul"
|
||||
direction="Column"
|
||||
gap="200"
|
||||
style={{
|
||||
margin: 0,
|
||||
paddingLeft: config.space.S400,
|
||||
}}
|
||||
>
|
||||
<li>
|
||||
<Text size="T300">
|
||||
The{' '}
|
||||
<a
|
||||
href="https://github.com/matrix-org/matrix-js-sdk"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
matrix-js-sdk
|
||||
</a>{' '}
|
||||
is ©{' '}
|
||||
<a
|
||||
href="https://matrix.org/foundation"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
The Matrix.org Foundation C.I.C
|
||||
</a>{' '}
|
||||
used under the terms of{' '}
|
||||
<a
|
||||
href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Apache 2.0
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text size="T300">
|
||||
The{' '}
|
||||
<a
|
||||
href="https://github.com/mozilla/twemoji-colr"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
twemoji-colr
|
||||
</a>{' '}
|
||||
font is ©{' '}
|
||||
<a href="https://mozilla.org/" target="_blank" rel="noreferrer noopener">
|
||||
Mozilla Foundation
|
||||
</a>{' '}
|
||||
used under the terms of{' '}
|
||||
<a
|
||||
href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Apache 2.0
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text size="T300">
|
||||
The{' '}
|
||||
<a
|
||||
href="https://twemoji.twitter.com"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Twemoji
|
||||
</a>{' '}
|
||||
emoji art is ©{' '}
|
||||
<a
|
||||
href="https://twemoji.twitter.com"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Twitter, Inc and other contributors
|
||||
</a>{' '}
|
||||
used under the terms of{' '}
|
||||
<a
|
||||
href="https://creativecommons.org/licenses/by/4.0/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
CC-BY 4.0
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text size="T300">
|
||||
The{' '}
|
||||
<a
|
||||
href="https://material.io/design/sound/sound-resources.html"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Material sound resources
|
||||
</a>{' '}
|
||||
are ©{' '}
|
||||
<a href="https://google.com" target="_blank" rel="noreferrer noopener">
|
||||
Google
|
||||
</a>{' '}
|
||||
used under the terms of{' '}
|
||||
<a
|
||||
href="https://creativecommons.org/licenses/by/4.0/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
CC-BY 4.0
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</li>
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
<Box direction="Column" alignItems="Center" gap="100">
|
||||
<Text size="H2">Vojo</Text>
|
||||
<Text size="T300" priority="400" align="Center">
|
||||
{t('Settings.about_tagline')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Chip
|
||||
variant="Primary"
|
||||
fill="Soft"
|
||||
radii="Pill"
|
||||
onClick={() => copyToClipboard(version)}
|
||||
aria-label={t('Settings.copy')}
|
||||
>
|
||||
<Text as="span" size="T200" className={Mono}>
|
||||
{version}
|
||||
</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
</Page>
|
||||
|
||||
{/* Connection strip — the Dawn canon's mono status bar. A green "online"
|
||||
dot plus the real homeserver this session is connected to. */}
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{
|
||||
padding: `${config.space.S300} ${config.space.S400}`,
|
||||
backgroundColor: color.Background.Container,
|
||||
border: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
|
||||
borderRadius: config.radii.R400,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: toRem(7),
|
||||
height: toRem(7),
|
||||
borderRadius: '50%',
|
||||
backgroundColor: color.Success.Main,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Text as="span" size="T200" priority="400">
|
||||
{t('Settings.about_connected')}
|
||||
</Text>
|
||||
<Text as="span" size="T200" className={Mono}>
|
||||
{domain}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Actions — privacy policy + cache reset, in the shared grouped panel. */}
|
||||
<SettingsSection>
|
||||
<SettingTile
|
||||
title={t('Settings.privacy_policy_title')}
|
||||
description={t('Settings.privacy_policy_desc')}
|
||||
after={
|
||||
<Button
|
||||
as="a"
|
||||
href="https://vojo.chat/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
>
|
||||
<Text size="B300">{t('Settings.privacy_policy_open')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.clear_cache_title')}
|
||||
description={t('Settings.clear_cache_desc')}
|
||||
after={
|
||||
<Button
|
||||
onClick={() => clearCacheAndReload(mx)}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
>
|
||||
<Text size="B300">{t('Settings.clear_cache')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
{/* Compact open-source attribution — Twemoji art & Material sounds are
|
||||
CC-BY 4.0 and matrix-js-sdk is Apache 2.0, all of which require
|
||||
attribution, so one quiet line stays in place of the old credits. */}
|
||||
<Text size="T200" priority="400" align="Center" style={{ opacity: 0.55 }}>
|
||||
{t('Settings.about_credits')}
|
||||
</Text>
|
||||
</SettingsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { MatrixId } from './MatrixId';
|
||||
import { Profile } from './Profile';
|
||||
import { ContactInformation } from './ContactInfo';
|
||||
import { IgnoredUserList } from './IgnoredUserList';
|
||||
import { SettingsPage } from '../SettingsPage';
|
||||
|
||||
type AccountProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -13,33 +12,11 @@ type AccountProps = {
|
|||
export function Account({ requestClose }: AccountProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{t('Settings.account_title')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="700">
|
||||
<Profile />
|
||||
<MatrixId />
|
||||
<ContactInformation />
|
||||
<IgnoredUserList />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
<SettingsPage title={t('Settings.account_title')} requestClose={requestClose}>
|
||||
<Profile />
|
||||
<MatrixId />
|
||||
<ContactInformation />
|
||||
<IgnoredUserList />
|
||||
</SettingsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Box, Text, Chip } from 'folds';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { Mono } from '../styles.css';
|
||||
|
||||
export function ContactInformation() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -23,28 +23,21 @@ export function ContactInformation() {
|
|||
}, [loadThreePIds]);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.contact_info')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
<SettingsSection label={t('Settings.contact_info')}>
|
||||
<SettingTile
|
||||
title={t('Settings.email_address')}
|
||||
description={t('Settings.email_address_desc')}
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.email_address')}
|
||||
description={t('Settings.email_address_desc')}
|
||||
>
|
||||
<Box>
|
||||
{emailIds?.map((email) => (
|
||||
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
|
||||
<Text size="T200">{email.address}</Text>
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
{/* <Input defaultValue="" variant="Secondary" radii="300" /> */}
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<Box wrap="Wrap" gap="200">
|
||||
{emailIds?.map((email) => (
|
||||
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
|
||||
<Text size="T200" className={Mono}>
|
||||
{email.address}
|
||||
</Text>
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
</SettingTile>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { ChangeEventHandler, FormEventHandler, useCallback, useState } from 'react';
|
||||
import { Box, Button, Chip, Icon, IconButton, Icons, Input, Spinner, Text, config } from 'folds';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { isUserId } from '../../../utils/matrix';
|
||||
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { Mono } from '../styles.css';
|
||||
|
||||
function IgnoreUserInput({ userList }: { userList: string[] }) {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -123,7 +123,7 @@ function IgnoredUserChip({ userId, userList }: { userId: string; userList: strin
|
|||
onClick={handleUnignore}
|
||||
disabled={unIgnoring}
|
||||
>
|
||||
<Text size="T200" truncate>
|
||||
<Text size="T200" truncate className={Mono}>
|
||||
{userId}
|
||||
</Text>
|
||||
</Chip>
|
||||
|
|
@ -135,32 +135,19 @@ export function IgnoredUserList() {
|
|||
const ignoredUsers = useIgnoredUsers();
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||
<Text size="L400">{t('Settings.blocked_users')}</Text>
|
||||
</Box>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile title={t('Settings.select_user')} description={t('Settings.select_user_desc')}>
|
||||
<Box direction="Column" gap="300">
|
||||
<IgnoreUserInput userList={ignoredUsers} />
|
||||
{ignoredUsers.length > 0 && (
|
||||
<Box direction="Inherit" gap="100">
|
||||
<Text size="L400">{t('Settings.users')}</Text>
|
||||
<Box wrap="Wrap" gap="200">
|
||||
{ignoredUsers.map((userId) => (
|
||||
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.blocked_users')}>
|
||||
<SettingTile title={t('Settings.select_user')} description={t('Settings.select_user_desc')}>
|
||||
<Box direction="Column" gap="300">
|
||||
<IgnoreUserInput userList={ignoredUsers} />
|
||||
{ignoredUsers.length > 0 && (
|
||||
<Box wrap="Wrap" gap="200">
|
||||
{ignoredUsers.map((userId) => (
|
||||
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</SettingTile>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
import React from 'react';
|
||||
import { Box, Text, Chip } from 'folds';
|
||||
import { Text, Chip } from 'folds';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { Mono } from '../styles.css';
|
||||
|
||||
export function MatrixId() {
|
||||
const { t } = useTranslation();
|
||||
const userId = useAuthedUserId();
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.matrix_id')}</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title={userId}
|
||||
after={
|
||||
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
|
||||
<Text size="T200">{t('Settings.copy')}</Text>
|
||||
</Chip>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.matrix_id')}>
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="T300" className={Mono}>
|
||||
{userId}
|
||||
</Text>
|
||||
}
|
||||
after={
|
||||
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
|
||||
<Text size="T200">{t('Settings.copy')}</Text>
|
||||
</Chip>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,8 @@ import {
|
|||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
||||
|
|
@ -92,11 +91,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
|
|||
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
{t('Settings.avatar')}
|
||||
</Text>
|
||||
}
|
||||
title={t('Settings.avatar')}
|
||||
after={
|
||||
<Avatar size="500" radii="300">
|
||||
<UserAvatar
|
||||
|
|
@ -249,13 +244,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
|||
|
||||
const hasChanges = displayName !== defaultDisplayName;
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
{t('Settings.display_name')}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<SettingTile title={t('Settings.display_name')}>
|
||||
<Box direction="Column" grow="Yes" gap="100">
|
||||
<Box
|
||||
as="form"
|
||||
|
|
@ -313,12 +302,9 @@ export function Profile() {
|
|||
const profile = useUserProfile(userId);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.profile')}</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<ProfileAvatar userId={userId} profile={profile} />
|
||||
<ProfileDisplayName userId={userId} profile={profile} />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.profile')}>
|
||||
<ProfileAvatar userId={userId} profile={profile} />
|
||||
<ProfileDisplayName userId={userId} profile={profile} />
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, Icon, Icons, Button, MenuItem } from 'folds';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useAccountDataCallback } from '../../../hooks/useAccountDataCallback';
|
||||
import { CutoutCard } from '../../../components/cutout-card';
|
||||
|
||||
type AccountDataProps = {
|
||||
expand: boolean;
|
||||
onExpandToggle: (expand: boolean) => void;
|
||||
onSelect: (type: string | null) => void;
|
||||
};
|
||||
export function AccountData({ expand, onExpandToggle, onSelect }: AccountDataProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const [accountDataTypes, setAccountDataKeys] = useState(() =>
|
||||
Array.from(mx.store.accountData.keys())
|
||||
);
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(() => {
|
||||
setAccountDataKeys(Array.from(mx.store.accountData.keys()));
|
||||
}, [mx])
|
||||
);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.account_data')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.account_data_global')}
|
||||
description={t('Settings.account_data_desc')}
|
||||
after={
|
||||
<Button
|
||||
onClick={() => onExpandToggle(!expand)}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
before={
|
||||
<Icon src={expand ? Icons.ChevronTop : Icons.ChevronBottom} size="100" filled />
|
||||
}
|
||||
>
|
||||
<Text size="B300">{expand ? t('Settings.collapse') : t('Settings.expand')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{expand && (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box justifyContent="SpaceBetween">
|
||||
<Text size="L400">{t('Settings.events')}</Text>
|
||||
<Text size="L400">{t('Settings.total', { count: accountDataTypes.length })}</Text>
|
||||
</Box>
|
||||
<CutoutCard>
|
||||
<MenuItem
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="0"
|
||||
before={<Icon size="50" src={Icons.Plus} />}
|
||||
onClick={() => onSelect(null)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" truncate>
|
||||
{t('Settings.add_new')}
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
{accountDataTypes.sort().map((type) => (
|
||||
<MenuItem
|
||||
key={type}
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="0"
|
||||
after={<Icon size="50" src={Icons.ChevronRight} />}
|
||||
onClick={() => onSelect(type)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" truncate>
|
||||
{type}
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</CutoutCard>
|
||||
</Box>
|
||||
)}
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import type { AccountDataEvents } from 'matrix-js-sdk';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import {
|
||||
AccountDataEditor,
|
||||
AccountDataSubmitCallback,
|
||||
} from '../../../components/AccountDataEditor';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { AccountData } from './AccountData';
|
||||
|
||||
type DeveloperToolsProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const [expand, setExpend] = useState(false);
|
||||
const [accountDataType, setAccountDataType] = useState<string | null>();
|
||||
|
||||
const submitAccountData: AccountDataSubmitCallback = useCallback(
|
||||
async (type, content) => {
|
||||
// Dev tool accepts arbitrary keys; SDK signature wants `keyof AccountDataEvents`.
|
||||
await mx.setAccountData(type as keyof AccountDataEvents, content);
|
||||
},
|
||||
[mx]
|
||||
);
|
||||
|
||||
if (accountDataType !== undefined) {
|
||||
return (
|
||||
<AccountDataEditor
|
||||
type={accountDataType ?? undefined}
|
||||
content={
|
||||
accountDataType
|
||||
? mx.getAccountData(accountDataType as keyof AccountDataEvents)?.getContent()
|
||||
: undefined
|
||||
}
|
||||
submitChange={submitAccountData}
|
||||
requestClose={() => setAccountDataType(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{t('Settings.devtools_title')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton
|
||||
onClick={requestClose}
|
||||
variant="SurfaceVariant"
|
||||
aria-label={t('Settings.close')}
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="700">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.options')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.enable_devtools')}
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={developerTools}
|
||||
onChange={setDeveloperTools}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
{developerTools && (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.access_token')}
|
||||
description={t('Settings.access_token_desc')}
|
||||
after={
|
||||
<Button
|
||||
onClick={() =>
|
||||
copyToClipboard(mx.getAccessToken() ?? '<NO_ACCESS_TOKEN_FOUND>')
|
||||
}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
>
|
||||
<Text size="B300">{t('Settings.copy')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
</Box>
|
||||
{developerTools && (
|
||||
<AccountData
|
||||
expand={expand}
|
||||
onExpandToggle={setExpend}
|
||||
onSelect={setAccountDataType}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './DevelopTools';
|
||||
|
|
@ -25,16 +25,17 @@ import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../../utils
|
|||
import { BreakWord } from '../../../styles/Text.css';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { Mono, SettingRow } from '../styles.css';
|
||||
import { LogoutDialog } from '../../../components/LogoutDialog';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
|
||||
export function DeviceTilePlaceholder() {
|
||||
return (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
className={SettingRow}
|
||||
style={{ height: toRem(66) }}
|
||||
variant="Background"
|
||||
outlined
|
||||
direction="Column"
|
||||
gap="400"
|
||||
/>
|
||||
|
|
@ -52,8 +53,7 @@ function DeviceActiveTime({ ts }: { ts: number }) {
|
|||
<>
|
||||
{today(ts) && t('Settings.today')}
|
||||
{yesterday(ts) && t('Settings.yesterday')}
|
||||
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)}{' '}
|
||||
{timeHourMinute(ts)}
|
||||
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
|
||||
</>
|
||||
</Text>
|
||||
);
|
||||
|
|
@ -66,13 +66,13 @@ function DeviceDetails({ device }: { device: IMyDevice }) {
|
|||
{typeof device.device_id === 'string' && (
|
||||
<Text className={BreakWord} size="T200" priority="300">
|
||||
{t('Settings.device_id')}
|
||||
<i>{device.device_id}</i>
|
||||
<span className={Mono}>{device.device_id}</span>
|
||||
</Text>
|
||||
)}
|
||||
{typeof device.last_seen_ip === 'string' && (
|
||||
<Text className={BreakWord} size="T200" priority="300">
|
||||
{t('Settings.ip_address')}
|
||||
<i>{device.last_seen_ip}</i>
|
||||
<span className={Mono}>{device.last_seen_ip}</span>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -100,9 +100,9 @@ export function DeviceKeyDetails({ crypto }: DeviceKeyDetailsProps) {
|
|||
return (
|
||||
<Text className={BreakWord} size="T200" priority="300">
|
||||
{t('Settings.device_key')}
|
||||
<i>
|
||||
<span className={Mono}>
|
||||
{keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : t('Settings.loading')}
|
||||
</i>
|
||||
</span>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { Box, Text } from 'folds';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SectionLabel, SettingRow } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsPage } from '../SettingsPage';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { LocalBackup } from './LocalBackup';
|
||||
|
|
@ -67,101 +68,74 @@ export function Devices({ requestClose }: DevicesProps) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{t('Settings.devices_title')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="700">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.security')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.device_verification')}
|
||||
description={t('Settings.device_verification_desc')}
|
||||
after={
|
||||
<>
|
||||
<EnableVerification visible={!crossSigningActive} />
|
||||
{crossSigningActive && (
|
||||
<Box gap="200" alignItems="Center">
|
||||
<VerificationStatusBadge
|
||||
verificationStatus={verificationStatus}
|
||||
otherUnverifiedCount={unverifiedDeviceCount}
|
||||
/>
|
||||
<DeviceVerificationOptions />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
<SettingsPage title={t('Settings.devices_title')} requestClose={requestClose}>
|
||||
<SettingsSection label={t('Settings.security')}>
|
||||
<SettingTile
|
||||
title={t('Settings.device_verification')}
|
||||
description={t('Settings.device_verification_desc')}
|
||||
after={
|
||||
<>
|
||||
<EnableVerification visible={!crossSigningActive} />
|
||||
{crossSigningActive && (
|
||||
<Box gap="200" alignItems="Center">
|
||||
<VerificationStatusBadge
|
||||
verificationStatus={verificationStatus}
|
||||
otherUnverifiedCount={unverifiedDeviceCount}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.current')}</Text>
|
||||
{currentDevice ? (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<DeviceTile
|
||||
device={currentDevice}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
options={<DeviceLogoutBtn />}
|
||||
>
|
||||
{crypto && <DeviceKeyDetails crypto={crypto} />}
|
||||
</DeviceTile>
|
||||
{crossSigningActive &&
|
||||
verificationStatus === VerificationStatus.Unverified &&
|
||||
defaultSecretStorageKeyId &&
|
||||
defaultSecretStorageKeyContent && (
|
||||
<VerifyCurrentDeviceTile
|
||||
secretStorageKeyId={defaultSecretStorageKeyId}
|
||||
secretStorageKeyContent={defaultSecretStorageKeyContent}
|
||||
/>
|
||||
)}
|
||||
{crypto && verificationStatus === VerificationStatus.Verified && (
|
||||
<BackupRestoreTile crypto={crypto} />
|
||||
)}
|
||||
</SequenceCard>
|
||||
) : (
|
||||
<DeviceTilePlaceholder />
|
||||
)}
|
||||
</Box>
|
||||
{devices === undefined && <DevicesPlaceholder />}
|
||||
{otherDevices && (
|
||||
<OtherDevices
|
||||
devices={otherDevices}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
showVerification={
|
||||
crossSigningActive && verificationStatus === VerificationStatus.Verified
|
||||
}
|
||||
<DeviceVerificationOptions />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
<Box direction="Column" gap="200">
|
||||
<Text as="span" className={SectionLabel}>
|
||||
{t('Settings.current')}
|
||||
</Text>
|
||||
{currentDevice ? (
|
||||
<SequenceCard
|
||||
className={SettingRow}
|
||||
variant="Background"
|
||||
outlined
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<DeviceTile
|
||||
device={currentDevice}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
options={<DeviceLogoutBtn />}
|
||||
>
|
||||
{crypto && <DeviceKeyDetails crypto={crypto} />}
|
||||
</DeviceTile>
|
||||
{crossSigningActive &&
|
||||
verificationStatus === VerificationStatus.Unverified &&
|
||||
defaultSecretStorageKeyId &&
|
||||
defaultSecretStorageKeyContent && (
|
||||
<VerifyCurrentDeviceTile
|
||||
secretStorageKeyId={defaultSecretStorageKeyId}
|
||||
secretStorageKeyContent={defaultSecretStorageKeyContent}
|
||||
/>
|
||||
)}
|
||||
<LocalBackup />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
{crypto && verificationStatus === VerificationStatus.Verified && (
|
||||
<BackupRestoreTile crypto={crypto} />
|
||||
)}
|
||||
</SequenceCard>
|
||||
) : (
|
||||
<DeviceTilePlaceholder />
|
||||
)}
|
||||
</Box>
|
||||
</Page>
|
||||
{devices === undefined && <DevicesPlaceholder />}
|
||||
{otherDevices && (
|
||||
<OtherDevices
|
||||
devices={otherDevices}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
showVerification={
|
||||
crossSigningActive && verificationStatus === VerificationStatus.Verified
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<LocalBackup />
|
||||
</SettingsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import React, { FormEventHandler, useCallback, useEffect, useState } from 'react
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Button, color, Icon, Icons, Spinner, Text, toRem } from 'folds';
|
||||
import FileSaver from 'file-saver';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { PasswordInput } from '../../../components/password-input';
|
||||
import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
|
|
@ -308,24 +307,9 @@ function ImportKeysTile() {
|
|||
export function LocalBackup() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.local_backup')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<ExportKeysTile />
|
||||
</SequenceCard>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<ImportKeysTile />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.local_backup')}>
|
||||
<ExportKeysTile />
|
||||
<ImportKeysTile />
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Box, Button, config, Menu, Spinner, Text } from 'folds';
|
||||
import { AuthDict, IMyDevice, MatrixError } from 'matrix-js-sdk';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SectionLabel, SettingRow } from '../styles.css';
|
||||
import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA';
|
||||
import { DeviceDeleteBtn, DeviceTile } from './DeviceTile';
|
||||
import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback';
|
||||
|
|
@ -104,83 +104,91 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
|
|||
|
||||
return devices.length > 0 ? (
|
||||
<>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.others')}</Text>
|
||||
{authMetadata && (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.device_dashboard')}
|
||||
description={t('Settings.device_dashboard_desc')}
|
||||
after={
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={handleDashboardOIDC}
|
||||
>
|
||||
<Text size="B300">{t('Settings.open')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
{devices
|
||||
.sort((d1, d2) => {
|
||||
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
|
||||
return d1.last_seen_ts < d2.last_seen_ts ? 1 : -1;
|
||||
})
|
||||
.map((device) => (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text as="span" className={SectionLabel}>
|
||||
{t('Settings.others')}
|
||||
</Text>
|
||||
<Box direction="Column">
|
||||
{authMetadata && (
|
||||
<SequenceCard
|
||||
key={device.device_id}
|
||||
className={SequenceCardStyle}
|
||||
variant={deleted.has(device.device_id) ? 'Critical' : 'SurfaceVariant'}
|
||||
className={SettingRow}
|
||||
variant="Background"
|
||||
outlined
|
||||
mergeBorder
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<DeviceTile
|
||||
device={device}
|
||||
deleted={deleted.has(device.device_id)}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
disabled={deleting}
|
||||
options={
|
||||
authMetadata ? (
|
||||
<DeviceDeleteBtn
|
||||
deviceId={device.device_id}
|
||||
deleted={false}
|
||||
onDeleteToggle={handleDeleteOIDC}
|
||||
/>
|
||||
) : (
|
||||
<DeviceDeleteBtn
|
||||
deviceId={device.device_id}
|
||||
deleted={deleted.has(device.device_id)}
|
||||
onDeleteToggle={handleToggleDelete}
|
||||
disabled={deleting}
|
||||
/>
|
||||
)
|
||||
<SettingTile
|
||||
title={t('Settings.device_dashboard')}
|
||||
description={t('Settings.device_dashboard_desc')}
|
||||
after={
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={handleDashboardOIDC}
|
||||
>
|
||||
<Text size="B300">{t('Settings.open')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{showVerification && crypto && (
|
||||
<DeviceVerificationStatus
|
||||
crypto={crypto}
|
||||
userId={mx.getSafeUserId()}
|
||||
deviceId={device.device_id}
|
||||
>
|
||||
{(status) =>
|
||||
status === VerificationStatus.Unverified && (
|
||||
<VerifyOtherDeviceTile crypto={crypto} deviceId={device.device_id} />
|
||||
</SequenceCard>
|
||||
)}
|
||||
{devices
|
||||
.sort((d1, d2) => {
|
||||
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
|
||||
return d1.last_seen_ts < d2.last_seen_ts ? 1 : -1;
|
||||
})
|
||||
.map((device) => (
|
||||
<SequenceCard
|
||||
key={device.device_id}
|
||||
className={SettingRow}
|
||||
variant={deleted.has(device.device_id) ? 'Critical' : 'Background'}
|
||||
outlined
|
||||
mergeBorder={!deleted.has(device.device_id)}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<DeviceTile
|
||||
device={device}
|
||||
deleted={deleted.has(device.device_id)}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
disabled={deleting}
|
||||
options={
|
||||
authMetadata ? (
|
||||
<DeviceDeleteBtn
|
||||
deviceId={device.device_id}
|
||||
deleted={false}
|
||||
onDeleteToggle={handleDeleteOIDC}
|
||||
/>
|
||||
) : (
|
||||
<DeviceDeleteBtn
|
||||
deviceId={device.device_id}
|
||||
deleted={deleted.has(device.device_id)}
|
||||
onDeleteToggle={handleToggleDelete}
|
||||
disabled={deleting}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</DeviceVerificationStatus>
|
||||
)}
|
||||
</SequenceCard>
|
||||
))}
|
||||
/>
|
||||
{showVerification && crypto && (
|
||||
<DeviceVerificationStatus
|
||||
crypto={crypto}
|
||||
userId={mx.getSafeUserId()}
|
||||
deviceId={device.device_id}
|
||||
>
|
||||
{(status) =>
|
||||
status === VerificationStatus.Unverified && (
|
||||
<VerifyOtherDeviceTile crypto={crypto} deviceId={device.device_id} />
|
||||
)
|
||||
}
|
||||
</DeviceVerificationStatus>
|
||||
)}
|
||||
</SequenceCard>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
{deleted.size > 0 && (
|
||||
<Menu
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { GlobalPacks } from './GlobalPacks';
|
||||
import { UserPack } from './UserPack';
|
||||
import { ImagePack } from '../../../plugins/custom-emoji';
|
||||
import { ImagePackView } from '../../../components/image-pack-view';
|
||||
import { SettingsPage } from '../SettingsPage';
|
||||
|
||||
type EmojisStickersProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -23,31 +22,9 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{t('Settings.emojis_stickers_title')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="700">
|
||||
<UserPack onViewPack={setImagePack} />
|
||||
<GlobalPacks onViewPack={setImagePack} />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
<SettingsPage title={t('Settings.emojis_stickers_title')} requestClose={requestClose}>
|
||||
<UserPack onViewPack={setImagePack} />
|
||||
<GlobalPacks onViewPack={setImagePack} />
|
||||
</SettingsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { useAtomValue } from 'jotai';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useGlobalImagePacks, useRoomsImagePacks } from '../../../hooks/useImagePacks';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SectionLabel, SettingRow } from '../styles.css';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||
|
|
@ -182,7 +182,7 @@ function GlobalPackSelector({
|
|||
return (
|
||||
<SequenceCard
|
||||
key={pack.id}
|
||||
className={SequenceCardStyle}
|
||||
className={SettingRow}
|
||||
variant={added ? 'Success' : 'SurfaceVariant'}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
|
|
@ -220,7 +220,7 @@ function GlobalPackSelector({
|
|||
|
||||
{roomToPacks.size === 0 && (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
className={SettingRow}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
|
|
@ -357,8 +357,10 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
|
|||
return (
|
||||
<SequenceCard
|
||||
key={pack.id}
|
||||
className={SequenceCardStyle}
|
||||
variant={removed ? 'Critical' : 'SurfaceVariant'}
|
||||
className={SettingRow}
|
||||
variant={removed ? 'Critical' : 'Background'}
|
||||
outlined
|
||||
mergeBorder={!removed}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
|
|
@ -424,71 +426,77 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.favorite_packs')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.select_pack')}
|
||||
description={t('Settings.select_pack_desc')}
|
||||
after={
|
||||
<>
|
||||
<Button
|
||||
onClick={handleSelectMenu}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
>
|
||||
<Text size="B300">{t('Settings.select')}</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
style={{
|
||||
display: 'flex',
|
||||
maxWidth: toRem(400),
|
||||
width: '100vw',
|
||||
maxHeight: toRem(500),
|
||||
<Box direction="Column" gap="200">
|
||||
<Text as="span" className={SectionLabel}>
|
||||
{t('Settings.favorite_packs')}
|
||||
</Text>
|
||||
<Box direction="Column">
|
||||
<SequenceCard
|
||||
className={SettingRow}
|
||||
variant="Background"
|
||||
outlined
|
||||
mergeBorder
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.select_pack')}
|
||||
description={t('Settings.select_pack_desc')}
|
||||
after={
|
||||
<>
|
||||
<Button
|
||||
onClick={handleSelectMenu}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
>
|
||||
<Text size="B300">{t('Settings.select')}</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<GlobalPackSelector
|
||||
packs={unselectedGlobalPacks}
|
||||
useAuthentication={useAuthentication}
|
||||
onSelect={handleSelected}
|
||||
/>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
{globalPacks.map(renderPack)}
|
||||
{nonGlobalPacks
|
||||
.filter((pack) => !!selectedPacks.find((addr) => packAddressEqual(pack.address, addr)))
|
||||
.map(renderPack)}
|
||||
<Menu
|
||||
style={{
|
||||
display: 'flex',
|
||||
maxWidth: toRem(400),
|
||||
width: '100vw',
|
||||
maxHeight: toRem(500),
|
||||
}}
|
||||
>
|
||||
<GlobalPackSelector
|
||||
packs={unselectedGlobalPacks}
|
||||
useAuthentication={useAuthentication}
|
||||
onSelect={handleSelected}
|
||||
/>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
{globalPacks.map(renderPack)}
|
||||
{nonGlobalPacks
|
||||
.filter((pack) => !!selectedPacks.find((addr) => packAddressEqual(pack.address, addr)))
|
||||
.map(renderPack)}
|
||||
</Box>
|
||||
</Box>
|
||||
{hasChanges && (
|
||||
<Menu
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Avatar, AvatarFallback, AvatarImage, Box, Button, Icon, Icons, Text } from 'folds';
|
||||
import { Avatar, AvatarFallback, AvatarImage, Button, Icon, Icons, Text } from 'folds';
|
||||
import { useUserImagePack } from '../../../hooks/useImagePacks';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { ImagePack, ImageUsage } from '../../../plugins/custom-emoji';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||
|
|
@ -32,37 +31,34 @@ export function UserPack({ onViewPack }: UserPackProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.default_pack')}</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title={userPack?.meta.name ?? t('Settings.unknown')}
|
||||
description={userPack?.meta.attribution}
|
||||
before={
|
||||
<Avatar size="300" radii="300">
|
||||
{avatarUrl ? (
|
||||
<AvatarImage style={{ objectFit: 'contain' }} src={avatarUrl} />
|
||||
) : (
|
||||
<AvatarFallback>
|
||||
<Icon size="400" src={Icons.Sticker} filled />
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
}
|
||||
after={
|
||||
<Button
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={handleView}
|
||||
>
|
||||
<Text size="B300">{t('Settings.view')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.default_pack')}>
|
||||
<SettingTile
|
||||
title={userPack?.meta.name ?? t('Settings.unknown')}
|
||||
description={userPack?.meta.attribution}
|
||||
before={
|
||||
<Avatar size="300" radii="300">
|
||||
{avatarUrl ? (
|
||||
<AvatarImage style={{ objectFit: 'contain' }} src={avatarUrl} />
|
||||
) : (
|
||||
<AvatarFallback>
|
||||
<Icon size="400" src={Icons.Sticker} filled />
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
}
|
||||
after={
|
||||
<Button
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={handleView}
|
||||
>
|
||||
<Text size="B300">{t('Settings.view')}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ import {
|
|||
Button,
|
||||
config,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Scroll,
|
||||
Switch,
|
||||
Text,
|
||||
toRem,
|
||||
|
|
@ -25,8 +23,6 @@ import {
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
|
|
@ -34,7 +30,8 @@ import { KeySymbol } from '../../../utils/key-symbol';
|
|||
import { isMacOS } from '../../../utils/user-agent';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { DarkTheme, LightTheme } from '../../../hooks/useTheme';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingsPage } from '../SettingsPage';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
|
||||
type ThemeChoice = 'system' | typeof LightTheme.id | typeof DarkTheme.id;
|
||||
|
||||
|
|
@ -52,10 +49,7 @@ function ThemeSelect() {
|
|||
choice = LightTheme.id;
|
||||
}
|
||||
|
||||
const choices = useMemo<ThemeChoice[]>(
|
||||
() => ['system', LightTheme.id, DarkTheme.id],
|
||||
[]
|
||||
);
|
||||
const choices = useMemo<ThemeChoice[]>(() => ['system', LightTheme.id, DarkTheme.id], []);
|
||||
|
||||
const choiceLabel = useMemo<Record<ThemeChoice, string>>(
|
||||
() => ({
|
||||
|
|
@ -184,34 +178,17 @@ function PageZoomInput() {
|
|||
|
||||
function Appearance() {
|
||||
const { t } = useTranslation();
|
||||
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
||||
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.appearance')}</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile title={t('Settings.theme')} after={<ThemeSelect />} />
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.monochrome_mode')}
|
||||
after={<Switch variant="Primary" value={monochromeMode} onChange={setMonochromeMode} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.twitter_emoji')}
|
||||
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile title={t('Settings.page_zoom')} after={<PageZoomInput />} />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.appearance')}>
|
||||
<SettingTile title={t('Settings.theme')} after={<ThemeSelect />} />
|
||||
<SettingTile
|
||||
title={t('Settings.twitter_emoji')}
|
||||
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
|
||||
/>
|
||||
<SettingTile title={t('Settings.page_zoom')} after={<PageZoomInput />} />
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -222,31 +199,24 @@ function Editor() {
|
|||
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.editor')}</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.enter_newline')}
|
||||
description={t('Settings.enter_newline_desc', {
|
||||
key: isMacOS() ? KeySymbol.Command : 'Ctrl',
|
||||
})}
|
||||
after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.markdown')}
|
||||
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.hide_activity')}
|
||||
description={t('Settings.hide_activity_desc')}
|
||||
after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.editor')}>
|
||||
<SettingTile
|
||||
title={t('Settings.enter_newline')}
|
||||
description={t('Settings.enter_newline_desc', {
|
||||
key: isMacOS() ? KeySymbol.Command : 'Ctrl',
|
||||
})}
|
||||
after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.markdown')}
|
||||
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.hide_activity')}
|
||||
description={t('Settings.hide_activity_desc')}
|
||||
after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -262,69 +232,56 @@ function Messages() {
|
|||
);
|
||||
const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
|
||||
const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
||||
|
||||
// Merged control for the two service-event toggles (joins/leaves +
|
||||
// name/avatar changes). Reads on only when *both* classes are hidden, and
|
||||
// writes both atoms together. AND (not OR) keeps the switch honest against
|
||||
// the default state: out of the box hideMembershipEvents is false (joins/
|
||||
// leaves shown) and hideNickAvatarEvents is true, so the switch reads OFF
|
||||
// and the visible join/leave events agree with it. The timeline still reads
|
||||
// each atom directly, so existing persisted preferences are untouched until
|
||||
// the user flips this switch (which then sets both to the same value).
|
||||
const hideServiceEvents = hideMembershipEvents && hideNickAvatarEvents;
|
||||
const setHideServiceEvents = (value: boolean) => {
|
||||
setHideMembershipEvents(value);
|
||||
setHideNickAvatarEvents(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.messages')}</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.hide_membership')}
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={hideMembershipEvents}
|
||||
onChange={setHideMembershipEvents}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.hide_profile')}
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={hideNickAvatarEvents}
|
||||
onChange={setHideNickAvatarEvents}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.disable_media_auto_load')}
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={!mediaAutoLoad}
|
||||
onChange={(v) => setMediaAutoLoad(!v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.url_preview')}
|
||||
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.url_preview_encrypted')}
|
||||
after={<Switch variant="Primary" value={encUrlPreview} onChange={setEncUrlPreview} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
||||
<SettingTile
|
||||
title={t('Settings.show_hidden_events')}
|
||||
after={
|
||||
<Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.messages')}>
|
||||
<SettingTile
|
||||
title={t('Settings.hide_service_events')}
|
||||
description={t('Settings.hide_service_events_desc')}
|
||||
after={
|
||||
<Switch variant="Primary" value={hideServiceEvents} onChange={setHideServiceEvents} />
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.disable_media_auto_load')}
|
||||
after={
|
||||
<Switch variant="Primary" value={!mediaAutoLoad} onChange={(v) => setMediaAutoLoad(!v)} />
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.url_preview')}
|
||||
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function Advanced() {
|
||||
const { t } = useTranslation();
|
||||
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
|
||||
return (
|
||||
<SettingsSection label={t('Settings.advanced')}>
|
||||
<SettingTile
|
||||
title={t('Settings.developer_mode')}
|
||||
description={t('Settings.developer_mode_desc')}
|
||||
after={<Switch variant="Primary" value={developerTools} onChange={setDeveloperTools} />}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -334,32 +291,11 @@ type GeneralProps = {
|
|||
export function General({ requestClose }: GeneralProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{t('Settings.general_title')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="700">
|
||||
<Appearance />
|
||||
<Editor />
|
||||
<Messages />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
<SettingsPage title={t('Settings.general_title')} requestClose={requestClose}>
|
||||
<Appearance />
|
||||
<Editor />
|
||||
<Messages />
|
||||
<Advanced />
|
||||
</SettingsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export * from './Settings';
|
||||
export * from './SettingsScreen';
|
||||
export * from './MobileSettingsHorseshoe';
|
||||
export * from './SettingsPage';
|
||||
export * from './SettingsSection';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge, Box, Text } from 'folds';
|
||||
import { ConditionKind, IPushRules, PushRuleCondition, PushRuleKind, RuleId } from 'matrix-js-sdk';
|
||||
import { useAccountData } from '../../../hooks/useAccountData';
|
||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { PushRuleData, usePushRule } from '../../../hooks/usePushRule';
|
||||
import {
|
||||
getNotificationModeActions,
|
||||
|
|
@ -82,73 +80,36 @@ export function AllMessagesNotifications() {
|
|||
);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||
<Text size="L400">{t('Settings.all_messages')}</Text>
|
||||
<Box gap="100">
|
||||
<Text size="T200">{t('Settings.badge')}</Text>
|
||||
<Badge radii="300" variant="Secondary" fill="Solid">
|
||||
<Text size="L400">1</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.one_to_one')}
|
||||
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.DM} oneToOne />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.one_to_one_encrypted')}
|
||||
after={
|
||||
<AllMessagesModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.EncryptedDM}
|
||||
encrypted
|
||||
oneToOne
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.rooms')}
|
||||
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.Message} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.rooms_encrypted')}
|
||||
after={
|
||||
<AllMessagesModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.EncryptedMessage}
|
||||
encrypted
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.all_messages')}>
|
||||
<SettingTile
|
||||
title={t('Settings.one_to_one')}
|
||||
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.DM} oneToOne />}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.one_to_one_encrypted')}
|
||||
after={
|
||||
<AllMessagesModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.EncryptedDM}
|
||||
encrypted
|
||||
oneToOne
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.rooms')}
|
||||
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.Message} />}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.rooms_encrypted')}
|
||||
after={
|
||||
<AllMessagesModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.EncryptedMessage}
|
||||
encrypted
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IPushRule, IPushRules, PushRuleKind } from 'matrix-js-sdk';
|
||||
import { Box, Text, Badge, Button, Input, config, IconButton, Icons, Icon, Spinner } from 'folds';
|
||||
import { Box, Text, Button, Input, config, IconButton, Icons, Icon, Spinner } from 'folds';
|
||||
import { useAccountData } from '../../../hooks/useAccountData';
|
||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { Mono } from '../styles.css';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import {
|
||||
getNotificationModeActions,
|
||||
|
|
@ -163,44 +163,25 @@ export function KeywordMessagesNotifications() {
|
|||
}, [pushRules]);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||
<Text size="L400">{t('Settings.keyword_messages')}</Text>
|
||||
<Box gap="100">
|
||||
<Text size="T200">{t('Settings.badge')}</Text>
|
||||
<Badge radii="300" variant="Success" fill="Solid">
|
||||
<Text size="L400">1</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
<SettingsSection label={t('Settings.keyword_messages')}>
|
||||
<SettingTile
|
||||
title={t('Settings.select_keyword')}
|
||||
description={t('Settings.select_keyword_desc')}
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.select_keyword')}
|
||||
description={t('Settings.select_keyword_desc')}
|
||||
>
|
||||
<KeywordInput />
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
<KeywordInput />
|
||||
</SettingTile>
|
||||
{keywordPushRules.map((pushRule) => (
|
||||
<SequenceCard
|
||||
<SettingTile
|
||||
key={pushRule.rule_id}
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={`"${pushRule.pattern}"`}
|
||||
before={<KeywordCross pushRule={pushRule} />}
|
||||
after={<KeywordModeSwitcher pushRule={pushRule} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
title={
|
||||
<Text as="span" size="T300" className={Mono}>
|
||||
{`"${pushRule.pattern}"`}
|
||||
</Text>
|
||||
}
|
||||
before={<KeywordCross pushRule={pushRule} />}
|
||||
after={<KeywordModeSwitcher pushRule={pushRule} />}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SystemNotification } from './SystemNotification';
|
||||
import { AllMessagesNotifications } from './AllMessages';
|
||||
import { SpecialMessagesNotifications } from './SpecialMessages';
|
||||
import { KeywordMessagesNotifications } from './KeywordMessages';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsPage } from '../SettingsPage';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
|
||||
type NotificationsProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -16,44 +14,14 @@ type NotificationsProps = {
|
|||
export function Notifications({ requestClose }: NotificationsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Page variant="SurfaceVariant">
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{t('Settings.notifications_title')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
||||
<Box direction="Column" gap="700">
|
||||
<SystemNotification />
|
||||
<AllMessagesNotifications />
|
||||
<SpecialMessagesNotifications />
|
||||
<KeywordMessagesNotifications />
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.block_messages')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile description={t('Settings.block_messages_moved')} />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
<SettingsPage title={t('Settings.notifications_title')} requestClose={requestClose}>
|
||||
<SystemNotification />
|
||||
<AllMessagesNotifications />
|
||||
<SpecialMessagesNotifications />
|
||||
<KeywordMessagesNotifications />
|
||||
<SettingsSection label={t('Settings.block_messages')}>
|
||||
<SettingTile description={t('Settings.block_messages_moved')} />
|
||||
</SettingsSection>
|
||||
</SettingsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConditionKind, IPushRules, PushRuleKind, RuleId } from 'matrix-js-sdk';
|
||||
import { Box, Text, Badge } from 'folds';
|
||||
import { useAccountData } from '../../../hooks/useAccountData';
|
||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||
import { useUserProfile } from '../../../hooks/useUserProfile';
|
||||
|
|
@ -124,80 +122,61 @@ export function SpecialMessagesNotifications() {
|
|||
);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||
<Text size="L400">{t('Settings.special_messages')}</Text>
|
||||
<Box gap="100">
|
||||
<Text size="T200">{t('Settings.badge')}</Text>
|
||||
<Badge radii="300" variant="Success" fill="Solid">
|
||||
<Text size="L400">1</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title={t('Settings.mention_user_id', { userId })}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.IsUserMention}
|
||||
defaultPushRuleData={getDefaultIsUserMention(userId)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title={
|
||||
displayName
|
||||
? t('Settings.contains_displayname_value', { displayName })
|
||||
: t('Settings.contains_displayname')
|
||||
}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.ContainsDisplayName}
|
||||
defaultPushRuleData={DefaultContainsDisplayName}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title={t('Settings.contains_username', { username: getMxIdLocalPart(userId) })}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.ContainsUserName}
|
||||
defaultPushRuleData={getDefaultContainsUsername(getMxIdLocalPart(userId) ?? userId)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title={t('Settings.mention_room')}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.IsRoomMention}
|
||||
defaultPushRuleData={DefaultIsRoomMention}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title={t('Settings.contains_room')}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.AtRoomNotification}
|
||||
defaultPushRuleData={DefaultAtRoomNotification}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.special_messages')}>
|
||||
<SettingTile
|
||||
title={t('Settings.mention_user_id', { userId })}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.IsUserMention}
|
||||
defaultPushRuleData={getDefaultIsUserMention(userId)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={
|
||||
displayName
|
||||
? t('Settings.contains_displayname_value', { displayName })
|
||||
: t('Settings.contains_displayname')
|
||||
}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.ContainsDisplayName}
|
||||
defaultPushRuleData={DefaultContainsDisplayName}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.contains_username', { username: getMxIdLocalPart(userId) })}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.ContainsUserName}
|
||||
defaultPushRuleData={getDefaultContainsUsername(getMxIdLocalPart(userId) ?? userId)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.mention_room')}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.IsRoomMention}
|
||||
defaultPushRuleData={DefaultIsRoomMention}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.contains_room')}
|
||||
after={
|
||||
<MentionModeSwitcher
|
||||
pushRules={pushRules}
|
||||
ruleId={RuleId.AtRoomNotification}
|
||||
defaultPushRuleData={DefaultAtRoomNotification}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text, Switch, Button, color, Spinner } from 'folds';
|
||||
import { Text, Switch, Button, color, Spinner } from 'folds';
|
||||
import { IPusherRequest } from 'matrix-js-sdk';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SettingsSection } from '../SettingsSection';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
|
||||
|
|
@ -176,50 +175,26 @@ export function SystemNotification() {
|
|||
'isNotificationSounds'
|
||||
);
|
||||
const [inviteSpamFilter, setInviteSpamFilter] = useSetting(settingsAtom, 'inviteSpamFilter');
|
||||
// Gate the push row at the call site: PushNotification renders null when push
|
||||
// is unavailable, and SettingsSection's `filter(Boolean)` can only drop
|
||||
// JSX-level falsy children — a returned-null component would otherwise leave
|
||||
// an empty bordered first row in the grouped panel.
|
||||
const pushStatus = usePushNotificationStatus();
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">{t('Settings.system')}</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<PushNotification />
|
||||
</SequenceCard>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.notification_sound')}
|
||||
description={t('Settings.notification_sound_desc')}
|
||||
after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={t('Settings.invite_spam_filter')}
|
||||
description={t('Settings.invite_spam_filter_desc')}
|
||||
after={<Switch value={inviteSpamFilter} onChange={setInviteSpamFilter} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="Background"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<EmailNotification />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
<SettingsSection label={t('Settings.system')}>
|
||||
{pushStatus !== 'unavailable' && <PushNotification />}
|
||||
<SettingTile
|
||||
title={t('Settings.notification_sound')}
|
||||
description={t('Settings.notification_sound_desc')}
|
||||
after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
|
||||
/>
|
||||
<SettingTile
|
||||
title={t('Settings.invite_spam_filter')}
|
||||
description={t('Settings.invite_spam_filter_desc')}
|
||||
after={<Switch value={inviteSpamFilter} onChange={setInviteSpamFilter} />}
|
||||
/>
|
||||
<EmailNotification />
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,46 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
import { color, config, toRem } from 'folds';
|
||||
|
||||
export const SequenceCardStyle = style({
|
||||
padding: config.space.S300,
|
||||
// Section label above a grouped panel — uppercase, tracked, muted. Matches the
|
||||
// Dawn canon's "КОМАНДЫ" / "УЧАСТНИКИ · 28" panel headers (sans, not mono).
|
||||
export const SectionLabel = style({
|
||||
display: 'block',
|
||||
fontSize: toRem(11),
|
||||
lineHeight: toRem(16),
|
||||
fontWeight: config.fontWeight.W600,
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase',
|
||||
color: color.Surface.OnContainer,
|
||||
opacity: 0.5,
|
||||
paddingLeft: config.space.S100,
|
||||
paddingRight: config.space.S100,
|
||||
});
|
||||
|
||||
// Optional caption rendered under a section label (e.g. "Скрытые от чужих глаз").
|
||||
export const SectionFootnote = style({
|
||||
display: 'block',
|
||||
fontSize: toRem(12),
|
||||
lineHeight: toRem(16),
|
||||
color: color.Surface.OnContainer,
|
||||
opacity: 0.5,
|
||||
paddingLeft: config.space.S100,
|
||||
paddingRight: config.space.S100,
|
||||
paddingTop: config.space.S100,
|
||||
});
|
||||
|
||||
// One row inside a grouped panel. Slightly roomier than the old card so a
|
||||
// switch/menu row lands at a comfortable ~44px without feeling cramped.
|
||||
export const SettingRow = style({
|
||||
paddingTop: config.space.S300,
|
||||
paddingBottom: config.space.S300,
|
||||
paddingLeft: config.space.S400,
|
||||
paddingRight: config.space.S400,
|
||||
});
|
||||
|
||||
// JetBrains Mono for technical values — mxid, device ids, version, tokens —
|
||||
// the same stack the DM stream / bot surfaces use for handles & timestamps.
|
||||
export const Mono = style({
|
||||
fontFamily:
|
||||
'"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import {
|
|||
useActiveTheme,
|
||||
useSystemThemeKind,
|
||||
} from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
|
||||
export function UnAuthRouteThemeManager() {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
|
|
@ -30,7 +28,6 @@ export function UnAuthRouteThemeManager() {
|
|||
|
||||
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||
const activeTheme = useActiveTheme();
|
||||
const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = '';
|
||||
|
|
@ -38,12 +35,11 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
|||
|
||||
document.body.classList.add(...activeTheme.classNames);
|
||||
|
||||
if (monochromeMode) {
|
||||
document.body.style.filter = 'grayscale(1)';
|
||||
} else {
|
||||
document.body.style.filter = '';
|
||||
}
|
||||
}, [activeTheme, monochromeMode]);
|
||||
// Clear any leftover grayscale filter from the retired monochrome-mode
|
||||
// setting so users who had it enabled aren't stuck in grayscale after the
|
||||
// toggle was removed from Settings.
|
||||
document.body.style.filter = '';
|
||||
}, [activeTheme]);
|
||||
|
||||
return <ThemeContextProvider value={activeTheme}>{children}</ThemeContextProvider>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export type ThemeId = 'light-theme' | 'dark-theme';
|
|||
export interface Settings {
|
||||
themeId?: ThemeId;
|
||||
useSystemTheme: boolean;
|
||||
monochromeMode?: boolean;
|
||||
isMarkdown: boolean;
|
||||
editorToolbar: boolean;
|
||||
twitterEmoji: boolean;
|
||||
|
|
@ -39,7 +38,6 @@ const SYSTEM_TIME_FORMAT_CLEANUP_KEY = 'system-time-format-cleanup';
|
|||
const defaultSettings: Settings = {
|
||||
themeId: undefined,
|
||||
useSystemTheme: true,
|
||||
monochromeMode: false,
|
||||
isMarkdown: true,
|
||||
editorToolbar: false,
|
||||
twitterEmoji: false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue