From a3dbe0df7870237e409269d3a0d5a74ba63ef8e4 Mon Sep 17 00:00:00 2001 From: heaven Date: Wed, 3 Jun 2026 00:27:29 +0300 Subject: [PATCH] feat(settings): redesign settings as a grouped Dawn list and drop the Developer Tools tab --- public/locales/en.json | 30 +- public/locales/ru.json | 30 +- src/app/features/settings/Settings.tsx | 15 +- src/app/features/settings/SettingsPage.tsx | 51 +++ src/app/features/settings/SettingsScreen.tsx | 4 +- src/app/features/settings/SettingsSection.tsx | 68 ++++ src/app/features/settings/about/About.tsx | 345 ++++++------------ src/app/features/settings/account/Account.tsx | 37 +- .../features/settings/account/ContactInfo.tsx | 41 +-- .../settings/account/IgnoredUserList.tsx | 47 +-- .../features/settings/account/MatrixId.tsx | 33 +- src/app/features/settings/account/Profile.tsx | 28 +- .../settings/developer-tools/AccountData.tsx | 102 ------ .../settings/developer-tools/DevelopTools.tsx | 136 ------- .../settings/developer-tools/index.ts | 1 - .../features/settings/devices/DeviceTile.tsx | 16 +- src/app/features/settings/devices/Devices.tsx | 164 ++++----- .../features/settings/devices/LocalBackup.tsx | 26 +- .../settings/devices/OtherDevices.tsx | 148 ++++---- .../emojis-stickers/EmojisStickers.tsx | 33 +- .../settings/emojis-stickers/GlobalPacks.tsx | 144 ++++---- .../settings/emojis-stickers/UserPack.tsx | 66 ++-- src/app/features/settings/general/General.tsx | 230 +++++------- src/app/features/settings/index.ts | 2 + .../settings/notifications/AllMessages.tsx | 103 ++---- .../notifications/KeywordMessages.tsx | 57 +-- .../settings/notifications/Notifications.tsx | 54 +-- .../notifications/SpecialMessages.tsx | 135 +++---- .../notifications/SystemNotification.tsx | 67 ++-- src/app/features/settings/styles.css.ts | 46 ++- src/app/pages/ThemeManager.tsx | 14 +- src/app/state/settings.ts | 2 - 32 files changed, 865 insertions(+), 1410 deletions(-) create mode 100644 src/app/features/settings/SettingsPage.tsx create mode 100644 src/app/features/settings/SettingsSection.tsx delete mode 100644 src/app/features/settings/developer-tools/AccountData.tsx delete mode 100644 src/app/features/settings/developer-tools/DevelopTools.tsx delete mode 100644 src/app/features/settings/developer-tools/index.ts diff --git a/public/locales/en.json b/public/locales/en.json index b68b227c..291fd9c5 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -88,7 +88,6 @@ "menu_notifications": "Notifications", "menu_devices": "Devices", "menu_emojis_stickers": "Emojis & Stickers", - "menu_developer_tools": "Developer Tools", "menu_about": "About", "drag_to_close": "Drag down to close", "close": "Close", @@ -105,7 +104,6 @@ "theme_light": "Light", "theme_dark": "Dark", "theme": "Theme", - "monochrome_mode": "Monochrome Mode", "twitter_emoji": "Twitter Emoji", "page_zoom": "Page Zoom", "save": "Save", @@ -116,12 +114,13 @@ "hide_activity": "Hide Typing & Read Receipts", "hide_activity_desc": "Turn off both typing status and read receipts to keep your activity private.", "messages": "Messages", - "hide_membership": "Hide Membership Change", - "hide_profile": "Hide Profile Change", + "hide_service_events": "Hide Service Events", + "hide_service_events_desc": "Hide joins, leaves, name and avatar changes from the timeline.", "disable_media_auto_load": "Disable Media Auto Load", "url_preview": "Url Preview", - "url_preview_encrypted": "Url Preview in Encrypted Room", - "show_hidden_events": "Show Hidden Events", + "advanced": "Advanced", + "developer_mode": "Developer Mode", + "developer_mode_desc": "Unlock technical tools, like viewing the source of messages.", "account_title": "Account", "profile": "Profile", "avatar": "Avatar", @@ -139,7 +138,6 @@ "select_user": "Select User", "select_user_desc": "Prevent receiving messages or invites from user by adding their userId.", "block": "Block", - "users": "Users", "notifications_title": "Notifications", "block_messages": "Block Messages", "block_messages_moved": "This option has been moved to \"Account > Block Users\" section.", @@ -166,7 +164,6 @@ "email_send_notif_to": "Send notification to your email. (\"{{email}}\")", "unexpected_error": "Unexpected Error!", "all_messages": "All Messages", - "badge": "Badge: ", "one_to_one": "1-to-1 Chats", "one_to_one_encrypted": "1-to-1 Chats (Encrypted)", "rooms": "Rooms", @@ -256,26 +253,15 @@ "apply_ready": "Changes saved! Apply when ready.", "apply_changes": "Apply Changes", "about_title": "About", - "about_tagline": "Yet another matrix client.", - "options": "Options", + "about_tagline": "A messenger for everyone.", + "about_connected": "Connected to", "clear_cache_title": "Clear Cache & Reload", "clear_cache_desc": "Clear all your locally stored data and reload from server.", "clear_cache": "Clear Cache", - "legal": "Legal", "privacy_policy_title": "Privacy Policy", "privacy_policy_desc": "How your data is handled.", "privacy_policy_open": "Open", - "credits": "Credits", - "devtools_title": "Developer Tools", - "enable_devtools": "Enable Developer Tools", - "access_token": "Access Token", - "access_token_desc": "Copy access token to clipboard.", - "account_data": "Account Data", - "account_data_global": "Global", - "account_data_desc": "Data stored in your global account data.", - "events": "Events", - "total": "Total: {{count}}", - "add_new": "Add New" + "about_credits": "Vojo is built on open-source software — including matrix-js-sdk (Apache 2.0), Twemoji (CC-BY 4.0) and Material Design sounds (CC-BY 4.0)." }, "Search": { "search": "Search", diff --git a/public/locales/ru.json b/public/locales/ru.json index ef4e531d..a0822c9f 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -88,7 +88,6 @@ "menu_notifications": "Уведомления", "menu_devices": "Устройства", "menu_emojis_stickers": "Эмодзи и стикеры", - "menu_developer_tools": "Инструменты разработчика", "menu_about": "О приложении", "drag_to_close": "Потянуть вниз чтобы закрыть", "close": "Закрыть", @@ -105,7 +104,6 @@ "theme_light": "Светлая", "theme_dark": "Тёмная", "theme": "Тема", - "monochrome_mode": "Монохромный режим", "twitter_emoji": "Эмодзи Twitter", "page_zoom": "Масштаб страницы", "save": "Сохранить", @@ -116,12 +114,13 @@ "hide_activity": "Скрыть набор текста и уведомления о прочтении", "hide_activity_desc": "Отключить статус набора и отчёты о прочтении для сохранения приватности.", "messages": "Сообщения", - "hide_membership": "Скрыть изменения участников", - "hide_profile": "Скрыть изменения профиля", + "hide_service_events": "Скрывать служебные сообщения", + "hide_service_events_desc": "Скрывать вступления, выходы и смену имени или аватара в ленте.", "disable_media_auto_load": "Отключить автозагрузку медиа", "url_preview": "Предпросмотр ссылок", - "url_preview_encrypted": "Предпросмотр ссылок в зашифрованных комнатах", - "show_hidden_events": "Показывать скрытые события", + "advanced": "Дополнительно", + "developer_mode": "Режим разработчика", + "developer_mode_desc": "Открывает технические функции, например исходный код сообщений.", "account_title": "Аккаунт", "profile": "Профиль", "avatar": "Аватар", @@ -139,7 +138,6 @@ "select_user": "Выбрать пользователя", "select_user_desc": "Заблокируйте получение сообщений и приглашений от пользователя, добавив его идентификатор.", "block": "Заблокировать", - "users": "Пользователи", "notifications_title": "Уведомления", "block_messages": "Блокировка сообщений", "block_messages_moved": "Эта опция перенесена в раздел «Аккаунт > Заблокированные пользователи».", @@ -166,7 +164,6 @@ "email_send_notif_to": "Отправлять уведомления на вашу почту. (\"{{email}}\")", "unexpected_error": "Непредвиденная ошибка!", "all_messages": "Все сообщения", - "badge": "Значок: ", "one_to_one": "Личные чаты", "one_to_one_encrypted": "Личные чаты (зашифрованные)", "rooms": "Комнаты", @@ -256,26 +253,15 @@ "apply_ready": "Изменения сохранены! Примените, когда будете готовы.", "apply_changes": "Применить изменения", "about_title": "О приложении", - "about_tagline": "Ещё один клиент для Matrix.", - "options": "Параметры", + "about_tagline": "Вседоступный мессенджер.", + "about_connected": "Подключено к", "clear_cache_title": "Очистить кэш и перезагрузить", "clear_cache_desc": "Удалить все локально сохранённые данные и загрузить заново с сервера.", "clear_cache": "Очистить кэш", - "legal": "Юридическое", "privacy_policy_title": "Политика конфиденциальности", "privacy_policy_desc": "Как обрабатываются ваши данные.", "privacy_policy_open": "Открыть", - "credits": "Благодарности", - "devtools_title": "Инструменты разработчика", - "enable_devtools": "Включить инструменты разработчика", - "access_token": "Токен доступа", - "access_token_desc": "Скопировать токен доступа в буфер обмена.", - "account_data": "Данные аккаунта", - "account_data_global": "Глобальные", - "account_data_desc": "Данные, хранящиеся в глобальных данных вашего аккаунта.", - "events": "События", - "total": "Всего: {{count}}", - "add_new": "Добавить" + "about_credits": "Vojo создан на открытом ПО — включая matrix-js-sdk (Apache 2.0), Twemoji (CC-BY 4.0) и звуки Material Design (CC-BY 4.0)." }, "Search": { "search": "Поиск", diff --git a/src/app/features/settings/Settings.tsx b/src/app/features/settings/Settings.tsx index cff2b384..374e5e15 100644 --- a/src/app/features/settings/Settings.tsx +++ b/src/app/features/settings/Settings.tsx @@ -23,7 +23,6 @@ import { Account } from './account'; import { Notifications } from './notifications'; import { Devices } from './devices'; import { EmojisStickers } from './emojis-stickers'; -import { DeveloperTools } from './developer-tools'; import { About } from './about'; import { UseStateProvider } from '../../components/UseStateProvider'; import { stopPropagation } from '../../utils/keyboard'; @@ -35,7 +34,6 @@ export enum SettingsPages { NotificationPage, DevicesPage, EmojisStickersPage, - DeveloperToolsPage, AboutPage, } @@ -56,7 +54,6 @@ export const SETTINGS_PAGE_PARAM = { notifications: SettingsPages.NotificationPage, devices: SettingsPages.DevicesPage, emojis: SettingsPages.EmojisStickersPage, - 'developer-tools': SettingsPages.DeveloperToolsPage, about: SettingsPages.AboutPage, } as const satisfies Record; export const SETTINGS_PARAM_DEVICES = 'devices'; @@ -87,11 +84,6 @@ const SETTINGS_MENU_ITEMS: SettingsMenuItem[] = [ nameKey: 'Settings.menu_emojis_stickers', icon: Icons.Smile, }, - { - page: SettingsPages.DeveloperToolsPage, - nameKey: 'Settings.menu_developer_tools', - icon: Icons.Terminal, - }, { page: SettingsPages.AboutPage, nameKey: 'Settings.menu_about', icon: Icons.Info }, ]; @@ -148,9 +140,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) { const target = e.target as HTMLElement | null; if ( target && - (target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || - target.isContentEditable) + (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) ) { return; } @@ -301,9 +291,6 @@ export function Settings({ initialPage, requestClose }: SettingsProps) { {activePage === SettingsPages.EmojisStickersPage && ( )} - {activePage === SettingsPages.DeveloperToolsPage && ( - - )} {activePage === SettingsPages.AboutPage && } ); diff --git a/src/app/features/settings/SettingsPage.tsx b/src/app/features/settings/SettingsPage.tsx new file mode 100644 index 00000000..04059d80 --- /dev/null +++ b/src/app/features/settings/SettingsPage.tsx @@ -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 ( + + + + + + {title} + + + + + + + + + + + + + + {children} + + + + + + ); +} diff --git a/src/app/features/settings/SettingsScreen.tsx b/src/app/features/settings/SettingsScreen.tsx index ca02f8cf..32a5ebd7 100644 --- a/src/app/features/settings/SettingsScreen.tsx +++ b/src/app/features/settings/SettingsScreen.tsx @@ -102,9 +102,7 @@ export function SettingsScreen() { const target = e.target as HTMLElement | null; if ( target && - (target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || - target.isContentEditable) + (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) ) { return; } diff --git a/src/app/features/settings/SettingsSection.tsx b/src/app/features/settings/SettingsSection.tsx new file mode 100644 index 00000000..319b6021 --- /dev/null +++ b/src/app/features/settings/SettingsSection.tsx @@ -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['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 ( + + {label && ( + + + {label} + + {footnote && ( + + {footnote} + + )} + + )} + + {rows.map((row, index) => ( + + {row} + + ))} + + + ); +} diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx index 72bc5df7..fa3158c8 100644 --- a/src/app/features/settings/about/About.tsx +++ b/src/app/features/settings/about/About.tsx @@ -1,13 +1,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Text, IconButton, Icon, Icons, Scroll, Button, config, toRem } from 'folds'; -import { Page, PageContent, PageHeader } from '../../../components/page'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; +import { Box, Text, Button, Chip, color, config, toRem } from 'folds'; import { SettingTile } from '../../../components/setting-tile'; import VojoSVG from '../../../../../public/res/svg/vojo.svg'; import { clearCacheAndReload } from '../../../../client/initMatrix'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { copyToClipboard } from '../../../utils/dom'; +import { SettingsPage } from '../SettingsPage'; +import { SettingsSection } from '../SettingsSection'; +import { Mono } from '../styles.css'; type AboutProps = { requestClose: () => void; @@ -15,232 +16,122 @@ type AboutProps = { export function About({ requestClose }: AboutProps) { const { t } = useTranslation(); const mx = useMatrixClient(); + const domain = mx.getDomain() ?? 'vojo.chat'; + const version = __APP_VERSION__; return ( - - - - - - {t('Settings.about_title')} - - - - - - - + + {/* Brand hero — centered logo tile, wordmark, tagline, and a copyable + Fleet-tinted version chip. Deliberately not the stock logo-left row. */} + + + Vojo - - - - - - - - Vojo logo - - - - - Vojo - {__APP_VERSION__} - - {t('Settings.about_tagline')} - - - - - {t('Settings.options')} - - clearCacheAndReload(mx)} - variant="Secondary" - fill="Soft" - size="300" - radii="300" - outlined - > - {t('Settings.clear_cache')} - - } - /> - - - - {t('Settings.legal')} - - - {t('Settings.privacy_policy_open')} - - } - /> - - - - {t('Settings.credits')} - - -
  • - - The{' '} - - matrix-js-sdk - {' '} - is ©{' '} - - The Matrix.org Foundation C.I.C - {' '} - used under the terms of{' '} - - Apache 2.0 - - . - -
  • -
  • - - The{' '} - - twemoji-colr - {' '} - font is ©{' '} - - Mozilla Foundation - {' '} - used under the terms of{' '} - - Apache 2.0 - - . - -
  • -
  • - - The{' '} - - Twemoji - {' '} - emoji art is ©{' '} - - Twitter, Inc and other contributors - {' '} - used under the terms of{' '} - - CC-BY 4.0 - - . - -
  • -
  • - - The{' '} - - Material sound resources - {' '} - are ©{' '} - - Google - {' '} - used under the terms of{' '} - - CC-BY 4.0 - - . - -
  • -
    -
    -
    -
    -
    -
    + + Vojo + + {t('Settings.about_tagline')} + + + copyToClipboard(version)} + aria-label={t('Settings.copy')} + > + + {version} + +
    -
    + + {/* Connection strip — the Dawn canon's mono status bar. A green "online" + dot plus the real homeserver this session is connected to. */} + + + + {t('Settings.about_connected')} + + + {domain} + + + + {/* Actions — privacy policy + cache reset, in the shared grouped panel. */} + + + {t('Settings.privacy_policy_open')} + + } + /> + clearCacheAndReload(mx)} + variant="Secondary" + fill="Soft" + size="300" + radii="300" + outlined + > + {t('Settings.clear_cache')} + + } + /> + + + {/* 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. */} + + {t('Settings.about_credits')} + + ); } diff --git a/src/app/features/settings/account/Account.tsx b/src/app/features/settings/account/Account.tsx index 89ad8215..81429222 100644 --- a/src/app/features/settings/account/Account.tsx +++ b/src/app/features/settings/account/Account.tsx @@ -1,11 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds'; -import { Page, PageContent, PageHeader } from '../../../components/page'; import { MatrixId } from './MatrixId'; import { Profile } from './Profile'; import { ContactInformation } from './ContactInfo'; import { IgnoredUserList } from './IgnoredUserList'; +import { SettingsPage } from '../SettingsPage'; type AccountProps = { requestClose: () => void; @@ -13,33 +12,11 @@ type AccountProps = { export function Account({ requestClose }: AccountProps) { const { t } = useTranslation(); return ( - - - - - - {t('Settings.account_title')} - - - - - - - - - - - - - - - - - - - - - - + + + + + + ); } diff --git a/src/app/features/settings/account/ContactInfo.tsx b/src/app/features/settings/account/ContactInfo.tsx index 5c016b0e..de732028 100644 --- a/src/app/features/settings/account/ContactInfo.tsx +++ b/src/app/features/settings/account/ContactInfo.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect } from 'react'; import { Box, Text, Chip } from 'folds'; import { useTranslation } from 'react-i18next'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { SettingsSection } from '../SettingsSection'; +import { Mono } from '../styles.css'; export function ContactInformation() { const { t } = useTranslation(); @@ -23,28 +23,21 @@ export function ContactInformation() { }, [loadThreePIds]); return ( - - {t('Settings.contact_info')} - + - - - {emailIds?.map((email) => ( - - {email.address} - - ))} - - {/* */} - - - + + {emailIds?.map((email) => ( + + + {email.address} + + + ))} + + + ); } diff --git a/src/app/features/settings/account/IgnoredUserList.tsx b/src/app/features/settings/account/IgnoredUserList.tsx index 2cc6dcb4..44e05567 100644 --- a/src/app/features/settings/account/IgnoredUserList.tsx +++ b/src/app/features/settings/account/IgnoredUserList.tsx @@ -1,14 +1,14 @@ import React, { ChangeEventHandler, FormEventHandler, useCallback, useState } from 'react'; import { Box, Button, Chip, Icon, IconButton, Icons, Input, Spinner, Text, config } from 'folds'; import { useTranslation } from 'react-i18next'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { isUserId } from '../../../utils/matrix'; import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers'; import { useAlive } from '../../../hooks/useAlive'; +import { SettingsSection } from '../SettingsSection'; +import { Mono } from '../styles.css'; function IgnoreUserInput({ userList }: { userList: string[] }) { const { t } = useTranslation(); @@ -123,7 +123,7 @@ function IgnoredUserChip({ userId, userList }: { userId: string; userList: strin onClick={handleUnignore} disabled={unIgnoring} > - + {userId} @@ -135,32 +135,19 @@ export function IgnoredUserList() { const ignoredUsers = useIgnoredUsers(); return ( - - - {t('Settings.blocked_users')} - - - - - - {ignoredUsers.length > 0 && ( - - {t('Settings.users')} - - {ignoredUsers.map((userId) => ( - - ))} - - - )} - - - - + + + + + {ignoredUsers.length > 0 && ( + + {ignoredUsers.map((userId) => ( + + ))} + + )} + + + ); } diff --git a/src/app/features/settings/account/MatrixId.tsx b/src/app/features/settings/account/MatrixId.tsx index 4e283313..40e106c6 100644 --- a/src/app/features/settings/account/MatrixId.tsx +++ b/src/app/features/settings/account/MatrixId.tsx @@ -1,29 +1,30 @@ import React from 'react'; -import { Box, Text, Chip } from 'folds'; +import { Text, Chip } from 'folds'; import { useTranslation } from 'react-i18next'; import { useAuthedUserId } from '../../../hooks/useAuthedUserId'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { copyToClipboard } from '../../../utils/dom'; +import { SettingsSection } from '../SettingsSection'; +import { Mono } from '../styles.css'; export function MatrixId() { const { t } = useTranslation(); const userId = useAuthedUserId(); return ( - - {t('Settings.matrix_id')} - - copyToClipboard(userId)}> - {t('Settings.copy')} - - } - /> - - + + + {userId} + + } + after={ + copyToClipboard(userId)}> + {t('Settings.copy')} + + } + /> + ); } diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index 398b0877..74eb981b 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -26,9 +26,8 @@ import { } from 'folds'; import FocusTrap from 'focus-trap-react'; import { useTranslation } from 'react-i18next'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { SettingsSection } from '../SettingsSection'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useAuthedUserId } from '../../../hooks/useAuthedUserId'; import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile'; @@ -92,11 +91,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) { return ( - {t('Settings.avatar')} - - } + title={t('Settings.avatar')} after={ - {t('Settings.display_name')} - - } - > + - {t('Settings.profile')} - - - - - + + + + ); } diff --git a/src/app/features/settings/developer-tools/AccountData.tsx b/src/app/features/settings/developer-tools/AccountData.tsx deleted file mode 100644 index 67446a52..00000000 --- a/src/app/features/settings/developer-tools/AccountData.tsx +++ /dev/null @@ -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 ( - - {t('Settings.account_data')} - - onExpandToggle(!expand)} - variant="Secondary" - fill="Soft" - size="300" - radii="300" - outlined - before={ - - } - > - {expand ? t('Settings.collapse') : t('Settings.expand')} - - } - /> - {expand && ( - - - {t('Settings.events')} - {t('Settings.total', { count: accountDataTypes.length })} - - - } - onClick={() => onSelect(null)} - > - - - {t('Settings.add_new')} - - - - {accountDataTypes.sort().map((type) => ( - } - onClick={() => onSelect(type)} - > - - - {type} - - - - ))} - - - )} - - - ); -} diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx deleted file mode 100644 index cba7b666..00000000 --- a/src/app/features/settings/developer-tools/DevelopTools.tsx +++ /dev/null @@ -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(); - - 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 ( - setAccountDataType(undefined)} - /> - ); - } - - return ( - - - - - - {t('Settings.devtools_title')} - - - - - - - - - - - - - - - {t('Settings.options')} - - - } - /> - - {developerTools && ( - - - copyToClipboard(mx.getAccessToken() ?? '') - } - variant="Secondary" - fill="Soft" - size="300" - radii="300" - outlined - > - {t('Settings.copy')} - - } - /> - - )} - - {developerTools && ( - - )} - - - - - - ); -} diff --git a/src/app/features/settings/developer-tools/index.ts b/src/app/features/settings/developer-tools/index.ts deleted file mode 100644 index 1fcceff5..00000000 --- a/src/app/features/settings/developer-tools/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DevelopTools'; diff --git a/src/app/features/settings/devices/DeviceTile.tsx b/src/app/features/settings/devices/DeviceTile.tsx index 1b9d84b9..05087a97 100644 --- a/src/app/features/settings/devices/DeviceTile.tsx +++ b/src/app/features/settings/devices/DeviceTile.tsx @@ -25,16 +25,17 @@ import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../../utils import { BreakWord } from '../../../styles/Text.css'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; +import { Mono, SettingRow } from '../styles.css'; import { LogoutDialog } from '../../../components/LogoutDialog'; import { stopPropagation } from '../../../utils/keyboard'; export function DeviceTilePlaceholder() { return ( @@ -52,8 +53,7 @@ function DeviceActiveTime({ ts }: { ts: number }) { <> {today(ts) && t('Settings.today')} {yesterday(ts) && t('Settings.yesterday')} - {!today(ts) && !yesterday(ts) && timeDayMonYear(ts)}{' '} - {timeHourMinute(ts)} + {!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)} ); @@ -66,13 +66,13 @@ function DeviceDetails({ device }: { device: IMyDevice }) { {typeof device.device_id === 'string' && ( {t('Settings.device_id')} - {device.device_id} + {device.device_id} )} {typeof device.last_seen_ip === 'string' && ( {t('Settings.ip_address')} - {device.last_seen_ip} + {device.last_seen_ip} )} @@ -100,9 +100,9 @@ export function DeviceKeyDetails({ crypto }: DeviceKeyDetailsProps) { return ( {t('Settings.device_key')} - + {keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : t('Settings.loading')} - + ); } diff --git a/src/app/features/settings/devices/Devices.tsx b/src/app/features/settings/devices/Devices.tsx index 10d157e4..a9123857 100644 --- a/src/app/features/settings/devices/Devices.tsx +++ b/src/app/features/settings/devices/Devices.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds'; -import { Page, PageContent, PageHeader } from '../../../components/page'; +import { Box, Text } from 'folds'; import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; +import { SectionLabel, SettingRow } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { SettingsPage } from '../SettingsPage'; +import { SettingsSection } from '../SettingsSection'; import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { LocalBackup } from './LocalBackup'; @@ -67,101 +68,74 @@ export function Devices({ requestClose }: DevicesProps) { ); return ( - - - - - - {t('Settings.devices_title')} - - - - - - - - - - - - - - - {t('Settings.security')} - - - - {crossSigningActive && ( - - - - - )} - - } + + + + + {crossSigningActive && ( + + - - - - {t('Settings.current')} - {currentDevice ? ( - - } - > - {crypto && } - - {crossSigningActive && - verificationStatus === VerificationStatus.Unverified && - defaultSecretStorageKeyId && - defaultSecretStorageKeyContent && ( - - )} - {crypto && verificationStatus === VerificationStatus.Verified && ( - - )} - - ) : ( - - )} - - {devices === undefined && } - {otherDevices && ( - + + )} + + } + /> + + + + {t('Settings.current')} + + {currentDevice ? ( + + } + > + {crypto && } + + {crossSigningActive && + verificationStatus === VerificationStatus.Unverified && + defaultSecretStorageKeyId && + defaultSecretStorageKeyContent && ( + )} - - - - + {crypto && verificationStatus === VerificationStatus.Verified && ( + + )} + + ) : ( + + )} - + {devices === undefined && } + {otherDevices && ( + + )} + + ); } diff --git a/src/app/features/settings/devices/LocalBackup.tsx b/src/app/features/settings/devices/LocalBackup.tsx index baed1bbf..405b9202 100644 --- a/src/app/features/settings/devices/LocalBackup.tsx +++ b/src/app/features/settings/devices/LocalBackup.tsx @@ -2,9 +2,8 @@ import React, { FormEventHandler, useCallback, useEffect, useState } from 'react import { useTranslation } from 'react-i18next'; import { Box, Button, color, Icon, Icons, Spinner, Text, toRem } from 'folds'; import FileSaver from 'file-saver'; -import { SequenceCard } from '../../../components/sequence-card'; import { SettingTile } from '../../../components/setting-tile'; -import { SequenceCardStyle } from '../styles.css'; +import { SettingsSection } from '../SettingsSection'; import { PasswordInput } from '../../../components/password-input'; import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; @@ -308,24 +307,9 @@ function ImportKeysTile() { export function LocalBackup() { const { t } = useTranslation(); return ( - - {t('Settings.local_backup')} - - - - - - - + + + + ); } diff --git a/src/app/features/settings/devices/OtherDevices.tsx b/src/app/features/settings/devices/OtherDevices.tsx index 373e2fc1..86bfb314 100644 --- a/src/app/features/settings/devices/OtherDevices.tsx +++ b/src/app/features/settings/devices/OtherDevices.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Box, Button, config, Menu, Spinner, Text } from 'folds'; import { AuthDict, IMyDevice, MatrixError } from 'matrix-js-sdk'; import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; +import { SectionLabel, SettingRow } from '../styles.css'; import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA'; import { DeviceDeleteBtn, DeviceTile } from './DeviceTile'; import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback'; @@ -104,83 +104,91 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O return devices.length > 0 ? ( <> - - {t('Settings.others')} - {authMetadata && ( - - - {t('Settings.open')} - - } - /> - - )} - {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) => ( + + + {t('Settings.others')} + + + {authMetadata && ( - - ) : ( - - ) + + {t('Settings.open')} + } /> - {showVerification && crypto && ( - - {(status) => - status === VerificationStatus.Unverified && ( - + + )} + {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) => ( + + + ) : ( + ) } - - )} - - ))} + /> + {showVerification && crypto && ( + + {(status) => + status === VerificationStatus.Unverified && ( + + ) + } + + )} + + ))} + {deleted.size > 0 && ( void; @@ -23,31 +22,9 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) { } return ( - - - - - - {t('Settings.emojis_stickers_title')} - - - - - - - - - - - - - - - - - - - - + + + + ); } diff --git a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx index 44c5c686..10a78806 100644 --- a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx +++ b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx @@ -26,7 +26,7 @@ import FocusTrap from 'focus-trap-react'; import { useAtomValue } from 'jotai'; import { Room } from 'matrix-js-sdk'; import { useGlobalImagePacks, useRoomsImagePacks } from '../../../hooks/useImagePacks'; -import { SequenceCardStyle } from '../styles.css'; +import { SectionLabel, SettingRow } from '../styles.css'; import { SequenceCard } from '../../../components/sequence-card'; import { SettingTile } from '../../../components/setting-tile'; import { mxcUrlToHttp } from '../../../utils/matrix'; @@ -182,7 +182,7 @@ function GlobalPackSelector({ return ( @@ -424,71 +426,77 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) { return ( <> - - {t('Settings.favorite_packs')} - - - - setMenuCords(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => - evt.key === 'ArrowDown' || evt.key === 'ArrowRight', - isKeyBackward: (evt: KeyboardEvent) => - evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', - escapeDeactivates: stopPropagation, - }} - > - + + {t('Settings.favorite_packs')} + + + + + + setMenuCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, }} > - - - - } - /> - - } - /> - - {globalPacks.map(renderPack)} - {nonGlobalPacks - .filter((pack) => !!selectedPacks.find((addr) => packAddressEqual(pack.address, addr))) - .map(renderPack)} + + + + + } + /> + + } + /> + + {globalPacks.map(renderPack)} + {nonGlobalPacks + .filter((pack) => !!selectedPacks.find((addr) => packAddressEqual(pack.address, addr))) + .map(renderPack)} + {hasChanges && ( - {t('Settings.default_pack')} - - - {avatarUrl ? ( - - ) : ( - - - - )} - - } - after={ - - } - /> - - + + + {avatarUrl ? ( + + ) : ( + + + + )} + + } + after={ + + } + /> + ); } diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index 50d87539..5fd25eb2 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -10,14 +10,12 @@ import { Button, config, Icon, - IconButton, Icons, Input, Menu, MenuItem, PopOut, RectCords, - Scroll, Switch, Text, toRem, @@ -25,8 +23,6 @@ import { import FocusTrap from 'focus-trap-react'; import { isKeyHotkey } from 'is-hotkey'; import { useTranslation } from 'react-i18next'; -import { Page, PageContent, PageHeader } from '../../../components/page'; -import { SequenceCard } from '../../../components/sequence-card'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { SettingTile } from '../../../components/setting-tile'; @@ -34,7 +30,8 @@ import { KeySymbol } from '../../../utils/key-symbol'; import { isMacOS } from '../../../utils/user-agent'; import { stopPropagation } from '../../../utils/keyboard'; import { DarkTheme, LightTheme } from '../../../hooks/useTheme'; -import { SequenceCardStyle } from '../styles.css'; +import { SettingsPage } from '../SettingsPage'; +import { SettingsSection } from '../SettingsSection'; type ThemeChoice = 'system' | typeof LightTheme.id | typeof DarkTheme.id; @@ -52,10 +49,7 @@ function ThemeSelect() { choice = LightTheme.id; } - const choices = useMemo( - () => ['system', LightTheme.id, DarkTheme.id], - [] - ); + const choices = useMemo(() => ['system', LightTheme.id, DarkTheme.id], []); const choiceLabel = useMemo>( () => ({ @@ -184,34 +178,17 @@ function PageZoomInput() { function Appearance() { const { t } = useTranslation(); - const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode'); const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); return ( - - {t('Settings.appearance')} - - } /> - - - - } - /> - - - - } - /> - - - - } /> - - + + } /> + } + /> + } /> + ); } @@ -222,31 +199,24 @@ function Editor() { const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity'); return ( - - {t('Settings.editor')} - - } - /> - - - } - /> - - - } - /> - - + + } + /> + } + /> + } + /> + ); } @@ -262,69 +232,56 @@ function Messages() { ); const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview'); - const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview'); - const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents'); + + // Merged control for the two service-event toggles (joins/leaves + + // name/avatar changes). Reads on only when *both* classes are hidden, and + // writes both atoms together. AND (not OR) keeps the switch honest against + // the default state: out of the box hideMembershipEvents is false (joins/ + // leaves shown) and hideNickAvatarEvents is true, so the switch reads OFF + // and the visible join/leave events agree with it. The timeline still reads + // each atom directly, so existing persisted preferences are untouched until + // the user flips this switch (which then sets both to the same value). + const hideServiceEvents = hideMembershipEvents && hideNickAvatarEvents; + const setHideServiceEvents = (value: boolean) => { + setHideMembershipEvents(value); + setHideNickAvatarEvents(value); + }; return ( - - {t('Settings.messages')} - - - } - /> - - - - } - /> - - - setMediaAutoLoad(!v)} - /> - } - /> - - - } - /> - - - } - /> - - - - } - /> - - + + + } + /> + setMediaAutoLoad(!v)} /> + } + /> + } + /> + + ); +} + +function Advanced() { + const { t } = useTranslation(); + const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools'); + + return ( + + } + /> + ); } @@ -334,32 +291,11 @@ type GeneralProps = { export function General({ requestClose }: GeneralProps) { const { t } = useTranslation(); return ( - - - - - - {t('Settings.general_title')} - - - - - - - - - - - - - - - - - - - - - + + + + + + ); } diff --git a/src/app/features/settings/index.ts b/src/app/features/settings/index.ts index 61a44e51..44108574 100644 --- a/src/app/features/settings/index.ts +++ b/src/app/features/settings/index.ts @@ -1,3 +1,5 @@ export * from './Settings'; export * from './SettingsScreen'; export * from './MobileSettingsHorseshoe'; +export * from './SettingsPage'; +export * from './SettingsSection'; diff --git a/src/app/features/settings/notifications/AllMessages.tsx b/src/app/features/settings/notifications/AllMessages.tsx index 25dc5fb0..4502cfff 100644 --- a/src/app/features/settings/notifications/AllMessages.tsx +++ b/src/app/features/settings/notifications/AllMessages.tsx @@ -1,13 +1,11 @@ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Badge, Box, Text } from 'folds'; import { ConditionKind, IPushRules, PushRuleCondition, PushRuleKind, RuleId } from 'matrix-js-sdk'; import { useAccountData } from '../../../hooks/useAccountData'; import { AccountDataEvent } from '../../../../types/matrix/accountData'; import { NotificationModeSwitcher } from './NotificationModeSwitcher'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { SettingsSection } from '../SettingsSection'; import { PushRuleData, usePushRule } from '../../../hooks/usePushRule'; import { getNotificationModeActions, @@ -82,73 +80,36 @@ export function AllMessagesNotifications() { ); return ( - - - {t('Settings.all_messages')} - - {t('Settings.badge')} - - 1 - - - - - } - /> - - - - } - /> - - - } - /> - - - - } - /> - - + + } + /> + + } + /> + } + /> + + } + /> + ); } diff --git a/src/app/features/settings/notifications/KeywordMessages.tsx b/src/app/features/settings/notifications/KeywordMessages.tsx index f731b08e..6404af75 100644 --- a/src/app/features/settings/notifications/KeywordMessages.tsx +++ b/src/app/features/settings/notifications/KeywordMessages.tsx @@ -1,12 +1,12 @@ import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IPushRule, IPushRules, PushRuleKind } from 'matrix-js-sdk'; -import { Box, Text, Badge, Button, Input, config, IconButton, Icons, Icon, Spinner } from 'folds'; +import { Box, Text, Button, Input, config, IconButton, Icons, Icon, Spinner } from 'folds'; import { useAccountData } from '../../../hooks/useAccountData'; import { AccountDataEvent } from '../../../../types/matrix/accountData'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { SettingsSection } from '../SettingsSection'; +import { Mono } from '../styles.css'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { getNotificationModeActions, @@ -163,44 +163,25 @@ export function KeywordMessagesNotifications() { }, [pushRules]); return ( - - - {t('Settings.keyword_messages')} - - {t('Settings.badge')} - - 1 - - - - + - - - - + + {keywordPushRules.map((pushRule) => ( - - } - after={} - /> - + title={ + + {`"${pushRule.pattern}"`} + + } + before={} + after={} + /> ))} - + ); } diff --git a/src/app/features/settings/notifications/Notifications.tsx b/src/app/features/settings/notifications/Notifications.tsx index 9bb3a66b..b471df0b 100644 --- a/src/app/features/settings/notifications/Notifications.tsx +++ b/src/app/features/settings/notifications/Notifications.tsx @@ -1,14 +1,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds'; -import { Page, PageContent, PageHeader } from '../../../components/page'; import { SystemNotification } from './SystemNotification'; import { AllMessagesNotifications } from './AllMessages'; import { SpecialMessagesNotifications } from './SpecialMessages'; import { KeywordMessagesNotifications } from './KeywordMessages'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { SettingsPage } from '../SettingsPage'; +import { SettingsSection } from '../SettingsSection'; type NotificationsProps = { requestClose: () => void; @@ -16,44 +14,14 @@ type NotificationsProps = { export function Notifications({ requestClose }: NotificationsProps) { const { t } = useTranslation(); return ( - - - - - - {t('Settings.notifications_title')} - - - - - - - - - - - - - - - - - - - {t('Settings.block_messages')} - - - - - - - - - + + + + + + + + + ); } diff --git a/src/app/features/settings/notifications/SpecialMessages.tsx b/src/app/features/settings/notifications/SpecialMessages.tsx index e2b3d828..ae2fb3cf 100644 --- a/src/app/features/settings/notifications/SpecialMessages.tsx +++ b/src/app/features/settings/notifications/SpecialMessages.tsx @@ -1,12 +1,10 @@ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { ConditionKind, IPushRules, PushRuleKind, RuleId } from 'matrix-js-sdk'; -import { Box, Text, Badge } from 'folds'; import { useAccountData } from '../../../hooks/useAccountData'; import { AccountDataEvent } from '../../../../types/matrix/accountData'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { SettingsSection } from '../SettingsSection'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useAuthedUserId } from '../../../hooks/useAuthedUserId'; import { useUserProfile } from '../../../hooks/useUserProfile'; @@ -124,80 +122,61 @@ export function SpecialMessagesNotifications() { ); return ( - - - {t('Settings.special_messages')} - - {t('Settings.badge')} - - 1 - - - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - + + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + ); } diff --git a/src/app/features/settings/notifications/SystemNotification.tsx b/src/app/features/settings/notifications/SystemNotification.tsx index 3d617389..8001eb31 100644 --- a/src/app/features/settings/notifications/SystemNotification.tsx +++ b/src/app/features/settings/notifications/SystemNotification.tsx @@ -1,10 +1,9 @@ import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Text, Switch, Button, color, Spinner } from 'folds'; +import { Text, Switch, Button, color, Spinner } from 'folds'; import { IPusherRequest } from 'matrix-js-sdk'; -import { SequenceCard } from '../../../components/sequence-card'; -import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { SettingsSection } from '../SettingsSection'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { useEmailNotifications } from '../../../hooks/useEmailNotifications'; @@ -176,50 +175,26 @@ export function SystemNotification() { 'isNotificationSounds' ); const [inviteSpamFilter, setInviteSpamFilter] = useSetting(settingsAtom, 'inviteSpamFilter'); + // Gate the push row at the call site: PushNotification renders null when push + // is unavailable, and SettingsSection's `filter(Boolean)` can only drop + // JSX-level falsy children — a returned-null component would otherwise leave + // an empty bordered first row in the grouped panel. + const pushStatus = usePushNotificationStatus(); return ( - - {t('Settings.system')} - - - - - } - /> - - - } - /> - - - - - + + {pushStatus !== 'unavailable' && } + } + /> + } + /> + + ); } diff --git a/src/app/features/settings/styles.css.ts b/src/app/features/settings/styles.css.ts index ce89c16e..c6606d76 100644 --- a/src/app/features/settings/styles.css.ts +++ b/src/app/features/settings/styles.css.ts @@ -1,6 +1,46 @@ import { style } from '@vanilla-extract/css'; -import { config } from 'folds'; +import { color, config, toRem } from 'folds'; -export const SequenceCardStyle = style({ - padding: config.space.S300, +// Section label above a grouped panel — uppercase, tracked, muted. Matches the +// Dawn canon's "КОМАНДЫ" / "УЧАСТНИКИ · 28" panel headers (sans, not mono). +export const SectionLabel = style({ + display: 'block', + fontSize: toRem(11), + lineHeight: toRem(16), + fontWeight: config.fontWeight.W600, + letterSpacing: '0.06em', + textTransform: 'uppercase', + color: color.Surface.OnContainer, + opacity: 0.5, + paddingLeft: config.space.S100, + paddingRight: config.space.S100, +}); + +// Optional caption rendered under a section label (e.g. "Скрытые от чужих глаз"). +export const SectionFootnote = style({ + display: 'block', + fontSize: toRem(12), + lineHeight: toRem(16), + color: color.Surface.OnContainer, + opacity: 0.5, + paddingLeft: config.space.S100, + paddingRight: config.space.S100, + paddingTop: config.space.S100, +}); + +// One row inside a grouped panel. Slightly roomier than the old card so a +// switch/menu row lands at a comfortable ~44px without feeling cramped. +export const SettingRow = style({ + paddingTop: config.space.S300, + paddingBottom: config.space.S300, + paddingLeft: config.space.S400, + paddingRight: config.space.S400, +}); + +// JetBrains Mono for technical values — mxid, device ids, version, tokens — +// the same stack the DM stream / bot surfaces use for handles & timestamps. +export const Mono = style({ + fontFamily: + '"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', + fontVariantNumeric: 'tabular-nums', }); diff --git a/src/app/pages/ThemeManager.tsx b/src/app/pages/ThemeManager.tsx index 69d50cdb..9fa32f72 100644 --- a/src/app/pages/ThemeManager.tsx +++ b/src/app/pages/ThemeManager.tsx @@ -8,8 +8,6 @@ import { useActiveTheme, useSystemThemeKind, } from '../hooks/useTheme'; -import { useSetting } from '../state/hooks/settings'; -import { settingsAtom } from '../state/settings'; export function UnAuthRouteThemeManager() { const systemThemeKind = useSystemThemeKind(); @@ -30,7 +28,6 @@ export function UnAuthRouteThemeManager() { export function AuthRouteThemeManager({ children }: { children: ReactNode }) { const activeTheme = useActiveTheme(); - const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode'); useEffect(() => { document.body.className = ''; @@ -38,12 +35,11 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) { document.body.classList.add(...activeTheme.classNames); - if (monochromeMode) { - document.body.style.filter = 'grayscale(1)'; - } else { - document.body.style.filter = ''; - } - }, [activeTheme, monochromeMode]); + // Clear any leftover grayscale filter from the retired monochrome-mode + // setting so users who had it enabled aren't stuck in grayscale after the + // toggle was removed from Settings. + document.body.style.filter = ''; + }, [activeTheme]); return {children}; } diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index b9cdda52..a9504783 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -7,7 +7,6 @@ export type ThemeId = 'light-theme' | 'dark-theme'; export interface Settings { themeId?: ThemeId; useSystemTheme: boolean; - monochromeMode?: boolean; isMarkdown: boolean; editorToolbar: boolean; twitterEmoji: boolean; @@ -39,7 +38,6 @@ const SYSTEM_TIME_FORMAT_CLEANUP_KEY = 'system-time-format-cleanup'; const defaultSettings: Settings = { themeId: undefined, useSystemTheme: true, - monochromeMode: false, isMarkdown: true, editorToolbar: false, twitterEmoji: false,