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_notifications": "Notifications",
|
||||||
"menu_devices": "Devices",
|
"menu_devices": "Devices",
|
||||||
"menu_emojis_stickers": "Emojis & Stickers",
|
"menu_emojis_stickers": "Emojis & Stickers",
|
||||||
"menu_developer_tools": "Developer Tools",
|
|
||||||
"menu_about": "About",
|
"menu_about": "About",
|
||||||
"drag_to_close": "Drag down to close",
|
"drag_to_close": "Drag down to close",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
|
@ -105,7 +104,6 @@
|
||||||
"theme_light": "Light",
|
"theme_light": "Light",
|
||||||
"theme_dark": "Dark",
|
"theme_dark": "Dark",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"monochrome_mode": "Monochrome Mode",
|
|
||||||
"twitter_emoji": "Twitter Emoji",
|
"twitter_emoji": "Twitter Emoji",
|
||||||
"page_zoom": "Page Zoom",
|
"page_zoom": "Page Zoom",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
|
@ -116,12 +114,13 @@
|
||||||
"hide_activity": "Hide Typing & Read Receipts",
|
"hide_activity": "Hide Typing & Read Receipts",
|
||||||
"hide_activity_desc": "Turn off both typing status and read receipts to keep your activity private.",
|
"hide_activity_desc": "Turn off both typing status and read receipts to keep your activity private.",
|
||||||
"messages": "Messages",
|
"messages": "Messages",
|
||||||
"hide_membership": "Hide Membership Change",
|
"hide_service_events": "Hide Service Events",
|
||||||
"hide_profile": "Hide Profile Change",
|
"hide_service_events_desc": "Hide joins, leaves, name and avatar changes from the timeline.",
|
||||||
"disable_media_auto_load": "Disable Media Auto Load",
|
"disable_media_auto_load": "Disable Media Auto Load",
|
||||||
"url_preview": "Url Preview",
|
"url_preview": "Url Preview",
|
||||||
"url_preview_encrypted": "Url Preview in Encrypted Room",
|
"advanced": "Advanced",
|
||||||
"show_hidden_events": "Show Hidden Events",
|
"developer_mode": "Developer Mode",
|
||||||
|
"developer_mode_desc": "Unlock technical tools, like viewing the source of messages.",
|
||||||
"account_title": "Account",
|
"account_title": "Account",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
|
|
@ -139,7 +138,6 @@
|
||||||
"select_user": "Select User",
|
"select_user": "Select User",
|
||||||
"select_user_desc": "Prevent receiving messages or invites from user by adding their userId.",
|
"select_user_desc": "Prevent receiving messages or invites from user by adding their userId.",
|
||||||
"block": "Block",
|
"block": "Block",
|
||||||
"users": "Users",
|
|
||||||
"notifications_title": "Notifications",
|
"notifications_title": "Notifications",
|
||||||
"block_messages": "Block Messages",
|
"block_messages": "Block Messages",
|
||||||
"block_messages_moved": "This option has been moved to \"Account > Block Users\" section.",
|
"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}}\")",
|
"email_send_notif_to": "Send notification to your email. (\"{{email}}\")",
|
||||||
"unexpected_error": "Unexpected Error!",
|
"unexpected_error": "Unexpected Error!",
|
||||||
"all_messages": "All Messages",
|
"all_messages": "All Messages",
|
||||||
"badge": "Badge: ",
|
|
||||||
"one_to_one": "1-to-1 Chats",
|
"one_to_one": "1-to-1 Chats",
|
||||||
"one_to_one_encrypted": "1-to-1 Chats (Encrypted)",
|
"one_to_one_encrypted": "1-to-1 Chats (Encrypted)",
|
||||||
"rooms": "Rooms",
|
"rooms": "Rooms",
|
||||||
|
|
@ -256,26 +253,15 @@
|
||||||
"apply_ready": "Changes saved! Apply when ready.",
|
"apply_ready": "Changes saved! Apply when ready.",
|
||||||
"apply_changes": "Apply Changes",
|
"apply_changes": "Apply Changes",
|
||||||
"about_title": "About",
|
"about_title": "About",
|
||||||
"about_tagline": "Yet another matrix client.",
|
"about_tagline": "A messenger for everyone.",
|
||||||
"options": "Options",
|
"about_connected": "Connected to",
|
||||||
"clear_cache_title": "Clear Cache & Reload",
|
"clear_cache_title": "Clear Cache & Reload",
|
||||||
"clear_cache_desc": "Clear all your locally stored data and reload from server.",
|
"clear_cache_desc": "Clear all your locally stored data and reload from server.",
|
||||||
"clear_cache": "Clear Cache",
|
"clear_cache": "Clear Cache",
|
||||||
"legal": "Legal",
|
|
||||||
"privacy_policy_title": "Privacy Policy",
|
"privacy_policy_title": "Privacy Policy",
|
||||||
"privacy_policy_desc": "How your data is handled.",
|
"privacy_policy_desc": "How your data is handled.",
|
||||||
"privacy_policy_open": "Open",
|
"privacy_policy_open": "Open",
|
||||||
"credits": "Credits",
|
"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)."
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"Search": {
|
"Search": {
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@
|
||||||
"menu_notifications": "Уведомления",
|
"menu_notifications": "Уведомления",
|
||||||
"menu_devices": "Устройства",
|
"menu_devices": "Устройства",
|
||||||
"menu_emojis_stickers": "Эмодзи и стикеры",
|
"menu_emojis_stickers": "Эмодзи и стикеры",
|
||||||
"menu_developer_tools": "Инструменты разработчика",
|
|
||||||
"menu_about": "О приложении",
|
"menu_about": "О приложении",
|
||||||
"drag_to_close": "Потянуть вниз чтобы закрыть",
|
"drag_to_close": "Потянуть вниз чтобы закрыть",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
|
|
@ -105,7 +104,6 @@
|
||||||
"theme_light": "Светлая",
|
"theme_light": "Светлая",
|
||||||
"theme_dark": "Тёмная",
|
"theme_dark": "Тёмная",
|
||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
"monochrome_mode": "Монохромный режим",
|
|
||||||
"twitter_emoji": "Эмодзи Twitter",
|
"twitter_emoji": "Эмодзи Twitter",
|
||||||
"page_zoom": "Масштаб страницы",
|
"page_zoom": "Масштаб страницы",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
|
|
@ -116,12 +114,13 @@
|
||||||
"hide_activity": "Скрыть набор текста и уведомления о прочтении",
|
"hide_activity": "Скрыть набор текста и уведомления о прочтении",
|
||||||
"hide_activity_desc": "Отключить статус набора и отчёты о прочтении для сохранения приватности.",
|
"hide_activity_desc": "Отключить статус набора и отчёты о прочтении для сохранения приватности.",
|
||||||
"messages": "Сообщения",
|
"messages": "Сообщения",
|
||||||
"hide_membership": "Скрыть изменения участников",
|
"hide_service_events": "Скрывать служебные сообщения",
|
||||||
"hide_profile": "Скрыть изменения профиля",
|
"hide_service_events_desc": "Скрывать вступления, выходы и смену имени или аватара в ленте.",
|
||||||
"disable_media_auto_load": "Отключить автозагрузку медиа",
|
"disable_media_auto_load": "Отключить автозагрузку медиа",
|
||||||
"url_preview": "Предпросмотр ссылок",
|
"url_preview": "Предпросмотр ссылок",
|
||||||
"url_preview_encrypted": "Предпросмотр ссылок в зашифрованных комнатах",
|
"advanced": "Дополнительно",
|
||||||
"show_hidden_events": "Показывать скрытые события",
|
"developer_mode": "Режим разработчика",
|
||||||
|
"developer_mode_desc": "Открывает технические функции, например исходный код сообщений.",
|
||||||
"account_title": "Аккаунт",
|
"account_title": "Аккаунт",
|
||||||
"profile": "Профиль",
|
"profile": "Профиль",
|
||||||
"avatar": "Аватар",
|
"avatar": "Аватар",
|
||||||
|
|
@ -139,7 +138,6 @@
|
||||||
"select_user": "Выбрать пользователя",
|
"select_user": "Выбрать пользователя",
|
||||||
"select_user_desc": "Заблокируйте получение сообщений и приглашений от пользователя, добавив его идентификатор.",
|
"select_user_desc": "Заблокируйте получение сообщений и приглашений от пользователя, добавив его идентификатор.",
|
||||||
"block": "Заблокировать",
|
"block": "Заблокировать",
|
||||||
"users": "Пользователи",
|
|
||||||
"notifications_title": "Уведомления",
|
"notifications_title": "Уведомления",
|
||||||
"block_messages": "Блокировка сообщений",
|
"block_messages": "Блокировка сообщений",
|
||||||
"block_messages_moved": "Эта опция перенесена в раздел «Аккаунт > Заблокированные пользователи».",
|
"block_messages_moved": "Эта опция перенесена в раздел «Аккаунт > Заблокированные пользователи».",
|
||||||
|
|
@ -166,7 +164,6 @@
|
||||||
"email_send_notif_to": "Отправлять уведомления на вашу почту. (\"{{email}}\")",
|
"email_send_notif_to": "Отправлять уведомления на вашу почту. (\"{{email}}\")",
|
||||||
"unexpected_error": "Непредвиденная ошибка!",
|
"unexpected_error": "Непредвиденная ошибка!",
|
||||||
"all_messages": "Все сообщения",
|
"all_messages": "Все сообщения",
|
||||||
"badge": "Значок: ",
|
|
||||||
"one_to_one": "Личные чаты",
|
"one_to_one": "Личные чаты",
|
||||||
"one_to_one_encrypted": "Личные чаты (зашифрованные)",
|
"one_to_one_encrypted": "Личные чаты (зашифрованные)",
|
||||||
"rooms": "Комнаты",
|
"rooms": "Комнаты",
|
||||||
|
|
@ -256,26 +253,15 @@
|
||||||
"apply_ready": "Изменения сохранены! Примените, когда будете готовы.",
|
"apply_ready": "Изменения сохранены! Примените, когда будете готовы.",
|
||||||
"apply_changes": "Применить изменения",
|
"apply_changes": "Применить изменения",
|
||||||
"about_title": "О приложении",
|
"about_title": "О приложении",
|
||||||
"about_tagline": "Ещё один клиент для Matrix.",
|
"about_tagline": "Вседоступный мессенджер.",
|
||||||
"options": "Параметры",
|
"about_connected": "Подключено к",
|
||||||
"clear_cache_title": "Очистить кэш и перезагрузить",
|
"clear_cache_title": "Очистить кэш и перезагрузить",
|
||||||
"clear_cache_desc": "Удалить все локально сохранённые данные и загрузить заново с сервера.",
|
"clear_cache_desc": "Удалить все локально сохранённые данные и загрузить заново с сервера.",
|
||||||
"clear_cache": "Очистить кэш",
|
"clear_cache": "Очистить кэш",
|
||||||
"legal": "Юридическое",
|
|
||||||
"privacy_policy_title": "Политика конфиденциальности",
|
"privacy_policy_title": "Политика конфиденциальности",
|
||||||
"privacy_policy_desc": "Как обрабатываются ваши данные.",
|
"privacy_policy_desc": "Как обрабатываются ваши данные.",
|
||||||
"privacy_policy_open": "Открыть",
|
"privacy_policy_open": "Открыть",
|
||||||
"credits": "Благодарности",
|
"about_credits": "Vojo создан на открытом ПО — включая matrix-js-sdk (Apache 2.0), Twemoji (CC-BY 4.0) и звуки Material Design (CC-BY 4.0)."
|
||||||
"devtools_title": "Инструменты разработчика",
|
|
||||||
"enable_devtools": "Включить инструменты разработчика",
|
|
||||||
"access_token": "Токен доступа",
|
|
||||||
"access_token_desc": "Скопировать токен доступа в буфер обмена.",
|
|
||||||
"account_data": "Данные аккаунта",
|
|
||||||
"account_data_global": "Глобальные",
|
|
||||||
"account_data_desc": "Данные, хранящиеся в глобальных данных вашего аккаунта.",
|
|
||||||
"events": "События",
|
|
||||||
"total": "Всего: {{count}}",
|
|
||||||
"add_new": "Добавить"
|
|
||||||
},
|
},
|
||||||
"Search": {
|
"Search": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import { Account } from './account';
|
||||||
import { Notifications } from './notifications';
|
import { Notifications } from './notifications';
|
||||||
import { Devices } from './devices';
|
import { Devices } from './devices';
|
||||||
import { EmojisStickers } from './emojis-stickers';
|
import { EmojisStickers } from './emojis-stickers';
|
||||||
import { DeveloperTools } from './developer-tools';
|
|
||||||
import { About } from './about';
|
import { About } from './about';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
@ -35,7 +34,6 @@ export enum SettingsPages {
|
||||||
NotificationPage,
|
NotificationPage,
|
||||||
DevicesPage,
|
DevicesPage,
|
||||||
EmojisStickersPage,
|
EmojisStickersPage,
|
||||||
DeveloperToolsPage,
|
|
||||||
AboutPage,
|
AboutPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +54,6 @@ export const SETTINGS_PAGE_PARAM = {
|
||||||
notifications: SettingsPages.NotificationPage,
|
notifications: SettingsPages.NotificationPage,
|
||||||
devices: SettingsPages.DevicesPage,
|
devices: SettingsPages.DevicesPage,
|
||||||
emojis: SettingsPages.EmojisStickersPage,
|
emojis: SettingsPages.EmojisStickersPage,
|
||||||
'developer-tools': SettingsPages.DeveloperToolsPage,
|
|
||||||
about: SettingsPages.AboutPage,
|
about: SettingsPages.AboutPage,
|
||||||
} as const satisfies Record<string, SettingsPages>;
|
} as const satisfies Record<string, SettingsPages>;
|
||||||
export const SETTINGS_PARAM_DEVICES = 'devices';
|
export const SETTINGS_PARAM_DEVICES = 'devices';
|
||||||
|
|
@ -87,11 +84,6 @@ const SETTINGS_MENU_ITEMS: SettingsMenuItem[] = [
|
||||||
nameKey: 'Settings.menu_emojis_stickers',
|
nameKey: 'Settings.menu_emojis_stickers',
|
||||||
icon: Icons.Smile,
|
icon: Icons.Smile,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
page: SettingsPages.DeveloperToolsPage,
|
|
||||||
nameKey: 'Settings.menu_developer_tools',
|
|
||||||
icon: Icons.Terminal,
|
|
||||||
},
|
|
||||||
{ page: SettingsPages.AboutPage, nameKey: 'Settings.menu_about', icon: Icons.Info },
|
{ 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;
|
const target = e.target as HTMLElement | null;
|
||||||
if (
|
if (
|
||||||
target &&
|
target &&
|
||||||
(target.tagName === 'INPUT' ||
|
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
||||||
target.tagName === 'TEXTAREA' ||
|
|
||||||
target.isContentEditable)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -301,9 +291,6 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
||||||
{activePage === SettingsPages.EmojisStickersPage && (
|
{activePage === SettingsPages.EmojisStickersPage && (
|
||||||
<EmojisStickers requestClose={handlePageRequestClose} />
|
<EmojisStickers requestClose={handlePageRequestClose} />
|
||||||
)}
|
)}
|
||||||
{activePage === SettingsPages.DeveloperToolsPage && (
|
|
||||||
<DeveloperTools requestClose={handlePageRequestClose} />
|
|
||||||
)}
|
|
||||||
{activePage === SettingsPages.AboutPage && <About requestClose={handlePageRequestClose} />}
|
{activePage === SettingsPages.AboutPage && <About requestClose={handlePageRequestClose} />}
|
||||||
</PageRoot>
|
</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;
|
const target = e.target as HTMLElement | null;
|
||||||
if (
|
if (
|
||||||
target &&
|
target &&
|
||||||
(target.tagName === 'INPUT' ||
|
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
||||||
target.tagName === 'TEXTAREA' ||
|
|
||||||
target.isContentEditable)
|
|
||||||
) {
|
) {
|
||||||
return;
|
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 React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Box, Text, IconButton, Icon, Icons, Scroll, Button, config, toRem } from 'folds';
|
import { Box, Text, Button, Chip, color, config, toRem } 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 { SettingTile } from '../../../components/setting-tile';
|
||||||
import VojoSVG from '../../../../../public/res/svg/vojo.svg';
|
import VojoSVG from '../../../../../public/res/svg/vojo.svg';
|
||||||
import { clearCacheAndReload } from '../../../../client/initMatrix';
|
import { clearCacheAndReload } from '../../../../client/initMatrix';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
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 = {
|
type AboutProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
|
@ -15,232 +16,122 @@ type AboutProps = {
|
||||||
export function About({ requestClose }: AboutProps) {
|
export function About({ requestClose }: AboutProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const domain = mx.getDomain() ?? 'vojo.chat';
|
||||||
|
const version = __APP_VERSION__;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page variant="SurfaceVariant">
|
<SettingsPage title={t('Settings.about_title')} requestClose={requestClose}>
|
||||||
<PageHeader outlined={false}>
|
{/* Brand hero — centered logo tile, wordmark, tagline, and a copyable
|
||||||
<Box grow="Yes" gap="200">
|
Fleet-tinted version chip. Deliberately not the stock logo-left row. */}
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<Box direction="Column" alignItems="Center" gap="300" style={{ paddingTop: toRem(8) }}>
|
||||||
<Text size="H3" truncate>
|
<Box
|
||||||
{t('Settings.about_title')}
|
alignItems="Center"
|
||||||
</Text>
|
justifyContent="Center"
|
||||||
</Box>
|
shrink="No"
|
||||||
<Box shrink="No">
|
style={{
|
||||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
width: toRem(88),
|
||||||
<Icon src={Icons.Cross} />
|
height: toRem(88),
|
||||||
</IconButton>
|
borderRadius: toRem(22),
|
||||||
</Box>
|
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>
|
</Box>
|
||||||
</PageHeader>
|
<Box direction="Column" alignItems="Center" gap="100">
|
||||||
<Box grow="Yes">
|
<Text size="H2">Vojo</Text>
|
||||||
<Scroll hideTrack visibility="Hover">
|
<Text size="T300" priority="400" align="Center">
|
||||||
<PageContent style={{ paddingBottom: '1rem' }}>
|
{t('Settings.about_tagline')}
|
||||||
<Box direction="Column" gap="700">
|
</Text>
|
||||||
<Box gap="400">
|
</Box>
|
||||||
<Box shrink="No">
|
<Chip
|
||||||
<img
|
variant="Primary"
|
||||||
style={{ width: toRem(60), height: toRem(60) }}
|
fill="Soft"
|
||||||
src={VojoSVG}
|
radii="Pill"
|
||||||
alt="Vojo logo"
|
onClick={() => copyToClipboard(version)}
|
||||||
/>
|
aria-label={t('Settings.copy')}
|
||||||
</Box>
|
>
|
||||||
<Box direction="Column" gap="300">
|
<Text as="span" size="T200" className={Mono}>
|
||||||
<Box direction="Column" gap="100">
|
{version}
|
||||||
<Box gap="100" alignItems="End">
|
</Text>
|
||||||
<Text size="H3">Vojo</Text>
|
</Chip>
|
||||||
<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>
|
</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 React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { MatrixId } from './MatrixId';
|
||||||
import { Profile } from './Profile';
|
import { Profile } from './Profile';
|
||||||
import { ContactInformation } from './ContactInfo';
|
import { ContactInformation } from './ContactInfo';
|
||||||
import { IgnoredUserList } from './IgnoredUserList';
|
import { IgnoredUserList } from './IgnoredUserList';
|
||||||
|
import { SettingsPage } from '../SettingsPage';
|
||||||
|
|
||||||
type AccountProps = {
|
type AccountProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
|
@ -13,33 +12,11 @@ type AccountProps = {
|
||||||
export function Account({ requestClose }: AccountProps) {
|
export function Account({ requestClose }: AccountProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Page variant="SurfaceVariant">
|
<SettingsPage title={t('Settings.account_title')} requestClose={requestClose}>
|
||||||
<PageHeader outlined={false}>
|
<Profile />
|
||||||
<Box grow="Yes" gap="200">
|
<MatrixId />
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<ContactInformation />
|
||||||
<Text size="H3" truncate>
|
<IgnoredUserList />
|
||||||
{t('Settings.account_title')}
|
</SettingsPage>
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Box, Text, Chip } from 'folds';
|
import { Box, Text, Chip } from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
|
import { Mono } from '../styles.css';
|
||||||
|
|
||||||
export function ContactInformation() {
|
export function ContactInformation() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -23,28 +23,21 @@ export function ContactInformation() {
|
||||||
}, [loadThreePIds]);
|
}, [loadThreePIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.contact_info')}>
|
||||||
<Text size="L400">{t('Settings.contact_info')}</Text>
|
<SettingTile
|
||||||
<SequenceCard
|
title={t('Settings.email_address')}
|
||||||
className={SequenceCardStyle}
|
description={t('Settings.email_address_desc')}
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
>
|
||||||
<SettingTile
|
<Box wrap="Wrap" gap="200">
|
||||||
title={t('Settings.email_address')}
|
{emailIds?.map((email) => (
|
||||||
description={t('Settings.email_address_desc')}
|
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
|
||||||
>
|
<Text size="T200" className={Mono}>
|
||||||
<Box>
|
{email.address}
|
||||||
{emailIds?.map((email) => (
|
</Text>
|
||||||
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
|
</Chip>
|
||||||
<Text size="T200">{email.address}</Text>
|
))}
|
||||||
</Chip>
|
</Box>
|
||||||
))}
|
</SettingTile>
|
||||||
</Box>
|
</SettingsSection>
|
||||||
{/* <Input defaultValue="" variant="Secondary" radii="300" /> */}
|
|
||||||
</SettingTile>
|
|
||||||
</SequenceCard>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { ChangeEventHandler, FormEventHandler, useCallback, useState } from 'react';
|
import React, { ChangeEventHandler, FormEventHandler, useCallback, useState } from 'react';
|
||||||
import { Box, Button, Chip, Icon, IconButton, Icons, Input, Spinner, Text, config } from 'folds';
|
import { Box, Button, Chip, Icon, IconButton, Icons, Input, Spinner, Text, config } from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { isUserId } from '../../../utils/matrix';
|
import { isUserId } from '../../../utils/matrix';
|
||||||
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||||
import { useAlive } from '../../../hooks/useAlive';
|
import { useAlive } from '../../../hooks/useAlive';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
|
import { Mono } from '../styles.css';
|
||||||
|
|
||||||
function IgnoreUserInput({ userList }: { userList: string[] }) {
|
function IgnoreUserInput({ userList }: { userList: string[] }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -123,7 +123,7 @@ function IgnoredUserChip({ userId, userList }: { userId: string; userList: strin
|
||||||
onClick={handleUnignore}
|
onClick={handleUnignore}
|
||||||
disabled={unIgnoring}
|
disabled={unIgnoring}
|
||||||
>
|
>
|
||||||
<Text size="T200" truncate>
|
<Text size="T200" truncate className={Mono}>
|
||||||
{userId}
|
{userId}
|
||||||
</Text>
|
</Text>
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|
@ -135,32 +135,19 @@ export function IgnoredUserList() {
|
||||||
const ignoredUsers = useIgnoredUsers();
|
const ignoredUsers = useIgnoredUsers();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.blocked_users')}>
|
||||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
<SettingTile title={t('Settings.select_user')} description={t('Settings.select_user_desc')}>
|
||||||
<Text size="L400">{t('Settings.blocked_users')}</Text>
|
<Box direction="Column" gap="300">
|
||||||
</Box>
|
<IgnoreUserInput userList={ignoredUsers} />
|
||||||
<SequenceCard
|
{ignoredUsers.length > 0 && (
|
||||||
className={SequenceCardStyle}
|
<Box wrap="Wrap" gap="200">
|
||||||
variant="Background"
|
{ignoredUsers.map((userId) => (
|
||||||
direction="Column"
|
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />
|
||||||
gap="400"
|
))}
|
||||||
>
|
</Box>
|
||||||
<SettingTile title={t('Settings.select_user')} description={t('Settings.select_user_desc')}>
|
)}
|
||||||
<Box direction="Column" gap="300">
|
</Box>
|
||||||
<IgnoreUserInput userList={ignoredUsers} />
|
</SettingTile>
|
||||||
{ignoredUsers.length > 0 && (
|
</SettingsSection>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Text, Chip } from 'folds';
|
import { Text, Chip } from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { copyToClipboard } from '../../../utils/dom';
|
import { copyToClipboard } from '../../../utils/dom';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
|
import { Mono } from '../styles.css';
|
||||||
|
|
||||||
export function MatrixId() {
|
export function MatrixId() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const userId = useAuthedUserId();
|
const userId = useAuthedUserId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.matrix_id')}>
|
||||||
<Text size="L400">{t('Settings.matrix_id')}</Text>
|
<SettingTile
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
title={
|
||||||
<SettingTile
|
<Text as="span" size="T300" className={Mono}>
|
||||||
title={userId}
|
{userId}
|
||||||
after={
|
</Text>
|
||||||
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
|
}
|
||||||
<Text size="T200">{t('Settings.copy')}</Text>
|
after={
|
||||||
</Chip>
|
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
|
||||||
}
|
<Text size="T200">{t('Settings.copy')}</Text>
|
||||||
/>
|
</Chip>
|
||||||
</SequenceCard>
|
}
|
||||||
</Box>
|
/>
|
||||||
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,8 @@ import {
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
||||||
|
|
@ -92,11 +91,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={
|
title={t('Settings.avatar')}
|
||||||
<Text as="span" size="L400">
|
|
||||||
{t('Settings.avatar')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
after={
|
after={
|
||||||
<Avatar size="500" radii="300">
|
<Avatar size="500" radii="300">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
|
|
@ -249,13 +244,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
||||||
|
|
||||||
const hasChanges = displayName !== defaultDisplayName;
|
const hasChanges = displayName !== defaultDisplayName;
|
||||||
return (
|
return (
|
||||||
<SettingTile
|
<SettingTile title={t('Settings.display_name')}>
|
||||||
title={
|
|
||||||
<Text as="span" size="L400">
|
|
||||||
{t('Settings.display_name')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Box direction="Column" grow="Yes" gap="100">
|
<Box direction="Column" grow="Yes" gap="100">
|
||||||
<Box
|
<Box
|
||||||
as="form"
|
as="form"
|
||||||
|
|
@ -313,12 +302,9 @@ export function Profile() {
|
||||||
const profile = useUserProfile(userId);
|
const profile = useUserProfile(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.profile')}>
|
||||||
<Text size="L400">{t('Settings.profile')}</Text>
|
<ProfileAvatar userId={userId} profile={profile} />
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
<ProfileDisplayName userId={userId} profile={profile} />
|
||||||
<ProfileAvatar userId={userId} profile={profile} />
|
</SettingsSection>
|
||||||
<ProfileDisplayName userId={userId} profile={profile} />
|
|
||||||
</SequenceCard>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { BreakWord } from '../../../styles/Text.css';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { Mono, SettingRow } from '../styles.css';
|
||||||
import { LogoutDialog } from '../../../components/LogoutDialog';
|
import { LogoutDialog } from '../../../components/LogoutDialog';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
|
||||||
export function DeviceTilePlaceholder() {
|
export function DeviceTilePlaceholder() {
|
||||||
return (
|
return (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={SequenceCardStyle}
|
className={SettingRow}
|
||||||
style={{ height: toRem(66) }}
|
style={{ height: toRem(66) }}
|
||||||
variant="Background"
|
variant="Background"
|
||||||
|
outlined
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
/>
|
/>
|
||||||
|
|
@ -52,8 +53,7 @@ function DeviceActiveTime({ ts }: { ts: number }) {
|
||||||
<>
|
<>
|
||||||
{today(ts) && t('Settings.today')}
|
{today(ts) && t('Settings.today')}
|
||||||
{yesterday(ts) && t('Settings.yesterday')}
|
{yesterday(ts) && t('Settings.yesterday')}
|
||||||
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)}{' '}
|
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
|
||||||
{timeHourMinute(ts)}
|
|
||||||
</>
|
</>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
@ -66,13 +66,13 @@ function DeviceDetails({ device }: { device: IMyDevice }) {
|
||||||
{typeof device.device_id === 'string' && (
|
{typeof device.device_id === 'string' && (
|
||||||
<Text className={BreakWord} size="T200" priority="300">
|
<Text className={BreakWord} size="T200" priority="300">
|
||||||
{t('Settings.device_id')}
|
{t('Settings.device_id')}
|
||||||
<i>{device.device_id}</i>
|
<span className={Mono}>{device.device_id}</span>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{typeof device.last_seen_ip === 'string' && (
|
{typeof device.last_seen_ip === 'string' && (
|
||||||
<Text className={BreakWord} size="T200" priority="300">
|
<Text className={BreakWord} size="T200" priority="300">
|
||||||
{t('Settings.ip_address')}
|
{t('Settings.ip_address')}
|
||||||
<i>{device.last_seen_ip}</i>
|
<span className={Mono}>{device.last_seen_ip}</span>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -100,9 +100,9 @@ export function DeviceKeyDetails({ crypto }: DeviceKeyDetailsProps) {
|
||||||
return (
|
return (
|
||||||
<Text className={BreakWord} size="T200" priority="300">
|
<Text className={BreakWord} size="T200" priority="300">
|
||||||
{t('Settings.device_key')}
|
{t('Settings.device_key')}
|
||||||
<i>
|
<span className={Mono}>
|
||||||
{keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : t('Settings.loading')}
|
{keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : t('Settings.loading')}
|
||||||
</i>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
|
import { Box, Text } from 'folds';
|
||||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SectionLabel, SettingRow } from '../styles.css';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsPage } from '../SettingsPage';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList';
|
import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { LocalBackup } from './LocalBackup';
|
import { LocalBackup } from './LocalBackup';
|
||||||
|
|
@ -67,101 +68,74 @@ export function Devices({ requestClose }: DevicesProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page variant="SurfaceVariant">
|
<SettingsPage title={t('Settings.devices_title')} requestClose={requestClose}>
|
||||||
<PageHeader outlined={false}>
|
<SettingsSection label={t('Settings.security')}>
|
||||||
<Box grow="Yes" gap="200">
|
<SettingTile
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
title={t('Settings.device_verification')}
|
||||||
<Text size="H3" truncate>
|
description={t('Settings.device_verification_desc')}
|
||||||
{t('Settings.devices_title')}
|
after={
|
||||||
</Text>
|
<>
|
||||||
</Box>
|
<EnableVerification visible={!crossSigningActive} />
|
||||||
<Box shrink="No">
|
{crossSigningActive && (
|
||||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
<Box gap="200" alignItems="Center">
|
||||||
<Icon src={Icons.Cross} />
|
<VerificationStatusBadge
|
||||||
</IconButton>
|
verificationStatus={verificationStatus}
|
||||||
</Box>
|
otherUnverifiedCount={unverifiedDeviceCount}
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
<DeviceVerificationOptions />
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Column" gap="100">
|
)}
|
||||||
<Text size="L400">{t('Settings.current')}</Text>
|
</>
|
||||||
{currentDevice ? (
|
}
|
||||||
<SequenceCard
|
/>
|
||||||
className={SequenceCardStyle}
|
</SettingsSection>
|
||||||
variant="Background"
|
<Box direction="Column" gap="200">
|
||||||
direction="Column"
|
<Text as="span" className={SectionLabel}>
|
||||||
gap="400"
|
{t('Settings.current')}
|
||||||
>
|
</Text>
|
||||||
<DeviceTile
|
{currentDevice ? (
|
||||||
device={currentDevice}
|
<SequenceCard
|
||||||
refreshDeviceList={refreshDeviceList}
|
className={SettingRow}
|
||||||
options={<DeviceLogoutBtn />}
|
variant="Background"
|
||||||
>
|
outlined
|
||||||
{crypto && <DeviceKeyDetails crypto={crypto} />}
|
direction="Column"
|
||||||
</DeviceTile>
|
gap="400"
|
||||||
{crossSigningActive &&
|
>
|
||||||
verificationStatus === VerificationStatus.Unverified &&
|
<DeviceTile
|
||||||
defaultSecretStorageKeyId &&
|
device={currentDevice}
|
||||||
defaultSecretStorageKeyContent && (
|
refreshDeviceList={refreshDeviceList}
|
||||||
<VerifyCurrentDeviceTile
|
options={<DeviceLogoutBtn />}
|
||||||
secretStorageKeyId={defaultSecretStorageKeyId}
|
>
|
||||||
secretStorageKeyContent={defaultSecretStorageKeyContent}
|
{crypto && <DeviceKeyDetails crypto={crypto} />}
|
||||||
/>
|
</DeviceTile>
|
||||||
)}
|
{crossSigningActive &&
|
||||||
{crypto && verificationStatus === VerificationStatus.Verified && (
|
verificationStatus === VerificationStatus.Unverified &&
|
||||||
<BackupRestoreTile crypto={crypto} />
|
defaultSecretStorageKeyId &&
|
||||||
)}
|
defaultSecretStorageKeyContent && (
|
||||||
</SequenceCard>
|
<VerifyCurrentDeviceTile
|
||||||
) : (
|
secretStorageKeyId={defaultSecretStorageKeyId}
|
||||||
<DeviceTilePlaceholder />
|
secretStorageKeyContent={defaultSecretStorageKeyContent}
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{devices === undefined && <DevicesPlaceholder />}
|
|
||||||
{otherDevices && (
|
|
||||||
<OtherDevices
|
|
||||||
devices={otherDevices}
|
|
||||||
refreshDeviceList={refreshDeviceList}
|
|
||||||
showVerification={
|
|
||||||
crossSigningActive && verificationStatus === VerificationStatus.Verified
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LocalBackup />
|
{crypto && verificationStatus === VerificationStatus.Verified && (
|
||||||
</Box>
|
<BackupRestoreTile crypto={crypto} />
|
||||||
</PageContent>
|
)}
|
||||||
</Scroll>
|
</SequenceCard>
|
||||||
|
) : (
|
||||||
|
<DeviceTilePlaceholder />
|
||||||
|
)}
|
||||||
</Box>
|
</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 { useTranslation } from 'react-i18next';
|
||||||
import { Box, Button, color, Icon, Icons, Spinner, Text, toRem } from 'folds';
|
import { Box, Button, color, Icon, Icons, Spinner, Text, toRem } from 'folds';
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { PasswordInput } from '../../../components/password-input';
|
import { PasswordInput } from '../../../components/password-input';
|
||||||
import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
|
import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
|
@ -308,24 +307,9 @@ function ImportKeysTile() {
|
||||||
export function LocalBackup() {
|
export function LocalBackup() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.local_backup')}>
|
||||||
<Text size="L400">{t('Settings.local_backup')}</Text>
|
<ExportKeysTile />
|
||||||
<SequenceCard
|
<ImportKeysTile />
|
||||||
className={SequenceCardStyle}
|
</SettingsSection>
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<ExportKeysTile />
|
|
||||||
</SequenceCard>
|
|
||||||
<SequenceCard
|
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<ImportKeysTile />
|
|
||||||
</SequenceCard>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { Box, Button, config, Menu, Spinner, Text } from 'folds';
|
import { Box, Button, config, Menu, Spinner, Text } from 'folds';
|
||||||
import { AuthDict, IMyDevice, MatrixError } from 'matrix-js-sdk';
|
import { AuthDict, IMyDevice, MatrixError } from 'matrix-js-sdk';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SectionLabel, SettingRow } from '../styles.css';
|
||||||
import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA';
|
import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA';
|
||||||
import { DeviceDeleteBtn, DeviceTile } from './DeviceTile';
|
import { DeviceDeleteBtn, DeviceTile } from './DeviceTile';
|
||||||
import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback';
|
import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback';
|
||||||
|
|
@ -104,83 +104,91 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
|
||||||
|
|
||||||
return devices.length > 0 ? (
|
return devices.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="200">
|
||||||
<Text size="L400">{t('Settings.others')}</Text>
|
<Text as="span" className={SectionLabel}>
|
||||||
{authMetadata && (
|
{t('Settings.others')}
|
||||||
<SequenceCard
|
</Text>
|
||||||
className={SequenceCardStyle}
|
<Box direction="Column">
|
||||||
variant="Background"
|
{authMetadata && (
|
||||||
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) => (
|
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
key={device.device_id}
|
className={SettingRow}
|
||||||
className={SequenceCardStyle}
|
variant="Background"
|
||||||
variant={deleted.has(device.device_id) ? 'Critical' : 'SurfaceVariant'}
|
outlined
|
||||||
|
mergeBorder
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
>
|
>
|
||||||
<DeviceTile
|
<SettingTile
|
||||||
device={device}
|
title={t('Settings.device_dashboard')}
|
||||||
deleted={deleted.has(device.device_id)}
|
description={t('Settings.device_dashboard_desc')}
|
||||||
refreshDeviceList={refreshDeviceList}
|
after={
|
||||||
disabled={deleting}
|
<Button
|
||||||
options={
|
size="300"
|
||||||
authMetadata ? (
|
variant="Secondary"
|
||||||
<DeviceDeleteBtn
|
fill="Soft"
|
||||||
deviceId={device.device_id}
|
radii="300"
|
||||||
deleted={false}
|
outlined
|
||||||
onDeleteToggle={handleDeleteOIDC}
|
onClick={handleDashboardOIDC}
|
||||||
/>
|
>
|
||||||
) : (
|
<Text size="B300">{t('Settings.open')}</Text>
|
||||||
<DeviceDeleteBtn
|
</Button>
|
||||||
deviceId={device.device_id}
|
|
||||||
deleted={deleted.has(device.device_id)}
|
|
||||||
onDeleteToggle={handleToggleDelete}
|
|
||||||
disabled={deleting}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{showVerification && crypto && (
|
</SequenceCard>
|
||||||
<DeviceVerificationStatus
|
)}
|
||||||
crypto={crypto}
|
{devices
|
||||||
userId={mx.getSafeUserId()}
|
.sort((d1, d2) => {
|
||||||
deviceId={device.device_id}
|
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
|
||||||
>
|
return d1.last_seen_ts < d2.last_seen_ts ? 1 : -1;
|
||||||
{(status) =>
|
})
|
||||||
status === VerificationStatus.Unverified && (
|
.map((device) => (
|
||||||
<VerifyOtherDeviceTile crypto={crypto} deviceId={device.device_id} />
|
<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>
|
/>
|
||||||
)}
|
{showVerification && crypto && (
|
||||||
</SequenceCard>
|
<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>
|
</Box>
|
||||||
{deleted.size > 0 && (
|
{deleted.size > 0 && (
|
||||||
<Menu
|
<Menu
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { GlobalPacks } from './GlobalPacks';
|
||||||
import { UserPack } from './UserPack';
|
import { UserPack } from './UserPack';
|
||||||
import { ImagePack } from '../../../plugins/custom-emoji';
|
import { ImagePack } from '../../../plugins/custom-emoji';
|
||||||
import { ImagePackView } from '../../../components/image-pack-view';
|
import { ImagePackView } from '../../../components/image-pack-view';
|
||||||
|
import { SettingsPage } from '../SettingsPage';
|
||||||
|
|
||||||
type EmojisStickersProps = {
|
type EmojisStickersProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
|
@ -23,31 +22,9 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page variant="SurfaceVariant">
|
<SettingsPage title={t('Settings.emojis_stickers_title')} requestClose={requestClose}>
|
||||||
<PageHeader outlined={false}>
|
<UserPack onViewPack={setImagePack} />
|
||||||
<Box grow="Yes" gap="200">
|
<GlobalPacks onViewPack={setImagePack} />
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
</SettingsPage>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import FocusTrap from 'focus-trap-react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useGlobalImagePacks, useRoomsImagePacks } from '../../../hooks/useImagePacks';
|
import { useGlobalImagePacks, useRoomsImagePacks } from '../../../hooks/useImagePacks';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SectionLabel, SettingRow } from '../styles.css';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
|
|
@ -182,7 +182,7 @@ function GlobalPackSelector({
|
||||||
return (
|
return (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
key={pack.id}
|
key={pack.id}
|
||||||
className={SequenceCardStyle}
|
className={SettingRow}
|
||||||
variant={added ? 'Success' : 'SurfaceVariant'}
|
variant={added ? 'Success' : 'SurfaceVariant'}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
|
|
@ -220,7 +220,7 @@ function GlobalPackSelector({
|
||||||
|
|
||||||
{roomToPacks.size === 0 && (
|
{roomToPacks.size === 0 && (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={SequenceCardStyle}
|
className={SettingRow}
|
||||||
variant="Background"
|
variant="Background"
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
|
|
@ -357,8 +357,10 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
|
||||||
return (
|
return (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
key={pack.id}
|
key={pack.id}
|
||||||
className={SequenceCardStyle}
|
className={SettingRow}
|
||||||
variant={removed ? 'Critical' : 'SurfaceVariant'}
|
variant={removed ? 'Critical' : 'Background'}
|
||||||
|
outlined
|
||||||
|
mergeBorder={!removed}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
>
|
>
|
||||||
|
|
@ -424,71 +426,77 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="200">
|
||||||
<Text size="L400">{t('Settings.favorite_packs')}</Text>
|
<Text as="span" className={SectionLabel}>
|
||||||
<SequenceCard
|
{t('Settings.favorite_packs')}
|
||||||
className={SequenceCardStyle}
|
</Text>
|
||||||
variant="Background"
|
<Box direction="Column">
|
||||||
direction="Column"
|
<SequenceCard
|
||||||
gap="400"
|
className={SettingRow}
|
||||||
>
|
variant="Background"
|
||||||
<SettingTile
|
outlined
|
||||||
title={t('Settings.select_pack')}
|
mergeBorder
|
||||||
description={t('Settings.select_pack_desc')}
|
direction="Column"
|
||||||
after={
|
gap="400"
|
||||||
<>
|
>
|
||||||
<Button
|
<SettingTile
|
||||||
onClick={handleSelectMenu}
|
title={t('Settings.select_pack')}
|
||||||
variant="Secondary"
|
description={t('Settings.select_pack_desc')}
|
||||||
fill="Soft"
|
after={
|
||||||
size="300"
|
<>
|
||||||
radii="300"
|
<Button
|
||||||
outlined
|
onClick={handleSelectMenu}
|
||||||
>
|
variant="Secondary"
|
||||||
<Text size="B300">{t('Settings.select')}</Text>
|
fill="Soft"
|
||||||
</Button>
|
size="300"
|
||||||
<PopOut
|
radii="300"
|
||||||
anchor={menuCords}
|
outlined
|
||||||
position="Bottom"
|
>
|
||||||
align="End"
|
<Text size="B300">{t('Settings.select')}</Text>
|
||||||
content={
|
</Button>
|
||||||
<FocusTrap
|
<PopOut
|
||||||
focusTrapOptions={{
|
anchor={menuCords}
|
||||||
initialFocus: false,
|
position="Bottom"
|
||||||
onDeactivate: () => setMenuCords(undefined),
|
align="End"
|
||||||
clickOutsideDeactivates: true,
|
content={
|
||||||
isKeyForward: (evt: KeyboardEvent) =>
|
<FocusTrap
|
||||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
focusTrapOptions={{
|
||||||
isKeyBackward: (evt: KeyboardEvent) =>
|
initialFocus: false,
|
||||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
onDeactivate: () => setMenuCords(undefined),
|
||||||
escapeDeactivates: stopPropagation,
|
clickOutsideDeactivates: true,
|
||||||
}}
|
isKeyForward: (evt: KeyboardEvent) =>
|
||||||
>
|
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||||
<Menu
|
isKeyBackward: (evt: KeyboardEvent) =>
|
||||||
style={{
|
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||||
display: 'flex',
|
escapeDeactivates: stopPropagation,
|
||||||
maxWidth: toRem(400),
|
|
||||||
width: '100vw',
|
|
||||||
maxHeight: toRem(500),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GlobalPackSelector
|
<Menu
|
||||||
packs={unselectedGlobalPacks}
|
style={{
|
||||||
useAuthentication={useAuthentication}
|
display: 'flex',
|
||||||
onSelect={handleSelected}
|
maxWidth: toRem(400),
|
||||||
/>
|
width: '100vw',
|
||||||
</Menu>
|
maxHeight: toRem(500),
|
||||||
</FocusTrap>
|
}}
|
||||||
}
|
>
|
||||||
/>
|
<GlobalPackSelector
|
||||||
</>
|
packs={unselectedGlobalPacks}
|
||||||
}
|
useAuthentication={useAuthentication}
|
||||||
/>
|
onSelect={handleSelected}
|
||||||
</SequenceCard>
|
/>
|
||||||
{globalPacks.map(renderPack)}
|
</Menu>
|
||||||
{nonGlobalPacks
|
</FocusTrap>
|
||||||
.filter((pack) => !!selectedPacks.find((addr) => packAddressEqual(pack.address, addr)))
|
}
|
||||||
.map(renderPack)}
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SequenceCard>
|
||||||
|
{globalPacks.map(renderPack)}
|
||||||
|
{nonGlobalPacks
|
||||||
|
.filter((pack) => !!selectedPacks.find((addr) => packAddressEqual(pack.address, addr)))
|
||||||
|
.map(renderPack)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<Menu
|
<Menu
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { useUserImagePack } from '../../../hooks/useImagePacks';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { ImagePack, ImageUsage } from '../../../plugins/custom-emoji';
|
import { ImagePack, ImageUsage } from '../../../plugins/custom-emoji';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
|
|
@ -32,37 +31,34 @@ export function UserPack({ onViewPack }: UserPackProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.default_pack')}>
|
||||||
<Text size="L400">{t('Settings.default_pack')}</Text>
|
<SettingTile
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
title={userPack?.meta.name ?? t('Settings.unknown')}
|
||||||
<SettingTile
|
description={userPack?.meta.attribution}
|
||||||
title={userPack?.meta.name ?? t('Settings.unknown')}
|
before={
|
||||||
description={userPack?.meta.attribution}
|
<Avatar size="300" radii="300">
|
||||||
before={
|
{avatarUrl ? (
|
||||||
<Avatar size="300" radii="300">
|
<AvatarImage style={{ objectFit: 'contain' }} src={avatarUrl} />
|
||||||
{avatarUrl ? (
|
) : (
|
||||||
<AvatarImage style={{ objectFit: 'contain' }} src={avatarUrl} />
|
<AvatarFallback>
|
||||||
) : (
|
<Icon size="400" src={Icons.Sticker} filled />
|
||||||
<AvatarFallback>
|
</AvatarFallback>
|
||||||
<Icon size="400" src={Icons.Sticker} filled />
|
)}
|
||||||
</AvatarFallback>
|
</Avatar>
|
||||||
)}
|
}
|
||||||
</Avatar>
|
after={
|
||||||
}
|
<Button
|
||||||
after={
|
variant="Secondary"
|
||||||
<Button
|
fill="Soft"
|
||||||
variant="Secondary"
|
size="300"
|
||||||
fill="Soft"
|
radii="300"
|
||||||
size="300"
|
outlined
|
||||||
radii="300"
|
onClick={handleView}
|
||||||
outlined
|
>
|
||||||
onClick={handleView}
|
<Text size="B300">{t('Settings.view')}</Text>
|
||||||
>
|
</Button>
|
||||||
<Text size="B300">{t('Settings.view')}</Text>
|
}
|
||||||
</Button>
|
/>
|
||||||
}
|
</SettingsSection>
|
||||||
/>
|
|
||||||
</SequenceCard>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,12 @@ import {
|
||||||
Button,
|
Button,
|
||||||
config,
|
config,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
|
||||||
Icons,
|
Icons,
|
||||||
Input,
|
Input,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
PopOut,
|
PopOut,
|
||||||
RectCords,
|
RectCords,
|
||||||
Scroll,
|
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
toRem,
|
toRem,
|
||||||
|
|
@ -25,8 +23,6 @@ import {
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { useSetting } from '../../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../../state/settings';
|
import { settingsAtom } from '../../../state/settings';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
|
@ -34,7 +30,8 @@ import { KeySymbol } from '../../../utils/key-symbol';
|
||||||
import { isMacOS } from '../../../utils/user-agent';
|
import { isMacOS } from '../../../utils/user-agent';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { DarkTheme, LightTheme } from '../../../hooks/useTheme';
|
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;
|
type ThemeChoice = 'system' | typeof LightTheme.id | typeof DarkTheme.id;
|
||||||
|
|
||||||
|
|
@ -52,10 +49,7 @@ function ThemeSelect() {
|
||||||
choice = LightTheme.id;
|
choice = LightTheme.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const choices = useMemo<ThemeChoice[]>(
|
const choices = useMemo<ThemeChoice[]>(() => ['system', LightTheme.id, DarkTheme.id], []);
|
||||||
() => ['system', LightTheme.id, DarkTheme.id],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const choiceLabel = useMemo<Record<ThemeChoice, string>>(
|
const choiceLabel = useMemo<Record<ThemeChoice, string>>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -184,34 +178,17 @@ function PageZoomInput() {
|
||||||
|
|
||||||
function Appearance() {
|
function Appearance() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
|
||||||
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.appearance')}>
|
||||||
<Text size="L400">{t('Settings.appearance')}</Text>
|
<SettingTile title={t('Settings.theme')} after={<ThemeSelect />} />
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
<SettingTile
|
||||||
<SettingTile title={t('Settings.theme')} after={<ThemeSelect />} />
|
title={t('Settings.twitter_emoji')}
|
||||||
</SequenceCard>
|
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
|
||||||
|
/>
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
<SettingTile title={t('Settings.page_zoom')} after={<PageZoomInput />} />
|
||||||
<SettingTile
|
</SettingsSection>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,31 +199,24 @@ function Editor() {
|
||||||
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
|
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.editor')}>
|
||||||
<Text size="L400">{t('Settings.editor')}</Text>
|
<SettingTile
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
title={t('Settings.enter_newline')}
|
||||||
<SettingTile
|
description={t('Settings.enter_newline_desc', {
|
||||||
title={t('Settings.enter_newline')}
|
key: isMacOS() ? KeySymbol.Command : 'Ctrl',
|
||||||
description={t('Settings.enter_newline_desc', {
|
})}
|
||||||
key: isMacOS() ? KeySymbol.Command : 'Ctrl',
|
after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
|
||||||
})}
|
/>
|
||||||
after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
|
<SettingTile
|
||||||
/>
|
title={t('Settings.markdown')}
|
||||||
</SequenceCard>
|
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('Settings.markdown')}
|
title={t('Settings.hide_activity')}
|
||||||
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
|
description={t('Settings.hide_activity_desc')}
|
||||||
/>
|
after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
|
||||||
</SequenceCard>
|
/>
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
</SettingsSection>
|
||||||
<SettingTile
|
|
||||||
title={t('Settings.hide_activity')}
|
|
||||||
description={t('Settings.hide_activity_desc')}
|
|
||||||
after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
|
|
||||||
/>
|
|
||||||
</SequenceCard>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,69 +232,56 @@ function Messages() {
|
||||||
);
|
);
|
||||||
const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||||
const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
|
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 (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.messages')}>
|
||||||
<Text size="L400">{t('Settings.messages')}</Text>
|
<SettingTile
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
title={t('Settings.hide_service_events')}
|
||||||
<SettingTile
|
description={t('Settings.hide_service_events_desc')}
|
||||||
title={t('Settings.hide_membership')}
|
after={
|
||||||
after={
|
<Switch variant="Primary" value={hideServiceEvents} onChange={setHideServiceEvents} />
|
||||||
<Switch
|
}
|
||||||
variant="Primary"
|
/>
|
||||||
value={hideMembershipEvents}
|
<SettingTile
|
||||||
onChange={setHideMembershipEvents}
|
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
|
||||||
<SettingTile
|
title={t('Settings.url_preview')}
|
||||||
title={t('Settings.hide_profile')}
|
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
|
||||||
after={
|
/>
|
||||||
<Switch
|
</SettingsSection>
|
||||||
variant="Primary"
|
);
|
||||||
value={hideNickAvatarEvents}
|
}
|
||||||
onChange={setHideNickAvatarEvents}
|
|
||||||
/>
|
function Advanced() {
|
||||||
}
|
const { t } = useTranslation();
|
||||||
/>
|
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||||
</SequenceCard>
|
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
|
return (
|
||||||
<SettingTile
|
<SettingsSection label={t('Settings.advanced')}>
|
||||||
title={t('Settings.disable_media_auto_load')}
|
<SettingTile
|
||||||
after={
|
title={t('Settings.developer_mode')}
|
||||||
<Switch
|
description={t('Settings.developer_mode_desc')}
|
||||||
variant="Primary"
|
after={<Switch variant="Primary" value={developerTools} onChange={setDeveloperTools} />}
|
||||||
value={!mediaAutoLoad}
|
/>
|
||||||
onChange={(v) => setMediaAutoLoad(!v)}
|
</SettingsSection>
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,32 +291,11 @@ type GeneralProps = {
|
||||||
export function General({ requestClose }: GeneralProps) {
|
export function General({ requestClose }: GeneralProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Page variant="SurfaceVariant">
|
<SettingsPage title={t('Settings.general_title')} requestClose={requestClose}>
|
||||||
<PageHeader outlined={false}>
|
<Appearance />
|
||||||
<Box grow="Yes" gap="200">
|
<Editor />
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<Messages />
|
||||||
<Text size="H3" truncate>
|
<Advanced />
|
||||||
{t('Settings.general_title')}
|
</SettingsPage>
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
export * from './Settings';
|
export * from './Settings';
|
||||||
export * from './SettingsScreen';
|
export * from './SettingsScreen';
|
||||||
export * from './MobileSettingsHorseshoe';
|
export * from './MobileSettingsHorseshoe';
|
||||||
|
export * from './SettingsPage';
|
||||||
|
export * from './SettingsSection';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Badge, Box, Text } from 'folds';
|
|
||||||
import { ConditionKind, IPushRules, PushRuleCondition, PushRuleKind, RuleId } from 'matrix-js-sdk';
|
import { ConditionKind, IPushRules, PushRuleCondition, PushRuleKind, RuleId } from 'matrix-js-sdk';
|
||||||
import { useAccountData } from '../../../hooks/useAccountData';
|
import { useAccountData } from '../../../hooks/useAccountData';
|
||||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||||
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
|
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { PushRuleData, usePushRule } from '../../../hooks/usePushRule';
|
import { PushRuleData, usePushRule } from '../../../hooks/usePushRule';
|
||||||
import {
|
import {
|
||||||
getNotificationModeActions,
|
getNotificationModeActions,
|
||||||
|
|
@ -82,73 +80,36 @@ export function AllMessagesNotifications() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.all_messages')}>
|
||||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
<SettingTile
|
||||||
<Text size="L400">{t('Settings.all_messages')}</Text>
|
title={t('Settings.one_to_one')}
|
||||||
<Box gap="100">
|
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.DM} oneToOne />}
|
||||||
<Text size="T200">{t('Settings.badge')}</Text>
|
/>
|
||||||
<Badge radii="300" variant="Secondary" fill="Solid">
|
<SettingTile
|
||||||
<Text size="L400">1</Text>
|
title={t('Settings.one_to_one_encrypted')}
|
||||||
</Badge>
|
after={
|
||||||
</Box>
|
<AllMessagesModeSwitcher
|
||||||
</Box>
|
pushRules={pushRules}
|
||||||
<SequenceCard
|
ruleId={RuleId.EncryptedDM}
|
||||||
className={SequenceCardStyle}
|
encrypted
|
||||||
variant="Background"
|
oneToOne
|
||||||
direction="Column"
|
/>
|
||||||
gap="400"
|
}
|
||||||
>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('Settings.one_to_one')}
|
title={t('Settings.rooms')}
|
||||||
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.DM} oneToOne />}
|
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.Message} />}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
<SettingTile
|
||||||
<SequenceCard
|
title={t('Settings.rooms_encrypted')}
|
||||||
className={SequenceCardStyle}
|
after={
|
||||||
variant="Background"
|
<AllMessagesModeSwitcher
|
||||||
direction="Column"
|
pushRules={pushRules}
|
||||||
gap="400"
|
ruleId={RuleId.EncryptedMessage}
|
||||||
>
|
encrypted
|
||||||
<SettingTile
|
/>
|
||||||
title={t('Settings.one_to_one_encrypted')}
|
}
|
||||||
after={
|
/>
|
||||||
<AllMessagesModeSwitcher
|
</SettingsSection>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react';
|
import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IPushRule, IPushRules, PushRuleKind } from 'matrix-js-sdk';
|
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 { useAccountData } from '../../../hooks/useAccountData';
|
||||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
|
import { Mono } from '../styles.css';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import {
|
import {
|
||||||
getNotificationModeActions,
|
getNotificationModeActions,
|
||||||
|
|
@ -163,44 +163,25 @@ export function KeywordMessagesNotifications() {
|
||||||
}, [pushRules]);
|
}, [pushRules]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.keyword_messages')}>
|
||||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
<SettingTile
|
||||||
<Text size="L400">{t('Settings.keyword_messages')}</Text>
|
title={t('Settings.select_keyword')}
|
||||||
<Box gap="100">
|
description={t('Settings.select_keyword_desc')}
|
||||||
<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
|
<KeywordInput />
|
||||||
title={t('Settings.select_keyword')}
|
</SettingTile>
|
||||||
description={t('Settings.select_keyword_desc')}
|
|
||||||
>
|
|
||||||
<KeywordInput />
|
|
||||||
</SettingTile>
|
|
||||||
</SequenceCard>
|
|
||||||
{keywordPushRules.map((pushRule) => (
|
{keywordPushRules.map((pushRule) => (
|
||||||
<SequenceCard
|
<SettingTile
|
||||||
key={pushRule.rule_id}
|
key={pushRule.rule_id}
|
||||||
className={SequenceCardStyle}
|
title={
|
||||||
variant="Background"
|
<Text as="span" size="T300" className={Mono}>
|
||||||
direction="Column"
|
{`"${pushRule.pattern}"`}
|
||||||
gap="400"
|
</Text>
|
||||||
>
|
}
|
||||||
<SettingTile
|
before={<KeywordCross pushRule={pushRule} />}
|
||||||
title={`"${pushRule.pattern}"`}
|
after={<KeywordModeSwitcher pushRule={pushRule} />}
|
||||||
before={<KeywordCross pushRule={pushRule} />}
|
/>
|
||||||
after={<KeywordModeSwitcher pushRule={pushRule} />}
|
|
||||||
/>
|
|
||||||
</SequenceCard>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { SystemNotification } from './SystemNotification';
|
||||||
import { AllMessagesNotifications } from './AllMessages';
|
import { AllMessagesNotifications } from './AllMessages';
|
||||||
import { SpecialMessagesNotifications } from './SpecialMessages';
|
import { SpecialMessagesNotifications } from './SpecialMessages';
|
||||||
import { KeywordMessagesNotifications } from './KeywordMessages';
|
import { KeywordMessagesNotifications } from './KeywordMessages';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsPage } from '../SettingsPage';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
|
|
||||||
type NotificationsProps = {
|
type NotificationsProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
|
@ -16,44 +14,14 @@ type NotificationsProps = {
|
||||||
export function Notifications({ requestClose }: NotificationsProps) {
|
export function Notifications({ requestClose }: NotificationsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Page variant="SurfaceVariant">
|
<SettingsPage title={t('Settings.notifications_title')} requestClose={requestClose}>
|
||||||
<PageHeader outlined={false}>
|
<SystemNotification />
|
||||||
<Box grow="Yes" gap="200">
|
<AllMessagesNotifications />
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<SpecialMessagesNotifications />
|
||||||
<Text size="H3" truncate>
|
<KeywordMessagesNotifications />
|
||||||
{t('Settings.notifications_title')}
|
<SettingsSection label={t('Settings.block_messages')}>
|
||||||
</Text>
|
<SettingTile description={t('Settings.block_messages_moved')} />
|
||||||
</Box>
|
</SettingsSection>
|
||||||
<Box shrink="No">
|
</SettingsPage>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ConditionKind, IPushRules, PushRuleKind, RuleId } from 'matrix-js-sdk';
|
import { ConditionKind, IPushRules, PushRuleKind, RuleId } from 'matrix-js-sdk';
|
||||||
import { Box, Text, Badge } from 'folds';
|
|
||||||
import { useAccountData } from '../../../hooks/useAccountData';
|
import { useAccountData } from '../../../hooks/useAccountData';
|
||||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { useUserProfile } from '../../../hooks/useUserProfile';
|
import { useUserProfile } from '../../../hooks/useUserProfile';
|
||||||
|
|
@ -124,80 +122,61 @@ export function SpecialMessagesNotifications() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.special_messages')}>
|
||||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
<SettingTile
|
||||||
<Text size="L400">{t('Settings.special_messages')}</Text>
|
title={t('Settings.mention_user_id', { userId })}
|
||||||
<Box gap="100">
|
after={
|
||||||
<Text size="T200">{t('Settings.badge')}</Text>
|
<MentionModeSwitcher
|
||||||
<Badge radii="300" variant="Success" fill="Solid">
|
pushRules={pushRules}
|
||||||
<Text size="L400">1</Text>
|
ruleId={RuleId.IsUserMention}
|
||||||
</Badge>
|
defaultPushRuleData={getDefaultIsUserMention(userId)}
|
||||||
</Box>
|
/>
|
||||||
</Box>
|
}
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('Settings.mention_user_id', { userId })}
|
title={
|
||||||
after={
|
displayName
|
||||||
<MentionModeSwitcher
|
? t('Settings.contains_displayname_value', { displayName })
|
||||||
pushRules={pushRules}
|
: t('Settings.contains_displayname')
|
||||||
ruleId={RuleId.IsUserMention}
|
}
|
||||||
defaultPushRuleData={getDefaultIsUserMention(userId)}
|
after={
|
||||||
/>
|
<MentionModeSwitcher
|
||||||
}
|
pushRules={pushRules}
|
||||||
/>
|
ruleId={RuleId.ContainsDisplayName}
|
||||||
</SequenceCard>
|
defaultPushRuleData={DefaultContainsDisplayName}
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
/>
|
||||||
<SettingTile
|
}
|
||||||
title={
|
/>
|
||||||
displayName
|
<SettingTile
|
||||||
? t('Settings.contains_displayname_value', { displayName })
|
title={t('Settings.contains_username', { username: getMxIdLocalPart(userId) })}
|
||||||
: t('Settings.contains_displayname')
|
after={
|
||||||
}
|
<MentionModeSwitcher
|
||||||
after={
|
pushRules={pushRules}
|
||||||
<MentionModeSwitcher
|
ruleId={RuleId.ContainsUserName}
|
||||||
pushRules={pushRules}
|
defaultPushRuleData={getDefaultContainsUsername(getMxIdLocalPart(userId) ?? userId)}
|
||||||
ruleId={RuleId.ContainsDisplayName}
|
/>
|
||||||
defaultPushRuleData={DefaultContainsDisplayName}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
<SettingTile
|
||||||
/>
|
title={t('Settings.mention_room')}
|
||||||
</SequenceCard>
|
after={
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
<MentionModeSwitcher
|
||||||
<SettingTile
|
pushRules={pushRules}
|
||||||
title={t('Settings.contains_username', { username: getMxIdLocalPart(userId) })}
|
ruleId={RuleId.IsRoomMention}
|
||||||
after={
|
defaultPushRuleData={DefaultIsRoomMention}
|
||||||
<MentionModeSwitcher
|
/>
|
||||||
pushRules={pushRules}
|
}
|
||||||
ruleId={RuleId.ContainsUserName}
|
/>
|
||||||
defaultPushRuleData={getDefaultContainsUsername(getMxIdLocalPart(userId) ?? userId)}
|
<SettingTile
|
||||||
/>
|
title={t('Settings.contains_room')}
|
||||||
}
|
after={
|
||||||
/>
|
<MentionModeSwitcher
|
||||||
</SequenceCard>
|
pushRules={pushRules}
|
||||||
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
ruleId={RuleId.AtRoomNotification}
|
||||||
<SettingTile
|
defaultPushRuleData={DefaultAtRoomNotification}
|
||||||
title={t('Settings.mention_room')}
|
/>
|
||||||
after={
|
}
|
||||||
<MentionModeSwitcher
|
/>
|
||||||
pushRules={pushRules}
|
</SettingsSection>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { IPusherRequest } from 'matrix-js-sdk';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
import { useSetting } from '../../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../../state/settings';
|
import { settingsAtom } from '../../../state/settings';
|
||||||
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
|
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
|
||||||
|
|
@ -176,50 +175,26 @@ export function SystemNotification() {
|
||||||
'isNotificationSounds'
|
'isNotificationSounds'
|
||||||
);
|
);
|
||||||
const [inviteSpamFilter, setInviteSpamFilter] = useSetting(settingsAtom, 'inviteSpamFilter');
|
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 (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<SettingsSection label={t('Settings.system')}>
|
||||||
<Text size="L400">{t('Settings.system')}</Text>
|
{pushStatus !== 'unavailable' && <PushNotification />}
|
||||||
<SequenceCard
|
<SettingTile
|
||||||
className={SequenceCardStyle}
|
title={t('Settings.notification_sound')}
|
||||||
variant="Background"
|
description={t('Settings.notification_sound_desc')}
|
||||||
direction="Column"
|
after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
|
||||||
gap="400"
|
/>
|
||||||
>
|
<SettingTile
|
||||||
<PushNotification />
|
title={t('Settings.invite_spam_filter')}
|
||||||
</SequenceCard>
|
description={t('Settings.invite_spam_filter_desc')}
|
||||||
<SequenceCard
|
after={<Switch value={inviteSpamFilter} onChange={setInviteSpamFilter} />}
|
||||||
className={SequenceCardStyle}
|
/>
|
||||||
variant="Background"
|
<EmailNotification />
|
||||||
direction="Column"
|
</SettingsSection>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,46 @@
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
import { config } from 'folds';
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
export const SequenceCardStyle = style({
|
// Section label above a grouped panel — uppercase, tracked, muted. Matches the
|
||||||
padding: config.space.S300,
|
// 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,
|
useActiveTheme,
|
||||||
useSystemThemeKind,
|
useSystemThemeKind,
|
||||||
} from '../hooks/useTheme';
|
} from '../hooks/useTheme';
|
||||||
import { useSetting } from '../state/hooks/settings';
|
|
||||||
import { settingsAtom } from '../state/settings';
|
|
||||||
|
|
||||||
export function UnAuthRouteThemeManager() {
|
export function UnAuthRouteThemeManager() {
|
||||||
const systemThemeKind = useSystemThemeKind();
|
const systemThemeKind = useSystemThemeKind();
|
||||||
|
|
@ -30,7 +28,6 @@ export function UnAuthRouteThemeManager() {
|
||||||
|
|
||||||
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||||
const activeTheme = useActiveTheme();
|
const activeTheme = useActiveTheme();
|
||||||
const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.className = '';
|
document.body.className = '';
|
||||||
|
|
@ -38,12 +35,11 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
document.body.classList.add(...activeTheme.classNames);
|
document.body.classList.add(...activeTheme.classNames);
|
||||||
|
|
||||||
if (monochromeMode) {
|
// Clear any leftover grayscale filter from the retired monochrome-mode
|
||||||
document.body.style.filter = 'grayscale(1)';
|
// setting so users who had it enabled aren't stuck in grayscale after the
|
||||||
} else {
|
// toggle was removed from Settings.
|
||||||
document.body.style.filter = '';
|
document.body.style.filter = '';
|
||||||
}
|
}, [activeTheme]);
|
||||||
}, [activeTheme, monochromeMode]);
|
|
||||||
|
|
||||||
return <ThemeContextProvider value={activeTheme}>{children}</ThemeContextProvider>;
|
return <ThemeContextProvider value={activeTheme}>{children}</ThemeContextProvider>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export type ThemeId = 'light-theme' | 'dark-theme';
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
themeId?: ThemeId;
|
themeId?: ThemeId;
|
||||||
useSystemTheme: boolean;
|
useSystemTheme: boolean;
|
||||||
monochromeMode?: boolean;
|
|
||||||
isMarkdown: boolean;
|
isMarkdown: boolean;
|
||||||
editorToolbar: boolean;
|
editorToolbar: boolean;
|
||||||
twitterEmoji: boolean;
|
twitterEmoji: boolean;
|
||||||
|
|
@ -39,7 +38,6 @@ const SYSTEM_TIME_FORMAT_CLEANUP_KEY = 'system-time-format-cleanup';
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
themeId: undefined,
|
themeId: undefined,
|
||||||
useSystemTheme: true,
|
useSystemTheme: true,
|
||||||
monochromeMode: false,
|
|
||||||
isMarkdown: true,
|
isMarkdown: true,
|
||||||
editorToolbar: false,
|
editorToolbar: false,
|
||||||
twitterEmoji: false,
|
twitterEmoji: false,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue