localize settings

This commit is contained in:
v.lagerev 2026-04-13 23:36:03 +03:00
parent 351cd5eda9
commit 335c82e2c0
28 changed files with 727 additions and 332 deletions

View file

@ -1,73 +0,0 @@
{
"Organisms": {
"RoomCommon": {
"changed_room_name": " hat den Raum Name geändert"
}
},
"Auth": {
"title_login": "Anmelden",
"title_register": "Registrieren",
"title_reset_password": "Passwort zurücksetzen",
"homeserver": "Homeserver",
"homeserver_edit": "Bearbeiten",
"homeserver_dialog_title": "Homeserver",
"homeserver_dialog_desc": "Geben Sie die Adresse des Matrix-Homeservers ein, mit dem Sie sich verbinden möchten.",
"homeserver_dialog_placeholder": "example.com",
"homeserver_dialog_cancel": "Abbrechen",
"homeserver_dialog_confirm": "Weiter",
"username_placeholder": "Benutzername",
"password_placeholder": "Passwort",
"forgot_password": "Passwort vergessen?",
"login_button": "Anmelden",
"new_here": "Neu hier?",
"create_account": "Konto erstellen",
"already_have_account": "Bereits ein Konto?",
"remember_password": "Passwort wieder eingefallen?",
"loading_server": "Server wird gesucht...",
"loading_connecting": "Verbindung zu {{url}}...",
"loading_auth_flows": "Laden...",
"error_server_not_found": "Server konnte nicht gefunden werden.",
"error_server_config_invalid": "Verbindung fehlgeschlagen. Die Konfiguration des Servers {{host}} ist ungültig.",
"error_server_base_url_invalid": "Verbindung fehlgeschlagen. Ungültige base_url des Servers.",
"error_server_unavailable": "Verbindung zum Server konnte nicht hergestellt werden.",
"error_auth_flows": "Autorisierungsabläufe konnten nicht geladen werden.",
"error_client_unsupported": "Dieser Client unterstützt keine Autorisierung auf dem Server \"{{server}}\".",
"error_custom_server_not_allowed": "Anmeldung mit benutzerdefiniertem Server ist nicht erlaubt.",
"error_matrix_id_server": "Matrix-ID-Server konnte nicht gefunden werden.",
"error_invalid_credentials": "Ungültiger Benutzername oder Passwort.",
"error_account_deactivated": "Dieses Konto wurde deaktiviert.",
"error_invalid_request": "Anmeldung fehlgeschlagen. Ein Teil der Anfragedaten ist ungültig.",
"error_rate_limited": "Zu viele Versuche. Bitte versuchen Sie es später erneut.",
"error_unknown": "Anmeldung fehlgeschlagen. Unbekannter Fehler.",
"register_disabled": "Die Registrierung ist auf diesem Server deaktiviert.",
"register_rate_limited": "Zu viele Versuche. Bitte versuchen Sie es später erneut.",
"register_invalid_request": "Ungültige Anfrage. Registrierungsparameter konnten nicht abgerufen werden.",
"register_unsupported": "Diese Anwendung unterstützt keine Registrierung auf diesem Server.",
"register_username_label": "Benutzername",
"register_password_label": "Passwort",
"register_confirm_password_label": "Passwort bestätigen",
"register_button": "Registrieren",
"register_token_label": "Registrierungstoken",
"register_token_optional_label": "Registrierungstoken (optional)",
"register_email_label": "E-Mail",
"register_email_optional_label": "E-Mail (optional)",
"register_terms": "Ich akzeptiere die <termsLink>Nutzungsbedingungen</termsLink> des Servers.",
"register_error_user_taken": "Dieser Benutzername ist bereits vergeben.",
"register_error_user_invalid": "Dieser Benutzername enthält ungültige Zeichen.",
"register_error_user_exclusive": "Dieser Benutzername ist reserviert.",
"register_error_password_weak": "Schwaches Passwort. Vom Server abgelehnt, bitte wählen Sie ein stärkeres.",
"register_error_password_short": "Kurzes Passwort. Vom Server abgelehnt, bitte wählen Sie ein längeres.",
"register_error_rate_limited": "Registrierung fehlgeschlagen. Zu viele Anfragen, bitte versuchen Sie es später erneut.",
"register_error_forbidden": "Registrierung fehlgeschlagen. Der Server erlaubt keine Registrierung.",
"register_error_invalid_request": "Registrierung fehlgeschlagen. Ungültige Anfrage.",
"register_error_unknown": "Registrierung fehlgeschlagen. Unbekannter Fehler.",
"reset_description": "Der Homeserver <strong>{{server}}</strong> sendet Ihnen eine E-Mail, um Ihr Passwort zurückzusetzen.",
"reset_email_label": "E-Mail",
"reset_new_password": "Neues Passwort",
"reset_confirm_password": "Passwort bestätigen",
"reset_button": "Passwort zurücksetzen",
"reset_error_fallback": "Passwort konnte nicht zurückgesetzt werden.",
"reset_success_message": "Das Passwort wurde erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.",
"reset_success_login": "Anmelden"
}
}

View file

@ -69,5 +69,215 @@
"reset_error_fallback": "Failed to reset password.",
"reset_success_message": "Password has been reset successfully. Please login with your new password.",
"reset_success_login": "Login"
},
"Settings": {
"title": "Settings",
"menu_general": "General",
"menu_account": "Account",
"menu_notifications": "Notifications",
"menu_devices": "Devices",
"menu_emojis_stickers": "Emojis & Stickers",
"menu_developer_tools": "Developer Tools",
"menu_about": "About",
"logout": "Logout",
"general_title": "General",
"appearance": "Appearance",
"system_theme": "System Theme",
"system_theme_desc": "Choose between light and dark theme based on system preference.",
"light_theme": "Light Theme:",
"dark_theme": "Dark Theme:",
"theme": "Theme",
"theme_desc": "Theme to use when system theme is not enabled.",
"monochrome_mode": "Monochrome Mode",
"twitter_emoji": "Twitter Emoji",
"page_zoom": "Page Zoom",
"date_time": "Date & Time",
"hour_24": "24-Hour Time Format",
"date_format": "Date Format",
"custom": "Custom",
"formatting": "Formatting",
"year": "Year",
"two_digit_year": "Two-digit year",
"four_digit_year": "Four-digit year",
"month": "Month",
"the_month": "The month",
"two_digit_month": "Two-digit month",
"short_month_name": "Short month name",
"full_month_name": "Full month name",
"day_of_month": "Day of the Month",
"day_of_month_val": "Day of the month",
"two_digit_day": "Two-digit day of the month",
"day_of_week": "Day of the Week",
"day_of_week_sunday": "Day of the week (Sunday = 0)",
"two_letter_day": "Two-letter day name",
"short_day_name": "Short day name",
"full_day_name": "Full day name",
"save": "Save",
"editor": "Editor",
"enter_newline": "ENTER for Newline",
"enter_newline_desc": "Use {{key}} + ENTER to send message and ENTER for newline.",
"markdown": "Markdown Formatting",
"hide_activity": "Hide Typing & Read Receipts",
"hide_activity_desc": "Turn off both typing status and read receipts to keep your activity private.",
"messages": "Messages",
"message_layout": "Message Layout",
"message_spacing": "Message Spacing",
"legacy_username_color": "Legacy Username Color",
"hide_membership": "Hide Membership Change",
"hide_profile": "Hide Profile Change",
"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",
"account_title": "Account",
"profile": "Profile",
"avatar": "Avatar",
"upload": "Upload",
"remove": "Remove",
"remove_avatar": "Remove Avatar",
"remove_avatar_confirm": "Are you sure you want to remove profile avatar?",
"display_name": "Display Name",
"matrix_id": "Matrix ID",
"copy": "Copy",
"contact_info": "Contact Information",
"email_address": "Email Address",
"email_address_desc": "Email address attached to your account.",
"blocked_users": "Blocked Users",
"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.",
"system": "System",
"desktop_notifications": "Desktop Notifications",
"notif_permission_blocked": "Notification permission is blocked. Please allow notification permission from browser address bar.",
"notif_not_supported": "Notifications are not supported by the system.",
"notif_show_desktop": "Show desktop notifications when a message arrives.",
"enable": "Enable",
"notification_sound": "Notification Sound",
"notification_sound_desc": "Play sound when a new message arrives.",
"email_notification": "Email Notification",
"email_no_email": "Your account does not have any email attached.",
"email_send_notif": "Send notification to your email.",
"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",
"rooms_encrypted": "Rooms (Encrypted)",
"special_messages": "Special Messages",
"mention_user_id": "Mention User ID (\"{{userId}}\")",
"contains_displayname": "Contains Display Name",
"contains_displayname_value": "Contains Display Name (\"{{displayName}}\")",
"contains_username": "Contains Username (\"{{username}}\")",
"mention_room": "Mention @room",
"contains_room": "Contains @room",
"keyword_messages": "Keyword Messages",
"select_keyword": "Select Keyword",
"select_keyword_desc": "Set a notification preference for message containing given keyword.",
"notif_disable": "Disable",
"notif_silent": "Notify Silent",
"notif_loud": "Notify Loud",
"devices_title": "Devices",
"security": "Security",
"device_verification": "Device Verification",
"device_verification_desc": "To verify device identity and grant access to encrypted messages.",
"current": "Current",
"last_activity": "Last activity: ",
"today": "Today",
"yesterday": "Yesterday",
"device_id": "Device ID: ",
"ip_address": "IP Address: ",
"device_key": "Device Key: ",
"loading": "loading...",
"device_name": "Device Name",
"device_name_public": "Device names are visible to public.",
"cancel": "Cancel",
"edit": "Edit",
"undo": "Undo",
"others": "Others",
"device_dashboard": "Device Dashboard",
"device_dashboard_desc": "Manage your devices on OIDC dashboard.",
"open": "Open",
"delete_devices_error": "Failed to logout devices! Please try again. {{message}}",
"delete_devices_confirm": "Logout from selected devices. ({{count}} selected)",
"delete_devices_unsupported": "Authentication steps to perform this action are not supported by the client.",
"unverified": "Unverified",
"unverified_count": "{{count}} Unverified",
"verified": "Verified",
"verify_steps_title": "Steps to verify from another device.",
"verify_step_1": "Open your other verified device.",
"verify_step_2_settings": "Settings",
"verify_step_3_section": "Devices/Sessions",
"verify_step_4": "Initiate verification.",
"verify_no_device": "If you do not have any verified device, press the \"Verify Manually\" button.",
"verify_current_title": "Unverified",
"verify_current_desc": "Start verification from other device or verify manually.",
"view_less": "View Less",
"learn_more": "Learn More",
"verify_manually": "Verify Manually",
"verify_other_title": "Unverified",
"verify_other_desc": "Verify device identity and grant access to encrypted messages.",
"verify": "Verify",
"reset": "Reset",
"local_backup": "Local Backup",
"new_password": "New Password",
"confirm_password": "Confirm Password",
"password": "Password",
"export": "Export",
"export_title": "Export Messages Data",
"export_desc": "Save password protected copy of encryption data on your device to decrypt messages later.",
"collapse": "Collapse",
"expand": "Expand",
"import_title": "Import Messages Data",
"import_desc": "Load password protected copy of encryption data from device to decrypt your messages.",
"import": "Import",
"decrypt": "Decrypt",
"emojis_stickers_title": "Emojis & Stickers",
"default_pack": "Default Pack",
"unknown": "Unknown",
"view": "View",
"favorite_packs": "Favorite Packs",
"select_pack": "Select Pack",
"select_pack_desc": "Pick emoji and sticker packs from rooms to use globally.",
"select": "Select",
"room_packs": "Room Packs",
"close": "Close",
"select_all": "Select All",
"unselect_all": "Unselect All",
"no_packs": "No Packs",
"no_packs_desc": "Packs from rooms will appear here. You do not have any rooms with packs yet.",
"apply_error": "Failed to apply changes! Please try again.",
"apply_ready": "Changes saved! Apply when ready.",
"apply_changes": "Apply Changes",
"about_title": "About",
"about_tagline": "Yet another matrix client.",
"options": "Options",
"clear_cache_title": "Clear Cache & Reload",
"clear_cache_desc": "Clear all your locally stored data and reload from server.",
"clear_cache": "Clear Cache",
"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"
}
}

View file

@ -49,8 +49,8 @@
"register_button": "Зарегистрироваться",
"register_token_label": "Токен регистрации",
"register_token_optional_label": "Токен регистрации (необязательно)",
"register_email_label": "Email",
"register_email_optional_label": "Email (необязательно)",
"register_email_label": "Эл. почта",
"register_email_optional_label": "Эл. почта (необязательно)",
"register_terms": "Я принимаю <termsLink>Условия использования</termsLink> сервера.",
"register_error_user_taken": "Это имя пользователя уже занято.",
"register_error_user_invalid": "Имя пользователя содержит недопустимые символы.",
@ -69,5 +69,215 @@
"reset_error_fallback": "Не удалось сбросить пароль.",
"reset_success_message": "Пароль успешно сброшен. Войдите с новым паролем.",
"reset_success_login": "Войти"
},
"Settings": {
"title": "Настройки",
"menu_general": "Общие",
"menu_account": "Аккаунт",
"menu_notifications": "Уведомления",
"menu_devices": "Устройства",
"menu_emojis_stickers": "Эмодзи и стикеры",
"menu_developer_tools": "Инструменты разработчика",
"menu_about": "О приложении",
"logout": "Выйти",
"general_title": "Общие",
"appearance": "Внешний вид",
"system_theme": "Системная тема",
"system_theme_desc": "Выбор между светлой и тёмной темой на основе системных настроек.",
"light_theme": "Светлая тема:",
"dark_theme": "Тёмная тема:",
"theme": "Тема",
"theme_desc": "Тема при отключённой системной теме.",
"monochrome_mode": "Монохромный режим",
"twitter_emoji": "Эмодзи Twitter",
"page_zoom": "Масштаб страницы",
"date_time": "Дата и время",
"hour_24": "24-часовой формат",
"date_format": "Формат даты",
"custom": "Пользовательский",
"formatting": "Форматирование",
"year": "Год",
"two_digit_year": "Двузначный год",
"four_digit_year": "Четырёхзначный год",
"month": "Месяц",
"the_month": "Месяц",
"two_digit_month": "Двузначный месяц",
"short_month_name": "Краткое название месяца",
"full_month_name": "Полное название месяца",
"day_of_month": "День месяца",
"day_of_month_val": "День месяца",
"two_digit_day": "Двузначный день месяца",
"day_of_week": "День недели",
"day_of_week_sunday": "День недели (воскресенье = 0)",
"two_letter_day": "Двухбуквенное название дня",
"short_day_name": "Краткое название дня",
"full_day_name": "Полное название дня",
"save": "Сохранить",
"editor": "Редактор",
"enter_newline": "ENTER для новой строки",
"enter_newline_desc": "Используйте {{key}} + ENTER для отправки сообщения и ENTER для новой строки.",
"markdown": "Форматирование Markdown",
"hide_activity": "Скрыть набор текста и уведомления о прочтении",
"hide_activity_desc": "Отключить статус набора и отчёты о прочтении для сохранения приватности.",
"messages": "Сообщения",
"message_layout": "Макет сообщений",
"message_spacing": "Интервал сообщений",
"legacy_username_color": "Классический цвет имени",
"hide_membership": "Скрыть изменения участников",
"hide_profile": "Скрыть изменения профиля",
"disable_media_auto_load": "Отключить автозагрузку медиа",
"url_preview": "Предпросмотр ссылок",
"url_preview_encrypted": "Предпросмотр ссылок в зашифрованных комнатах",
"show_hidden_events": "Показывать скрытые события",
"account_title": "Аккаунт",
"profile": "Профиль",
"avatar": "Аватар",
"upload": "Загрузить",
"remove": "Удалить",
"remove_avatar": "Удалить аватар",
"remove_avatar_confirm": "Вы уверены, что хотите удалить аватар профиля?",
"display_name": "Отображаемое имя",
"matrix_id": "Matrix ID",
"copy": "Копировать",
"contact_info": "Контактная информация",
"email_address": "Адрес электронной почты",
"email_address_desc": "Электронная почта, привязанная к вашему аккаунту.",
"blocked_users": "Заблокированные пользователи",
"select_user": "Выбрать пользователя",
"select_user_desc": "Заблокируйте получение сообщений и приглашений от пользователя, добавив его идентификатор.",
"block": "Заблокировать",
"users": "Пользователи",
"notifications_title": "Уведомления",
"block_messages": "Блокировка сообщений",
"block_messages_moved": "Эта опция перенесена в раздел «Аккаунт > Заблокированные пользователи».",
"system": "Система",
"desktop_notifications": "Уведомления на рабочем столе",
"notif_permission_blocked": "Разрешение на уведомления заблокировано. Разрешите уведомления в адресной строке браузера.",
"notif_not_supported": "Уведомления не поддерживаются системой.",
"notif_show_desktop": "Показывать уведомления на рабочем столе при получении сообщений.",
"enable": "Включить",
"notification_sound": "Звук уведомлений",
"notification_sound_desc": "Воспроизводить звук при получении нового сообщения.",
"email_notification": "Уведомления по почте",
"email_no_email": "К вашему аккаунту не привязана электронная почта.",
"email_send_notif": "Отправлять уведомления на вашу почту.",
"email_send_notif_to": "Отправлять уведомления на вашу почту. (\"{{email}}\")",
"unexpected_error": "Непредвиденная ошибка!",
"all_messages": "Все сообщения",
"badge": "Значок: ",
"one_to_one": "Личные чаты",
"one_to_one_encrypted": "Личные чаты (зашифрованные)",
"rooms": "Комнаты",
"rooms_encrypted": "Комнаты (зашифрованные)",
"special_messages": "Специальные сообщения",
"mention_user_id": "Упоминание ID пользователя (\"{{userId}}\")",
"contains_displayname": "Содержит отображаемое имя",
"contains_displayname_value": "Содержит отображаемое имя (\"{{displayName}}\")",
"contains_username": "Содержит имя пользователя (\"{{username}}\")",
"mention_room": "Упоминание @room",
"contains_room": "Содержит @room",
"keyword_messages": "Ключевые слова",
"select_keyword": "Выбрать ключевое слово",
"select_keyword_desc": "Настройте уведомление для сообщений, содержащих указанное ключевое слово.",
"notif_disable": "Отключить",
"notif_silent": "Тихое уведомление",
"notif_loud": "Громкое уведомление",
"devices_title": "Устройства",
"security": "Безопасность",
"device_verification": "Верификация устройства",
"device_verification_desc": "Для подтверждения идентичности устройства и доступа к зашифрованным сообщениям.",
"current": "Текущее",
"last_activity": "Последняя активность: ",
"today": "Сегодня",
"yesterday": "Вчера",
"device_id": "ID устройства: ",
"ip_address": "IP-адрес: ",
"device_key": "Ключ устройства: ",
"loading": "загрузка...",
"device_name": "Имя устройства",
"device_name_public": "Имена устройств видны всем.",
"cancel": "Отмена",
"edit": "Редактировать",
"undo": "Отменить",
"others": "Другие",
"device_dashboard": "Панель устройств",
"device_dashboard_desc": "Управление устройствами через панель OIDC.",
"open": "Открыть",
"delete_devices_error": "Не удалось выйти с устройств! Попробуйте снова. {{message}}",
"delete_devices_confirm": "Выйти с выбранных устройств. ({{count}} выбрано)",
"delete_devices_unsupported": "Шаги аутентификации для этого действия не поддерживаются клиентом.",
"unverified": "Не верифицировано",
"unverified_count": "{{count}} не верифицировано",
"verified": "Верифицировано",
"verify_steps_title": "Шаги для верификации с другого устройства.",
"verify_step_1": "Откройте другое верифицированное устройство.",
"verify_step_2_settings": "Настройки",
"verify_step_3_section": "Устройства/Сессии",
"verify_step_4": "Начните верификацию.",
"verify_no_device": "Если у вас нет верифицированного устройства, нажмите кнопку «Верифицировать вручную».",
"verify_current_title": "Не верифицировано",
"verify_current_desc": "Начните верификацию с другого устройства или верифицируйте вручную.",
"view_less": "Свернуть",
"learn_more": "Подробнее",
"verify_manually": "Верифицировать вручную",
"verify_other_title": "Не верифицировано",
"verify_other_desc": "Подтвердите идентичность устройства и получите доступ к зашифрованным сообщениям.",
"verify": "Верифицировать",
"reset": "Сбросить",
"local_backup": "Локальная копия",
"new_password": "Новый пароль",
"confirm_password": "Подтвердите пароль",
"password": "Пароль",
"export": "Экспорт",
"export_title": "Экспорт данных сообщений",
"export_desc": "Сохраните защищённую паролем копию ключей шифрования на устройстве для расшифровки сообщений позже.",
"collapse": "Свернуть",
"expand": "Развернуть",
"import_title": "Импорт данных сообщений",
"import_desc": "Загрузите защищённую паролем копию ключей шифрования с устройства для расшифровки сообщений.",
"import": "Импорт",
"decrypt": "Расшифровать",
"emojis_stickers_title": "Эмодзи и стикеры",
"default_pack": "Пакет по умолчанию",
"unknown": "Неизвестно",
"view": "Открыть",
"favorite_packs": "Избранные пакеты",
"select_pack": "Выбрать пакет",
"select_pack_desc": "Выберите пакеты эмодзи и стикеров из комнат для использования во всех комнатах.",
"select": "Выбрать",
"room_packs": "Пакеты комнат",
"close": "Закрыть",
"select_all": "Выбрать все",
"unselect_all": "Снять выделение",
"no_packs": "Нет пакетов",
"no_packs_desc": "Здесь появятся пакеты из комнат. У вас пока нет комнат с пакетами.",
"apply_error": "Не удалось применить изменения! Попробуйте снова.",
"apply_ready": "Изменения сохранены! Примените, когда будете готовы.",
"apply_changes": "Применить изменения",
"about_title": "О приложении",
"about_tagline": "Ещё один клиент для Matrix.",
"options": "Параметры",
"clear_cache_title": "Очистить кэш и перезагрузить",
"clear_cache_desc": "Удалить все локально сохранённые данные и загрузить заново с сервера.",
"clear_cache": "Очистить кэш",
"credits": "Благодарности",
"devtools_title": "Инструменты разработчика",
"enable_devtools": "Включить инструменты разработчика",
"access_token": "Токен доступа",
"access_token_desc": "Скопировать токен доступа в буфер обмена.",
"account_data": "Данные аккаунта",
"account_data_global": "Глобальные",
"account_data_desc": "Данные, хранящиеся в глобальных данных вашего аккаунта.",
"events": "События",
"total": "Всего: {{count}}",
"add_new": "Добавить"
}
}

View file

@ -1,4 +1,5 @@
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Avatar,
Box,
@ -46,57 +47,26 @@ export enum SettingsPages {
type SettingsMenuItem = {
page: SettingsPages;
name: string;
nameKey: string;
icon: IconSrc;
};
const useSettingsMenuItems = (): SettingsMenuItem[] =>
useMemo(
() => [
{
page: SettingsPages.GeneralPage,
name: 'General',
icon: Icons.Setting,
},
{
page: SettingsPages.AccountPage,
name: 'Account',
icon: Icons.User,
},
{
page: SettingsPages.NotificationPage,
name: 'Notifications',
icon: Icons.Bell,
},
{
page: SettingsPages.DevicesPage,
name: 'Devices',
icon: Icons.Monitor,
},
{
page: SettingsPages.EmojisStickersPage,
name: 'Emojis & Stickers',
icon: Icons.Smile,
},
{
page: SettingsPages.DeveloperToolsPage,
name: 'Developer Tools',
icon: Icons.Terminal,
},
{
page: SettingsPages.AboutPage,
name: 'About',
icon: Icons.Info,
},
],
[]
);
const SETTINGS_MENU_ITEMS: SettingsMenuItem[] = [
{ page: SettingsPages.GeneralPage, nameKey: 'Settings.menu_general', icon: Icons.Setting },
{ page: SettingsPages.AccountPage, nameKey: 'Settings.menu_account', icon: Icons.User },
{ page: SettingsPages.NotificationPage, nameKey: 'Settings.menu_notifications', icon: Icons.Bell },
{ page: SettingsPages.DevicesPage, nameKey: 'Settings.menu_devices', icon: Icons.Monitor },
{ page: SettingsPages.EmojisStickersPage, 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 },
];
type SettingsProps = {
initialPage?: SettingsPages;
requestClose: () => void;
};
export function Settings({ initialPage, requestClose }: SettingsProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const userId = mx.getUserId()!;
@ -111,7 +81,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
if (initialPage) return initialPage;
return screenSize === ScreenSize.Mobile ? undefined : SettingsPages.GeneralPage;
});
const menuItems = useSettingsMenuItems();
const menuItems = SETTINGS_MENU_ITEMS;
const handlePageRequestClose = () => {
if (screenSize === ScreenSize.Mobile) {
@ -136,7 +106,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
/>
</Avatar>
<Text size="H4" truncate>
Settings
{t('Settings.title')}
</Text>
</Box>
<Box shrink="No">
@ -152,7 +122,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
<div style={{ flexGrow: 1 }}>
{menuItems.map((item) => (
<MenuItem
key={item.name}
key={item.nameKey}
variant="Background"
radii="400"
aria-pressed={activePage === item.page}
@ -166,7 +136,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
size="T300"
truncate
>
{item.name}
{t(item.nameKey)}
</Text>
</MenuItem>
))}
@ -184,7 +154,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
before={<Icon src={Icons.Power} size="100" />}
onClick={() => setLogout(true)}
>
<Text size="B400">Logout</Text>
<Text size="B400">{t('Settings.logout')}</Text>
</Button>
{logout && (
<Overlay open backdrop={<OverlayBackdrop />}>

View file

@ -1,4 +1,5 @@
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';
@ -12,6 +13,7 @@ type AboutProps = {
requestClose: () => void;
};
export function About({ requestClose }: AboutProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
return (
@ -20,7 +22,7 @@ export function About({ requestClose }: AboutProps) {
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
About
{t('Settings.about_title')}
</Text>
</Box>
<Box shrink="No">
@ -48,13 +50,13 @@ export function About({ requestClose }: AboutProps) {
<Text size="H3">Vojo</Text>
<Text size="T200">v4.11.1</Text>
</Box>
<Text>Yet another matrix client.</Text>
<Text>{t('Settings.about_tagline')}</Text>
</Box>
</Box>
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
<Text size="L400">{t('Settings.options')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -62,8 +64,8 @@ export function About({ requestClose }: AboutProps) {
gap="400"
>
<SettingTile
title="Clear Cache & Reload"
description="Clear all your locally stored data and reload from server."
title={t('Settings.clear_cache_title')}
description={t('Settings.clear_cache_desc')}
after={
<Button
onClick={() => clearCacheAndReload(mx)}
@ -73,14 +75,14 @@ export function About({ requestClose }: AboutProps) {
radii="300"
outlined
>
<Text size="B300">Clear Cache</Text>
<Text size="B300">{t('Settings.clear_cache')}</Text>
</Button>
}
/>
</SequenceCard>
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Credits</Text>
<Text size="L400">{t('Settings.credits')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"

View file

@ -1,4 +1,5 @@
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';
@ -10,13 +11,14 @@ type AccountProps = {
requestClose: () => void;
};
export function Account({ requestClose }: AccountProps) {
const { t } = useTranslation();
return (
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Account
{t('Settings.account_title')}
</Text>
</Box>
<Box shrink="No">

View file

@ -1,5 +1,6 @@
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';
@ -7,6 +8,7 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
export function ContactInformation() {
const { t } = useTranslation();
const mx = useMatrixClient();
const [threePIdsState, loadThreePIds] = useAsyncCallback(
useCallback(() => mx.getThreePids(), [mx])
@ -22,14 +24,14 @@ export function ContactInformation() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Contact Information</Text>
<Text size="L400">{t('Settings.contact_info')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile title="Email Address" description="Email address attached to your account.">
<SettingTile title={t('Settings.email_address')} description={t('Settings.email_address_desc')}>
<Box>
{emailIds?.map((email) => (
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">

View file

@ -1,5 +1,6 @@
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';
@ -10,6 +11,7 @@ import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
import { useAlive } from '../../../hooks/useAlive';
function IgnoreUserInput({ userList }: { userList: string[] }) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [userId, setUserId] = useState<string>('');
const alive = useAlive();
@ -89,7 +91,7 @@ function IgnoreUserInput({ userList }: { userList: string[] }) {
disabled={ignoring}
>
{ignoring && <Spinner variant="Secondary" size="300" />}
<Text size="B400">Block</Text>
<Text size="B400">{t('Settings.block')}</Text>
</Button>
</Box>
);
@ -129,12 +131,13 @@ function IgnoredUserChip({ userId, userList }: { userId: string; userList: strin
}
export function IgnoredUserList() {
const { t } = useTranslation();
const ignoredUsers = useIgnoredUsers();
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">Blocked Users</Text>
<Text size="L400">{t('Settings.blocked_users')}</Text>
</Box>
<SequenceCard
className={SequenceCardStyle}
@ -143,14 +146,14 @@ export function IgnoredUserList() {
gap="400"
>
<SettingTile
title="Select User"
description="Prevent receiving messages or invites from user by adding their userId."
title={t('Settings.select_user')}
description={t('Settings.select_user_desc')}
>
<Box direction="Column" gap="300">
<IgnoreUserInput userList={ignoredUsers} />
{ignoredUsers.length > 0 && (
<Box direction="Inherit" gap="100">
<Text size="L400">Users</Text>
<Text size="L400">{t('Settings.users')}</Text>
<Box wrap="Wrap" gap="200">
{ignoredUsers.map((userId) => (
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Box, Text, Chip } from 'folds';
import { useTranslation } from 'react-i18next';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
@ -7,12 +8,13 @@ import { SettingTile } from '../../../components/setting-tile';
import { copyToClipboard } from '../../../utils/dom';
export function MatrixId() {
const { t } = useTranslation();
const mx = useMatrixClient();
const userId = mx.getUserId()!;
return (
<Box direction="Column" gap="100">
<Text size="L400">Matrix ID</Text>
<Text size="L400">{t('Settings.matrix_id')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -23,7 +25,7 @@ export function MatrixId() {
title={userId}
after={
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
<Text size="T200">Copy</Text>
<Text size="T200">{t('Settings.copy')}</Text>
</Chip>
}
/>

View file

@ -25,6 +25,7 @@ import {
Spinner,
} 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';
@ -49,6 +50,7 @@ type ProfileProps = {
userId: string;
};
function ProfileAvatar({ profile, userId }: ProfileProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const capabilities = useCapabilities();
@ -91,7 +93,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
<SettingTile
title={
<Text as="span" size="L400">
Avatar
{t('Settings.avatar')}
</Text>
}
after={
@ -123,7 +125,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
radii="300"
disabled={disableSetAvatar}
>
<Text size="B300">Upload</Text>
<Text size="B300">{t('Settings.upload')}</Text>
</Button>
{avatarUrl && (
<Button
@ -134,7 +136,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
disabled={disableSetAvatar}
onClick={() => setAlertRemove(true)}
>
<Text size="B300">Remove</Text>
<Text size="B300">{t('Settings.remove')}</Text>
</Button>
)}
</Box>
@ -183,7 +185,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
size="500"
>
<Box grow="Yes">
<Text size="H4">Remove Avatar</Text>
<Text size="H4">{t('Settings.remove_avatar')}</Text>
</Box>
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
<Icon src={Icons.Cross} />
@ -191,10 +193,10 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="200">
<Text priority="400">Are you sure you want to remove profile avatar?</Text>
<Text priority="400">{t('Settings.remove_avatar_confirm')}</Text>
</Box>
<Button variant="Critical" onClick={handleRemoveAvatar}>
<Text size="B400">Remove</Text>
<Text size="B400">{t('Settings.remove')}</Text>
</Button>
</Box>
</Dialog>
@ -206,6 +208,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
}
function ProfileDisplayName({ profile, userId }: ProfileProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const capabilities = useCapabilities();
const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
@ -248,7 +251,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
<SettingTile
title={
<Text as="span" size="L400">
Display Name
{t('Settings.display_name')}
</Text>
}
>
@ -295,7 +298,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
type="submit"
>
{changingDisplayName && <Spinner variant="Success" fill="Solid" size="300" />}
<Text size="B400">Save</Text>
<Text size="B400">{t('Settings.save')}</Text>
</Button>
</Box>
</Box>
@ -304,13 +307,14 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
}
export function Profile() {
const { t } = useTranslation();
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const profile = useUserProfile(userId);
return (
<Box direction="Column" gap="100">
<Text size="L400">Profile</Text>
<Text size="L400">{t('Settings.profile')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"

View file

@ -1,4 +1,5 @@
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';
@ -13,6 +14,7 @@ type AccountDataProps = {
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())
@ -27,7 +29,7 @@ export function AccountData({ expand, onExpandToggle, onSelect }: AccountDataPro
return (
<Box direction="Column" gap="100">
<Text size="L400">Account Data</Text>
<Text size="L400">{t('Settings.account_data')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -35,8 +37,8 @@ export function AccountData({ expand, onExpandToggle, onSelect }: AccountDataPro
gap="400"
>
<SettingTile
title="Global"
description="Data stored in your global account data."
title={t('Settings.account_data_global')}
description={t('Settings.account_data_desc')}
after={
<Button
onClick={() => onExpandToggle(!expand)}
@ -49,15 +51,15 @@ export function AccountData({ expand, onExpandToggle, onSelect }: AccountDataPro
<Icon src={expand ? Icons.ChevronTop : Icons.ChevronBottom} size="100" filled />
}
>
<Text size="B300">{expand ? 'Collapse' : 'Expand'}</Text>
<Text size="B300">{expand ? t('Settings.collapse') : t('Settings.expand')}</Text>
</Button>
}
/>
{expand && (
<Box direction="Column" gap="100">
<Box justifyContent="SpaceBetween">
<Text size="L400">Events</Text>
<Text size="L400">Total: {accountDataTypes.length}</Text>
<Text size="L400">{t('Settings.events')}</Text>
<Text size="L400">{t('Settings.total', { count: accountDataTypes.length })}</Text>
</Box>
<CutoutCard>
<MenuItem
@ -70,7 +72,7 @@ export function AccountData({ expand, onExpandToggle, onSelect }: AccountDataPro
>
<Box grow="Yes">
<Text size="T200" truncate>
Add New
{t('Settings.add_new')}
</Text>
</Box>
</MenuItem>

View file

@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react';
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';
@ -18,6 +19,7 @@ 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);
@ -47,7 +49,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Developer Tools
{t('Settings.devtools_title')}
</Text>
</Box>
<Box shrink="No">
@ -62,7 +64,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
<PageContent>
<Box direction="Column" gap="700">
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
<Text size="L400">{t('Settings.options')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -70,7 +72,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
gap="400"
>
<SettingTile
title="Enable Developer Tools"
title={t('Settings.enable_devtools')}
after={
<Switch
variant="Primary"
@ -88,8 +90,8 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
gap="400"
>
<SettingTile
title="Access Token"
description="Copy access token to clipboard."
title={t('Settings.access_token')}
description={t('Settings.access_token_desc')}
after={
<Button
onClick={() =>
@ -101,7 +103,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
radii="300"
outlined
>
<Text size="B300">Copy</Text>
<Text size="B300">{t('Settings.copy')}</Text>
</Button>
}
/>

View file

@ -18,6 +18,7 @@ import {
import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api';
import FocusTrap from 'focus-trap-react';
import { IMyDevice, MatrixError } from 'matrix-js-sdk';
import { useTranslation } from 'react-i18next';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../../utils/time';
@ -43,17 +44,18 @@ export function DeviceTilePlaceholder() {
}
function DeviceActiveTime({ ts }: { ts: number }) {
const { t } = useTranslation();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
return (
<Text className={BreakWord} size="T200">
<Text size="Inherit" as="span" priority="300">
{'Last activity: '}
{t('Settings.last_activity')}
</Text>
<>
{today(ts) && 'Today'}
{yesterday(ts) && 'Yesterday'}
{today(ts) && t('Settings.today')}
{yesterday(ts) && t('Settings.yesterday')}
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts, dateFormatString)}{' '}
{timeHourMinute(ts, hour24Clock)}
</>
@ -62,16 +64,17 @@ function DeviceActiveTime({ ts }: { ts: number }) {
}
function DeviceDetails({ device }: { device: IMyDevice }) {
const { t } = useTranslation();
return (
<>
{typeof device.device_id === 'string' && (
<Text className={BreakWord} size="T200" priority="300">
Device ID: <i>{device.device_id}</i>
{t('Settings.device_id')}<i>{device.device_id}</i>
</Text>
)}
{typeof device.last_seen_ip === 'string' && (
<Text className={BreakWord} size="T200" priority="300">
IP Address: <i>{device.last_seen_ip}</i>
{t('Settings.ip_address')}<i>{device.last_seen_ip}</i>
</Text>
)}
</>
@ -82,6 +85,7 @@ type DeviceKeyDetailsProps = {
crypto: CryptoApi;
};
export function DeviceKeyDetails({ crypto }: DeviceKeyDetailsProps) {
const { t } = useTranslation();
const [keysState, loadKeys] = useAsyncCallback(
useCallback(() => {
const keys = crypto.getOwnDeviceKeys();
@ -97,8 +101,8 @@ export function DeviceKeyDetails({ crypto }: DeviceKeyDetailsProps) {
return (
<Text className={BreakWord} size="T200" priority="300">
Device Key:{' '}
<i>{keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : 'loading...'}</i>
{t('Settings.device_key')}
<i>{keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : t('Settings.loading')}</i>
</Text>
);
}
@ -110,6 +114,7 @@ type DeviceRenameProps = {
refreshDeviceList: () => Promise<void>;
};
function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceRenameProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [renameState, rename] = useAsyncCallback<void, MatrixError, [string]>(
@ -145,7 +150,7 @@ function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceR
return (
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
<Text size="L400">Device Name</Text>
<Text size="L400">{t('Settings.device_name')}</Text>
<Box gap="200">
<Box grow="Yes" direction="Column">
<Input
@ -169,7 +174,7 @@ function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceR
disabled={renaming}
before={renaming && <Spinner size="100" variant="Success" fill="Solid" />}
>
<Text size="B300">Save</Text>
<Text size="B300">{t('Settings.save')}</Text>
</Button>
<Button
type="button"
@ -180,7 +185,7 @@ function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceR
onClick={onCancel}
disabled={renaming}
>
<Text size="B300">Cancel</Text>
<Text size="B300">{t('Settings.cancel')}</Text>
</Button>
</Box>
</Box>
@ -189,13 +194,14 @@ function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceR
{renameState.error.message}
</Text>
) : (
<Text size="T200">Device names are visible to public.</Text>
<Text size="T200">{t('Settings.device_name_public')}</Text>
)}
</Box>
);
}
export function DeviceLogoutBtn() {
const { t } = useTranslation();
const [prompt, setPrompt] = useState(false);
const handleClose = () => setPrompt(false);
@ -203,7 +209,7 @@ export function DeviceLogoutBtn() {
return (
<>
<Chip variant="Secondary" fill="Soft" radii="Pill" onClick={() => setPrompt(true)}>
<Text size="B300">Logout</Text>
<Text size="B300">{t('Settings.logout')}</Text>
</Chip>
{prompt && (
<Overlay open backdrop={<OverlayBackdrop />}>
@ -236,6 +242,7 @@ export function DeviceDeleteBtn({
onDeleteToggle,
disabled,
}: DeviceDeleteBtnProps) {
const { t } = useTranslation();
return deleted ? (
<Chip
variant="Critical"
@ -244,7 +251,7 @@ export function DeviceDeleteBtn({
onClick={() => onDeleteToggle(deviceId)}
disabled={disabled}
>
<Text size="B300">Undo</Text>
<Text size="B300">{t('Settings.undo')}</Text>
</Chip>
) : (
<Chip
@ -275,6 +282,7 @@ export function DeviceTile({
options,
children,
}: DeviceTileProps) {
const { t } = useTranslation();
const activeTs = device.last_seen_ts;
const [details, setDetails] = useState(false);
const [edit, setEdit] = useState(false);
@ -307,7 +315,7 @@ export function DeviceTile({
onClick={() => setEdit(true)}
disabled={disabled}
>
<Text size="B300">Edit</Text>
<Text size="B300">{t('Settings.edit')}</Text>
</Chip>
)}
</Box>

View file

@ -1,4 +1,5 @@
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 { SequenceCard } from '../../../components/sequence-card';
@ -40,6 +41,7 @@ type DevicesProps = {
requestClose: () => void;
};
export function Devices({ requestClose }: DevicesProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const crypto = mx.getCrypto();
const crossSigningActive = useCrossSigningActive();
@ -70,7 +72,7 @@ export function Devices({ requestClose }: DevicesProps) {
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Devices
{t('Settings.devices_title')}
</Text>
</Box>
<Box shrink="No">
@ -85,7 +87,7 @@ export function Devices({ requestClose }: DevicesProps) {
<PageContent>
<Box direction="Column" gap="700">
<Box direction="Column" gap="100">
<Text size="L400">Security</Text>
<Text size="L400">{t('Settings.security')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -93,8 +95,8 @@ export function Devices({ requestClose }: DevicesProps) {
gap="400"
>
<SettingTile
title="Device Verification"
description="To verify device identity and grant access to encrypted messages."
title={t('Settings.device_verification')}
description={t('Settings.device_verification_desc')}
after={
<>
<EnableVerification visible={!crossSigningActive} />
@ -113,7 +115,7 @@ export function Devices({ requestClose }: DevicesProps) {
</SequenceCard>
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Current</Text>
<Text size="L400">{t('Settings.current')}</Text>
{currentDevice ? (
<SequenceCard
className={SequenceCardStyle}

View file

@ -1,4 +1,5 @@
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';
@ -13,6 +14,7 @@ import { useAlive } from '../../../hooks/useAlive';
import { useFilePicker } from '../../../hooks/useFilePicker';
function ExportKeys() {
const { t } = useTranslation();
const mx = useMatrixClient();
const alive = useAlive();
@ -66,7 +68,7 @@ function ExportKeys() {
{(match, doMatch, passRef, confPassRef) => (
<>
<Box grow="Yes" direction="Column" gap="100">
<Text size="L400">New Password</Text>
<Text size="L400">{t('Settings.new_password')}</Text>
<PasswordInput
ref={passRef}
name="passwordInput"
@ -80,7 +82,7 @@ function ExportKeys() {
/>
</Box>
<Box grow="Yes" direction="Column" gap="100">
<Text size="L400">Confirm Password</Text>
<Text size="L400">{t('Settings.confirm_password')}</Text>
<PasswordInput
ref={confPassRef}
style={{ color: match ? undefined : color.Critical.Main }}
@ -107,7 +109,7 @@ function ExportKeys() {
before={exporting ? <Spinner size="200" variant="Secondary" fill="Soft" /> : undefined}
>
<Text as="span" size="B400">
Export
{t('Settings.export')}
</Text>
</Button>
</Box>
@ -122,13 +124,14 @@ function ExportKeys() {
}
function ExportKeysTile() {
const { t } = useTranslation();
const [expand, setExpand] = useState(false);
return (
<>
<SettingTile
title="Export Messages Data"
description="Save password protected copy of encryption data on your device to decrypt messages later."
title={t('Settings.export_title')}
description={t('Settings.export_desc')}
after={
<Box>
<Button
@ -144,7 +147,7 @@ function ExportKeysTile() {
}
>
<Text as="span" size="B300" truncate>
{expand ? 'Collapse' : 'Expand'}
{expand ? t('Settings.collapse') : t('Settings.expand')}
</Text>
</Button>
</Box>
@ -160,6 +163,7 @@ type ImportKeysProps = {
onDone?: () => void;
};
function ImportKeys({ file, onDone }: ImportKeysProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const alive = useAlive();
@ -209,7 +213,7 @@ function ImportKeys({ file, onDone }: ImportKeysProps) {
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
<Box gap="200" alignItems="End">
<Box grow="Yes" direction="Column" gap="100">
<Text size="L400">Password</Text>
<Text size="L400">{t('Settings.password')}</Text>
<PasswordInput
name="passwordInput"
size="400"
@ -231,7 +235,7 @@ function ImportKeys({ file, onDone }: ImportKeysProps) {
before={decrypting ? <Spinner size="200" variant="Secondary" fill="Soft" /> : undefined}
>
<Text as="span" size="B400">
Decrypt
{t('Settings.decrypt')}
</Text>
</Button>
</Box>
@ -246,6 +250,7 @@ function ImportKeys({ file, onDone }: ImportKeysProps) {
}
function ImportKeysTile() {
const { t } = useTranslation();
const [file, setFile] = useState<File>();
const pickFile = useFilePicker(setFile);
@ -256,8 +261,8 @@ function ImportKeysTile() {
return (
<>
<SettingTile
title="Import Messages Data"
description="Load password protected copy of encryption data from device to decrypt your messages."
title={t('Settings.import_title')}
description={t('Settings.import_desc')}
after={
<Box>
{file ? (
@ -288,7 +293,7 @@ function ImportKeysTile() {
before={<Icon size="100" src={Icons.ArrowRight} />}
>
<Text as="span" size="B300">
Import
{t('Settings.import')}
</Text>
</Button>
)}
@ -301,9 +306,10 @@ function ImportKeysTile() {
}
export function LocalBackup() {
const { t } = useTranslation();
return (
<Box direction="Column" gap="100">
<Text size="L400">Local Backup</Text>
<Text size="L400">{t('Settings.local_backup')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"

View file

@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react';
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';
@ -22,6 +23,7 @@ type OtherDevicesProps = {
showVerification?: boolean;
};
export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const crypto = mx.getCrypto();
const authMetadata = useAuthMetadata();
@ -104,7 +106,7 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
return devices.length > 0 ? (
<>
<Box direction="Column" gap="100">
<Text size="L400">Others</Text>
<Text size="L400">{t('Settings.others')}</Text>
{authMetadata && (
<SequenceCard
className={SequenceCardStyle}
@ -113,8 +115,8 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
gap="400"
>
<SettingTile
title="Device Dashboard"
description="Manage your devices on OIDC dashboard."
title={t('Settings.device_dashboard')}
description={t('Settings.device_dashboard_desc')}
after={
<Button
size="300"
@ -124,7 +126,7 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
outlined
onClick={handleDashboardOIDC}
>
<Text size="B300">Open</Text>
<Text size="B300">{t('Settings.open')}</Text>
</Button>
}
/>
@ -198,11 +200,11 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
<Box grow="Yes" direction="Column">
{deleteError ? (
<Text size="T200">
<b>Failed to logout devices! Please try again. {deleteError.message}</b>
<b>{t('Settings.delete_devices_error', { message: deleteError.message })}</b>
</Text>
) : (
<Text size="T200">
<b>Logout from selected devices. ({deleted.size} selected)</b>
<b>{t('Settings.delete_devices_confirm', { count: deleted.size })}</b>
</Text>
)}
{authData && (
@ -210,7 +212,7 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
authData={authData}
unsupported={() => (
<Text size="T200">
Authentication steps to perform this action are not supported by client.
{t('Settings.delete_devices_unsupported')}
</Text>
)}
>
@ -234,7 +236,7 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
disabled={deleting}
onClick={handleCancelDelete}
>
<Text size="B300">Cancel</Text>
<Text size="B300">{t('Settings.cancel')}</Text>
</Button>
<Button
size="300"
@ -244,7 +246,7 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
before={deleting && <Spinner variant="Critical" fill="Solid" size="100" />}
onClick={() => deleteDevices()}
>
<Text size="B300">Logout</Text>
<Text size="B300">{t('Settings.logout')}</Text>
</Button>
</Box>
</Box>

View file

@ -19,6 +19,7 @@ import {
MenuItem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { CryptoApi, VerificationRequest } from 'matrix-js-sdk/lib/crypto-api';
import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus';
import { InfoCard } from '../../../components/info-card';
@ -44,6 +45,7 @@ export function VerificationStatusBadge({
verificationStatus,
otherUnverifiedCount,
}: VerificationStatusBadgeProps) {
const { t } = useTranslation();
if (
verificationStatus === VerificationStatus.Unknown ||
typeof otherUnverifiedCount !== 'number'
@ -53,7 +55,7 @@ export function VerificationStatusBadge({
if (verificationStatus === VerificationStatus.Unverified) {
return (
<Badge variant="Critical" fill="Solid" size="500">
<Text size="L400">Unverified</Text>
<Text size="L400">{t('Settings.unverified')}</Text>
</Badge>
);
}
@ -61,36 +63,37 @@ export function VerificationStatusBadge({
if (otherUnverifiedCount > 0) {
return (
<Badge variant="Warning" fill="Solid" size="500">
<Text size="L400">{otherUnverifiedCount} Unverified</Text>
<Text size="L400">{t('Settings.unverified_count', { count: otherUnverifiedCount })}</Text>
</Badge>
);
}
return (
<Badge variant="Success" fill="Solid" size="500">
<Text size="L400">Verified</Text>
<Text size="L400">{t('Settings.verified')}</Text>
</Badge>
);
}
function LearnStartVerificationFromOtherDevice() {
const { t } = useTranslation();
return (
<Box direction="Column">
<Text size="T200">Steps to verify from other device.</Text>
<Text size="T200">{t('Settings.verify_steps_title')}</Text>
<Text as="div" size="T200">
<ul style={{ margin: `${config.space.S100} 0` }}>
<li>Open your other verified device.</li>
<li>{t('Settings.verify_step_1')}</li>
<li>
Open <i>Settings</i>.
Open <i>{t('Settings.verify_step_2_settings')}</i>.
</li>
<li>
Find this device in <i>Devices/Sessions</i> section.
Find this device in <i>{t('Settings.verify_step_3_section')}</i> section.
</li>
<li>Initiate verification.</li>
<li>{t('Settings.verify_step_4')}</li>
</ul>
</Text>
<Text size="T200">
If you do not have any verified device press the <i>&quot;Verify Manually&quot;</i> button.
{t('Settings.verify_no_device')}
</Text>
</Box>
);
@ -104,6 +107,7 @@ export function VerifyCurrentDeviceTile({
secretStorageKeyId,
secretStorageKeyContent,
}: VerifyCurrentDeviceTileProps) {
const { t } = useTranslation();
const [learnMore, setLearnMore] = useState(false);
const [manualVerification, setManualVerification] = useState(false);
@ -113,12 +117,12 @@ export function VerifyCurrentDeviceTile({
<>
<InfoCard
variant="Critical"
title="Unverified"
title={t('Settings.verify_current_title')}
description={
<>
Start verification from other device or verify manually.{' '}
{t('Settings.verify_current_desc')}{' '}
<Text as="a" size="T200" onClick={() => setLearnMore(!learnMore)}>
<b>{learnMore ? 'View Less' : 'Learn More'}</b>
<b>{learnMore ? t('Settings.view_less') : t('Settings.learn_more')}</b>
</Text>
</>
}
@ -133,7 +137,7 @@ export function VerifyCurrentDeviceTile({
onClick={() => setManualVerification(true)}
>
<Text as="span" size="B300">
Verify Manually
{t('Settings.verify_manually')}
</Text>
</Button>
)
@ -167,6 +171,7 @@ type VerifyOtherDeviceTileProps = {
deviceId: string;
};
export function VerifyOtherDeviceTile({ crypto, deviceId }: VerifyOtherDeviceTileProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [requestState, setRequestState] = useState<AsyncState<VerificationRequest, Error>>({
status: AsyncStatus.Idle,
@ -190,8 +195,8 @@ export function VerifyOtherDeviceTile({ crypto, deviceId }: VerifyOtherDeviceTil
return (
<InfoCard
variant="Warning"
title="Unverified"
description="Verify device identity and grant access to encrypted messages."
title={t('Settings.verify_other_title')}
description={t('Settings.verify_other_desc')}
after={
<Button
size="300"
@ -202,7 +207,7 @@ export function VerifyOtherDeviceTile({ crypto, deviceId }: VerifyOtherDeviceTil
disabled={requesting}
>
<Text as="span" size="B300">
Verify
{t('Settings.verify')}
</Text>
</Button>
}
@ -221,6 +226,7 @@ type EnableVerificationProps = {
visible: boolean;
};
export function EnableVerification({ visible }: EnableVerificationProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const handleCancel = useCallback(() => setOpen(false), []);
@ -230,7 +236,7 @@ export function EnableVerification({ visible }: EnableVerificationProps) {
{visible && (
<Button size="300" radii="300" onClick={() => setOpen(true)}>
<Text as="span" size="B300">
Enable
{t('Settings.enable')}
</Text>
</Button>
)}
@ -254,6 +260,7 @@ export function EnableVerification({ visible }: EnableVerificationProps) {
}
export function DeviceVerificationOptions() {
const { t } = useTranslation();
const [menuCords, setMenuCords] = useState<RectCords>();
const authMetadata = useAuthMetadata();
const accountManagementActions = useAccountManagementActions();
@ -324,7 +331,7 @@ export function DeviceVerificationOptions() {
fill="None"
>
<Text as="span" size="T300" truncate>
Reset
{t('Settings.reset')}
</Text>
</MenuItem>
</Box>

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { GlobalPacks } from './GlobalPacks';
@ -10,6 +11,7 @@ type EmojisStickersProps = {
requestClose: () => void;
};
export function EmojisStickers({ requestClose }: EmojisStickersProps) {
const { t } = useTranslation();
const [imagePack, setImagePack] = useState<ImagePack>();
const handleImagePackViewClose = () => {
@ -26,7 +28,7 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) {
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Emojis & Stickers
{t('Settings.emojis_stickers_title')}
</Text>
</Box>
<Box shrink="No">

View file

@ -1,4 +1,5 @@
import React, { MouseEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Text,
@ -53,6 +54,7 @@ function GlobalPackSelector({
useAuthentication: boolean;
onSelect: (addresses: PackAddress[]) => void;
}) {
const { t } = useTranslation();
const mx = useMatrixClient();
const roomToPacks = useMemo(() => {
const rToP = new Map<string, ImagePack[]>();
@ -107,7 +109,7 @@ function GlobalPackSelector({
<Header size="400" variant="Surface" style={{ padding: `0 ${config.space.S300}` }}>
<Box grow="Yes">
<Text size="L400" truncate>
Room Packs
{t('Settings.room_packs')}
</Text>
</Box>
<Box shrink="No">
@ -117,7 +119,7 @@ function GlobalPackSelector({
outlined={hasSelected}
onClick={() => onSelect(selected)}
>
<Text size="B300">{hasSelected ? 'Save' : 'Close'}</Text>
<Text size="B300">{hasSelected ? t('Settings.save') : t('Settings.close')}</Text>
</Chip>
</Box>
</Header>
@ -162,7 +164,7 @@ function GlobalPackSelector({
addSelected(roomPackAddresses);
}}
>
<Text size="B300">{allSelected ? 'Unselect All' : 'Select All'}</Text>
<Text size="B300">{allSelected ? t('Settings.unselect_all') : t('Settings.select_all')}</Text>
</Chip>
</Box>
</Box>
@ -184,7 +186,7 @@ function GlobalPackSelector({
gap="400"
>
<SettingTile
title={pack.meta.name ?? 'Unknown'}
title={pack.meta.name ?? t('Settings.unknown')}
description={<span className={LineClamp2}>{pack.meta.attribution}</span>}
before={
<Box alignItems="Center" gap="300">
@ -232,10 +234,10 @@ function GlobalPackSelector({
}}
>
<Text size="H5" align="Center">
No Packs
{t('Settings.no_packs')}
</Text>
<Text size="T200" align="Center">
Pack from rooms will appear here. You do not have any room with packs yet.
{t('Settings.no_packs_desc')}
</Text>
</Box>
</SequenceCard>
@ -251,6 +253,7 @@ type GlobalPacksProps = {
onViewPack: (imagePack: ImagePack) => void;
};
export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const globalPacks = useGlobalImagePacks();
@ -360,7 +363,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
<SettingTile
title={
<span style={{ textDecoration: removed ? 'line-through' : undefined }}>
{pack.meta.name ?? 'Unknown'}
{pack.meta.name ?? t('Settings.unknown')}
</span>
}
description={<span className={LineClamp2}>{pack.meta.attribution}</span>}
@ -408,7 +411,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
outlined
onClick={() => onViewPack(pack)}
>
<Text size="B300">View</Text>
<Text size="B300">{t('Settings.view')}</Text>
</Button>
)
}
@ -420,7 +423,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
return (
<>
<Box direction="Column" gap="100">
<Text size="L400">Favorite Packs</Text>
<Text size="L400">{t('Settings.favorite_packs')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -428,8 +431,8 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
gap="400"
>
<SettingTile
title="Select Pack"
description="Pick emojis and stickers pack from rooms to use in all rooms."
title={t('Settings.select_pack')}
description={t('Settings.select_pack_desc')}
after={
<>
<Button
@ -440,7 +443,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
radii="300"
outlined
>
<Text size="B300">Select</Text>
<Text size="B300">{t('Settings.select')}</Text>
</Button>
<PopOut
anchor={menuCords}
@ -502,11 +505,11 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
<Box grow="Yes" direction="Column">
{applyState.status === AsyncStatus.Error ? (
<Text size="T200">
<b>Failed to apply changes! Please try again.</b>
<b>{t('Settings.apply_error')}</b>
</Text>
) : (
<Text size="T200">
<b>Changes saved! Apply when ready.</b>
<b>{t('Settings.apply_ready')}</b>
</Text>
)}
</Box>
@ -519,7 +522,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
disabled={applyingChanges}
onClick={resetChanges}
>
<Text size="B300">Reset</Text>
<Text size="B300">{t('Settings.reset')}</Text>
</Button>
<Button
size="300"
@ -529,7 +532,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
before={applyingChanges && <Spinner variant="Success" fill="Solid" size="100" />}
onClick={applyChanges}
>
<Text size="B300">Apply Changes</Text>
<Text size="B300">{t('Settings.apply_changes')}</Text>
</Button>
</Box>
</Box>

View file

@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Avatar, AvatarFallback, AvatarImage, Box, Button, Icon, Icons, Text } from 'folds';
import { useUserImagePack } from '../../../hooks/useImagePacks';
import { SequenceCard } from '../../../components/sequence-card';
@ -13,6 +14,7 @@ type UserPackProps = {
onViewPack: (imagePack: ImagePack) => void;
};
export function UserPack({ onViewPack }: UserPackProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
@ -31,7 +33,7 @@ export function UserPack({ onViewPack }: UserPackProps) {
return (
<Box direction="Column" gap="100">
<Text size="L400">Default Pack</Text>
<Text size="L400">{t('Settings.default_pack')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -39,7 +41,7 @@ export function UserPack({ onViewPack }: UserPackProps) {
gap="400"
>
<SettingTile
title={userPack?.meta.name ?? 'Unknown'}
title={userPack?.meta.name ?? t('Settings.unknown')}
description={userPack?.meta.attribution}
before={
<Avatar size="300" radii="300">
@ -61,7 +63,7 @@ export function UserPack({ onViewPack }: UserPackProps) {
outlined
onClick={handleView}
>
<Text size="B300">View</Text>
<Text size="B300">{t('Settings.view')}</Text>
</Button>
}
/>

View file

@ -28,6 +28,7 @@ import {
toRem,
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
import { useTranslation } from 'react-i18next';
import FocusTrap from 'focus-trap-react';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
@ -139,6 +140,7 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
}
function SystemThemePreferences() {
const { t } = useTranslation();
const themeKind = useSystemThemeKind();
const themeNames = useThemeNames();
const themes = useThemes();
@ -174,7 +176,7 @@ function SystemThemePreferences() {
return (
<Box wrap="Wrap" gap="400">
<SettingTile
title="Light Theme:"
title={t('Settings.light_theme')}
after={
<Chip
variant={themeKind === ThemeKind.Light ? 'Primary' : 'Secondary'}
@ -215,7 +217,7 @@ function SystemThemePreferences() {
}
/>
<SettingTile
title="Dark Theme:"
title={t('Settings.dark_theme')}
after={
<Chip
variant={themeKind === ThemeKind.Dark ? 'Primary' : 'Secondary'}
@ -304,13 +306,14 @@ function PageZoomInput() {
}
function Appearance() {
const { t } = useTranslation();
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
return (
<Box direction="Column" gap="100">
<Text size="L400">Appearance</Text>
<Text size="L400">{t('Settings.appearance')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -318,8 +321,8 @@ function Appearance() {
gap="400"
>
<SettingTile
title="System Theme"
description="Choose between light and dark theme based on system preference."
title={t('Settings.system_theme')}
description={t('Settings.system_theme_desc')}
after={<Switch variant="Primary" value={systemTheme} onChange={setSystemTheme} />}
/>
{systemTheme && <SystemThemePreferences />}
@ -327,28 +330,28 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Theme"
description="Theme to use when system theme is not enabled."
title={t('Settings.theme')}
description={t('Settings.theme_desc')}
after={<SelectTheme disabled={systemTheme} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Monochrome Mode"
title={t('Settings.monochrome_mode')}
after={<Switch variant="Primary" value={monochromeMode} onChange={setMonochromeMode} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Twitter Emoji"
title={t('Settings.twitter_emoji')}
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
<SettingTile title={t('Settings.page_zoom')} after={<PageZoomInput />} />
</SequenceCard>
</Box>
);
@ -359,6 +362,7 @@ type DateHintProps = {
handleReset: () => void;
};
function DateHint({ hasChanges, handleReset }: DateHintProps) {
const { t } = useTranslation();
const [anchor, setAnchor] = useState<RectCords>();
const categoryPadding = { padding: config.space.S200, paddingTop: 0 };
@ -381,26 +385,26 @@ function DateHint({ hasChanges, handleReset }: DateHintProps) {
>
<Menu style={{ maxHeight: '85vh', overflowY: 'auto' }}>
<Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
<Text size="L400">Formatting</Text>
<Text size="L400">{t('Settings.formatting')}</Text>
</Header>
<Box direction="Column">
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">Year</Text>
<Text size="L400">{t('Settings.year')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
YY
<Text as="span" size="Inherit" priority="300">
{': '}
Two-digit year
{t('Settings.two_digit_year')}
</Text>{' '}
</Text>
<Text size="T300">
YYYY
<Text as="span" size="Inherit" priority="300">
{': '}Four-digit year
{': '}{t('Settings.four_digit_year')}
</Text>
</Text>
</Box>
@ -408,31 +412,31 @@ function DateHint({ hasChanges, handleReset }: DateHintProps) {
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">Month</Text>
<Text size="L400">{t('Settings.month')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
M
<Text as="span" size="Inherit" priority="300">
{': '}The month
{': '}{t('Settings.the_month')}
</Text>
</Text>
<Text size="T300">
MM
<Text as="span" size="Inherit" priority="300">
{': '}Two-digit month
{': '}{t('Settings.two_digit_month')}
</Text>{' '}
</Text>
<Text size="T300">
MMM
<Text as="span" size="Inherit" priority="300">
{': '}Short month name
{': '}{t('Settings.short_month_name')}
</Text>
</Text>
<Text size="T300">
MMMM
<Text as="span" size="Inherit" priority="300">
{': '}Full month name
{': '}{t('Settings.full_month_name')}
</Text>
</Text>
</Box>
@ -440,50 +444,50 @@ function DateHint({ hasChanges, handleReset }: DateHintProps) {
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">Day of the Month</Text>
<Text size="L400">{t('Settings.day_of_month')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
D
<Text as="span" size="Inherit" priority="300">
{': '}Day of the month
{': '}{t('Settings.day_of_month_val')}
</Text>
</Text>
<Text size="T300">
DD
<Text as="span" size="Inherit" priority="300">
{': '}Two-digit day of the month
{': '}{t('Settings.two_digit_day')}
</Text>
</Text>
</Box>
</Box>
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">Day of the Week</Text>
<Text size="L400">{t('Settings.day_of_week')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
d
<Text as="span" size="Inherit" priority="300">
{': '}Day of the week (Sunday = 0)
{': '}{t('Settings.day_of_week_sunday')}
</Text>
</Text>
<Text size="T300">
dd
<Text as="span" size="Inherit" priority="300">
{': '}Two-letter day name
{': '}{t('Settings.two_letter_day')}
</Text>
</Text>
<Text size="T300">
ddd
<Text as="span" size="Inherit" priority="300">
{': '}Short day name
{': '}{t('Settings.short_day_name')}
</Text>
</Text>
<Text size="T300">
dddd
<Text as="span" size="Inherit" priority="300">
{': '}Full day name
{': '}{t('Settings.full_day_name')}
</Text>
</Text>
</Box>
@ -526,6 +530,7 @@ type CustomDateFormatProps = {
onChange: (format: string) => void;
};
function CustomDateFormat({ value, onChange }: CustomDateFormatProps) {
const { t } = useTranslation();
const [dateFormatCustom, setDateFormatCustom] = useState(value);
useEffect(() => {
@ -579,7 +584,7 @@ function CustomDateFormat({ value, onChange }: CustomDateFormatProps) {
disabled={!hasChanges}
type="submit"
>
<Text size="B400">Save</Text>
<Text size="B400">{t('Settings.save')}</Text>
</Button>
</Box>
</SettingTile>
@ -591,11 +596,12 @@ type PresetDateFormatProps = {
onChange: (format: string) => void;
};
function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
const { t } = useTranslation();
const [menuCords, setMenuCords] = useState<RectCords>();
const dateFormatItems = useDateFormatItems();
const getDisplayDate = (format: string): string =>
format !== '' ? dayjs().format(format) : 'Custom';
format !== '' ? dayjs().format(format) : t('Settings.custom');
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCords(evt.currentTarget.getBoundingClientRect());
@ -662,6 +668,7 @@ function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
}
function SelectDateFormat() {
const { t } = useTranslation();
const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString);
const customDateFormat = selectedDateFormat === '';
@ -676,7 +683,7 @@ function SelectDateFormat() {
return (
<>
<SettingTile
title="Date Format"
title={t('Settings.date_format')}
description={customDateFormat ? dayjs().format(dateFormatString) : ''}
after={<PresetDateFormat value={selectedDateFormat} onChange={handlePresetChange} />}
/>
@ -688,14 +695,15 @@ function SelectDateFormat() {
}
function DateAndTime() {
const { t } = useTranslation();
const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
return (
<Box direction="Column" gap="100">
<Text size="L400">Date & Time</Text>
<Text size="L400">{t('Settings.date_time')}</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="24-Hour Time Format"
title={t('Settings.hour_24')}
after={<Switch variant="Primary" value={hour24Clock} onChange={setHour24Clock} />}
/>
</SequenceCard>
@ -708,32 +716,31 @@ function DateAndTime() {
}
function Editor() {
const { t } = useTranslation();
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
return (
<Box direction="Column" gap="100">
<Text size="L400">Editor</Text>
<Text size="L400">{t('Settings.editor')}</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="ENTER for Newline"
description={`Use ${
isMacOS() ? KeySymbol.Command : 'Ctrl'
} + ENTER to send message and ENTER for newline.`}
title={t('Settings.enter_newline')}
description={t('Settings.enter_newline_desc', { key: isMacOS() ? KeySymbol.Command : 'Ctrl' })}
after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Markdown Formatting"
title={t('Settings.markdown')}
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide Typing & Read Receipts"
description="Turn off both typing status and read receipts to keep your activity private."
title={t('Settings.hide_activity')}
description={t('Settings.hide_activity_desc')}
after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
/>
</SequenceCard>
@ -880,6 +887,7 @@ function SelectMessageSpacing() {
}
function Messages() {
const { t } = useTranslation();
const [legacyUsernameColor, setLegacyUsernameColor] = useSetting(
settingsAtom,
'legacyUsernameColor'
@ -899,16 +907,16 @@ function Messages() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Messages</Text>
<Text size="L400">{t('Settings.messages')}</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Message Layout" after={<SelectMessageLayout />} />
<SettingTile title={t('Settings.message_layout')} after={<SelectMessageLayout />} />
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Message Spacing" after={<SelectMessageSpacing />} />
<SettingTile title={t('Settings.message_spacing')} after={<SelectMessageSpacing />} />
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Legacy Username Color"
title={t('Settings.legacy_username_color')}
after={
<Switch
variant="Primary"
@ -920,7 +928,7 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide Membership Change"
title={t('Settings.hide_membership')}
after={
<Switch
variant="Primary"
@ -932,7 +940,7 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide Profile Change"
title={t('Settings.hide_profile')}
after={
<Switch
variant="Primary"
@ -944,7 +952,7 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Disable Media Auto Load"
title={t('Settings.disable_media_auto_load')}
after={
<Switch
variant="Primary"
@ -956,19 +964,19 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Url Preview"
title={t('Settings.url_preview')}
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Url Preview in Encrypted Room"
title={t('Settings.url_preview_encrypted')}
after={<Switch variant="Primary" value={encUrlPreview} onChange={setEncUrlPreview} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Show Hidden Events"
title={t('Settings.show_hidden_events')}
after={
<Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
}
@ -982,13 +990,14 @@ type GeneralProps = {
requestClose: () => void;
};
export function General({ requestClose }: GeneralProps) {
const { t } = useTranslation();
return (
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
General
{t('Settings.general_title')}
</Text>
</Box>
<Box shrink="No">

View file

@ -1,4 +1,5 @@
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';
@ -73,6 +74,7 @@ function AllMessagesModeSwitcher({
}
export function AllMessagesNotifications() {
const { t } = useTranslation();
const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
const pushRules = useMemo(
() => pushRulesEvt?.getContent<IPushRules>() ?? { global: {} },
@ -82,9 +84,9 @@ export function AllMessagesNotifications() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">All Messages</Text>
<Text size="L400">{t('Settings.all_messages')}</Text>
<Box gap="100">
<Text size="T200">Badge: </Text>
<Text size="T200">{t('Settings.badge')}</Text>
<Badge radii="300" variant="Secondary" fill="Solid">
<Text size="L400">1</Text>
</Badge>
@ -97,7 +99,7 @@ export function AllMessagesNotifications() {
gap="400"
>
<SettingTile
title="1-to-1 Chats"
title={t('Settings.one_to_one')}
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.DM} oneToOne />}
/>
</SequenceCard>
@ -108,7 +110,7 @@ export function AllMessagesNotifications() {
gap="400"
>
<SettingTile
title="1-to-1 Chats (Encrypted)"
title={t('Settings.one_to_one_encrypted')}
after={
<AllMessagesModeSwitcher
pushRules={pushRules}
@ -126,7 +128,7 @@ export function AllMessagesNotifications() {
gap="400"
>
<SettingTile
title="Rooms"
title={t('Settings.rooms')}
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.Message} />}
/>
</SequenceCard>
@ -137,7 +139,7 @@ export function AllMessagesNotifications() {
gap="400"
>
<SettingTile
title="Rooms (Encrypted)"
title={t('Settings.rooms_encrypted')}
after={
<AllMessagesModeSwitcher
pushRules={pushRules}

View file

@ -1,4 +1,5 @@
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 { useAccountData } from '../../../hooks/useAccountData';
@ -21,6 +22,7 @@ const NOTIFY_MODE_OPS: NotificationModeOptions = {
};
function KeywordInput() {
const { t } = useTranslation();
const mx = useMatrixClient();
const [keyword, setKeyword] = useState<string>('');
@ -97,7 +99,7 @@ function KeywordInput() {
disabled={addingKeyword}
>
{addingKeyword && <Spinner variant="Secondary" size="300" />}
<Text size="B400">Save</Text>
<Text size="B400">{t('Settings.save')}</Text>
</Button>
</Box>
);
@ -146,6 +148,7 @@ function KeywordModeSwitcher({ pushRule }: PushRulesProps) {
}
export function KeywordMessagesNotifications() {
const { t } = useTranslation();
const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
const pushRules = useMemo(
() => pushRulesEvt?.getContent<IPushRules>() ?? { global: {} },
@ -162,9 +165,9 @@ export function KeywordMessagesNotifications() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">Keyword Messages</Text>
<Text size="L400">{t('Settings.keyword_messages')}</Text>
<Box gap="100">
<Text size="T200">Badge: </Text>
<Text size="T200">{t('Settings.badge')}</Text>
<Badge radii="300" variant="Success" fill="Solid">
<Text size="L400">1</Text>
</Badge>
@ -177,8 +180,8 @@ export function KeywordMessagesNotifications() {
gap="400"
>
<SettingTile
title="Select Keyword"
description="Set a notification preference for message containing given keyword."
title={t('Settings.select_keyword')}
description={t('Settings.select_keyword_desc')}
>
<KeywordInput />
</SettingTile>

View file

@ -13,6 +13,7 @@ import {
} from 'folds';
import { IPushRule } from 'matrix-js-sdk';
import React, { MouseEventHandler, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import FocusTrap from 'focus-trap-react';
import { NotificationMode, useNotificationActionsMode } from '../../../hooks/useNotificationMode';
import { stopPropagation } from '../../../utils/keyboard';
@ -21,15 +22,17 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
export const useNotificationModes = (): NotificationMode[] =>
useMemo(() => [NotificationMode.NotifyLoud, NotificationMode.Notify, NotificationMode.OFF], []);
const useNotificationModeStr = (): Record<NotificationMode, string> =>
useMemo(
const useNotificationModeStr = (): Record<NotificationMode, string> => {
const { t } = useTranslation();
return useMemo(
() => ({
[NotificationMode.OFF]: 'Disable',
[NotificationMode.Notify]: 'Notify Silent',
[NotificationMode.NotifyLoud]: 'Notify Loud',
[NotificationMode.OFF]: t('Settings.notif_disable'),
[NotificationMode.Notify]: t('Settings.notif_silent'),
[NotificationMode.NotifyLoud]: t('Settings.notif_loud'),
}),
[]
[t]
);
};
type NotificationModeSwitcherProps = {
pushRule: IPushRule;

View file

@ -1,4 +1,5 @@
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';
@ -13,13 +14,14 @@ type NotificationsProps = {
requestClose: () => void;
};
export function Notifications({ requestClose }: NotificationsProps) {
const { t } = useTranslation();
return (
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Notifications
{t('Settings.notifications_title')}
</Text>
</Box>
<Box shrink="No">
@ -38,7 +40,7 @@ export function Notifications({ requestClose }: NotificationsProps) {
<SpecialMessagesNotifications />
<KeywordMessagesNotifications />
<Box direction="Column" gap="100">
<Text size="L400">Block Messages</Text>
<Text size="L400">{t('Settings.block_messages')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -46,7 +48,7 @@ export function Notifications({ requestClose }: NotificationsProps) {
gap="400"
>
<SettingTile
description={'This option has been moved to "Account > Block Users" section.'}
description={t('Settings.block_messages_moved')}
/>
</SequenceCard>
</Box>

View file

@ -1,4 +1,5 @@
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';
@ -112,6 +113,7 @@ function MentionModeSwitcher({ ruleId, pushRules, defaultPushRuleData }: PushRul
}
export function SpecialMessagesNotifications() {
const { t } = useTranslation();
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const { displayName } = useUserProfile(userId);
@ -124,9 +126,9 @@ export function SpecialMessagesNotifications() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">Special Messages</Text>
<Text size="L400">{t('Settings.special_messages')}</Text>
<Box gap="100">
<Text size="T200">Badge: </Text>
<Text size="T200">{t('Settings.badge')}</Text>
<Badge radii="300" variant="Success" fill="Solid">
<Text size="L400">1</Text>
</Badge>
@ -139,7 +141,7 @@ export function SpecialMessagesNotifications() {
gap="400"
>
<SettingTile
title={`Mention User ID ("${userId}")`}
title={t('Settings.mention_user_id', { userId })}
after={
<MentionModeSwitcher
pushRules={pushRules}
@ -156,7 +158,7 @@ export function SpecialMessagesNotifications() {
gap="400"
>
<SettingTile
title={`Contains Displayname ${displayName ? `("${displayName}")` : ''}`}
title={displayName ? t('Settings.contains_displayname_value', { displayName }) : t('Settings.contains_displayname')}
after={
<MentionModeSwitcher
pushRules={pushRules}
@ -173,7 +175,7 @@ export function SpecialMessagesNotifications() {
gap="400"
>
<SettingTile
title={`Contains Username ("${getMxIdLocalPart(userId)}")`}
title={t('Settings.contains_username', { username: getMxIdLocalPart(userId) })}
after={
<MentionModeSwitcher
pushRules={pushRules}
@ -190,7 +192,7 @@ export function SpecialMessagesNotifications() {
gap="400"
>
<SettingTile
title="Mention @room"
title={t('Settings.mention_room')}
after={
<MentionModeSwitcher
pushRules={pushRules}
@ -207,7 +209,7 @@ export function SpecialMessagesNotifications() {
gap="400"
>
<SettingTile
title="Contains @room"
title={t('Settings.contains_room')}
after={
<MentionModeSwitcher
pushRules={pushRules}

View file

@ -1,4 +1,5 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text, Switch, Button, color, Spinner } from 'folds';
import { IPusherRequest } from 'matrix-js-sdk';
import { SequenceCard } from '../../../components/sequence-card';
@ -12,6 +13,7 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
function EmailNotification() {
const { t } = useTranslation();
const mx = useMatrixClient();
const [result, refreshResult] = useEmailNotifications();
@ -53,21 +55,21 @@ function EmailNotification() {
return (
<SettingTile
title="Email Notification"
title={t('Settings.email_notification')}
description={
<>
{result && !result.email && (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
Your account does not have any email attached.
{t('Settings.email_no_email')}
</Text>
)}
{result && result.email && <>Send notification to your email. {`("${result.email}")`}</>}
{result && result.email && t('Settings.email_send_notif_to', { email: result.email })}
{result === null && (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
Unexpected Error!
{t('Settings.unexpected_error')}
</Text>
)}
{result === undefined && 'Send notification to your email.'}
{result === undefined && t('Settings.email_send_notif')}
</>
}
after={
@ -85,6 +87,7 @@ function EmailNotification() {
}
export function SystemNotification() {
const { t } = useTranslation();
const notifPermission = usePermissionState('notifications', getNotificationState());
const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications');
const [isNotificationSounds, setIsNotificationSounds] = useSetting(
@ -98,7 +101,7 @@ export function SystemNotification() {
return (
<Box direction="Column" gap="100">
<Text size="L400">System</Text>
<Text size="L400">{t('Settings.system')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@ -106,22 +109,22 @@ export function SystemNotification() {
gap="400"
>
<SettingTile
title="Desktop Notifications"
title={t('Settings.desktop_notifications')}
description={
notifPermission === 'denied' ? (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
{'Notification' in window
? 'Notification permission is blocked. Please allow notification permission from browser address bar.'
: 'Notifications are not supported by the system.'}
? t('Settings.notif_permission_blocked')
: t('Settings.notif_not_supported')}
</Text>
) : (
<span>Show desktop notifications when message arrive.</span>
<span>{t('Settings.notif_show_desktop')}</span>
)
}
after={
notifPermission === 'prompt' ? (
<Button size="300" radii="300" onClick={requestNotificationPermission}>
<Text size="B300">Enable</Text>
<Text size="B300">{t('Settings.enable')}</Text>
</Button>
) : (
<Switch
@ -140,8 +143,8 @@ export function SystemNotification() {
gap="400"
>
<SettingTile
title="Notification Sound"
description="Play sound when new message arrive."
title={t('Settings.notification_sound')}
description={t('Settings.notification_sound_desc')}
after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
/>
</SequenceCard>

View file

@ -374,7 +374,10 @@ export function PasswordRegisterForm({
<Text size="T300">
<Trans
i18nKey="Auth.register_terms"
components={{ termsLink: <a href={termUrl} target="_blank" rel="noreferrer" /> }}
components={{
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label -- Trans fills content at runtime
termsLink: <a href={termUrl} target="_blank" rel="noreferrer" />,
}}
/>
</Text>
</Box>