feat(i18n): localise hardcoded English across calls, lobby, device verification, moderation, editor and email stage, syncing en/ru keys
This commit is contained in:
parent
99391d9f28
commit
c7dc1d51de
22 changed files with 619 additions and 252 deletions
|
|
@ -59,16 +59,30 @@ The developer reviews Russian translations as a native speaker would see them in
|
|||
- `Organisms` — Complex UI pieces
|
||||
- `Boot` — Boot/splash error screens (`ConfigConfigError`, `SpecVersions` error, `ClientRoot` init/start error)
|
||||
|
||||
**Still to localise** (snapshot taken 2026-04-14 — verify before acting):
|
||||
**Still to localise** (snapshot taken 2026-06-12 — verify before acting):
|
||||
|
||||
- Room features: `MessageEditor`, `MembersDrawer`, `RoomTombstone`
|
||||
- Lobby: `Lobby.tsx`, `RoomItem.tsx`
|
||||
- Room features: `MembersDrawer`, `RoomTombstone`
|
||||
- `AddExisting` feature
|
||||
- System pages: `FeatureCheck`, `WelcomePage`
|
||||
- Auth: `OrDivider`
|
||||
- Dialogs: `LogoutDialog`, `ManualVerification`, `BackupRestore`
|
||||
- UIA stages: `ReCaptchaStage`, `EmailStage`, `RegistrationTokenStage`
|
||||
- Components: `TimePicker`, `DatePicker`, `ImageViewer`, `PdfViewer`, `UploadCard`, `UserModeration`
|
||||
- UIA stages: `ReCaptchaStage`, `RegistrationTokenStage`
|
||||
- Components: `TimePicker`, `DatePicker`, `ImageViewer`, `PdfViewer`
|
||||
- Various `aria-label` attributes throughout
|
||||
|
||||
Localised 2026-06-12 (the hardcoded-English sweep): `UserModeration`,
|
||||
call screens (`CallControls`/`PrescreenControls`/`CallView`/`CallMemberCard`),
|
||||
lobby (`Lobby`/`RoomItem`/`SpaceItem`/`LobbyHeader`), `MessageSearch`
|
||||
(aria-label), `LeaveSpacePrompt`, the device-verification cluster
|
||||
(`DeviceVerification`/`DeviceVerificationSetup`/`ManualVerification`/
|
||||
`BackupRestore`/settings `Verification`), `ComposerAttachments`
|
||||
(spoiler/retry/cancel/size-error — the attachment strip that replaced the
|
||||
UploadBoard window), editor `Toolbar` tooltips, `EmailStage`,
|
||||
`MediaViewerBody` aria-labels (`MediaViewer.*`/`Common.*`). `MessageEditor`
|
||||
was deleted (editing moved into the composer); `LogoutDialog` was rewritten
|
||||
as a localized self-contained card. Dead `Settings.email_*notification*`
|
||||
strings removed from both locale files (the section is never rendered), as
|
||||
were the keys orphaned by the dropped room-permissions page
|
||||
(`RoomSettings.perm_*` room-only set), the user emoji-pack settings page,
|
||||
and the old upload-size-limit concatenation fragments.
|
||||
|
||||
When the developer asks to localise a new area, check this list first and update it after finishing.
|
||||
|
|
|
|||
|
|
@ -103,15 +103,24 @@
|
|||
"reset_button": "Reset Password",
|
||||
"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"
|
||||
"reset_success_login": "Login",
|
||||
"uia_email_label": "Email",
|
||||
"uia_email_send_button": "Send Verification Email",
|
||||
"uia_email_cancel": "Cancel",
|
||||
"uia_email_sending": "Sending verification email...",
|
||||
"uia_email_verify_title": "Verify Email",
|
||||
"uia_email_send_error": "Failed to send verification email request.",
|
||||
"uia_email_sent_title": "Verification Request Sent",
|
||||
"uia_email_sent_message": "Please check your email \"{{email}}\" and validate before continuing further.",
|
||||
"uia_email_continue": "Continue",
|
||||
"uia_email_provide_title": "Provide Email",
|
||||
"uia_email_provide_message": "Please provide email to send verification request."
|
||||
},
|
||||
"Settings": {
|
||||
"title": "Settings",
|
||||
"menu_general": "General",
|
||||
"menu_account": "Account",
|
||||
"menu_notifications": "Notifications",
|
||||
"menu_devices": "Devices",
|
||||
"menu_emojis_stickers": "Emojis & Stickers",
|
||||
"menu_about": "About",
|
||||
"network": {
|
||||
"menu": "Connection",
|
||||
|
|
@ -224,10 +233,6 @@
|
|||
"fsi_prompt_body": "Let Vojo show full-screen notifications so incoming calls wake the screen and appear over the lockscreen, like WhatsApp or Telegram. Open \"Full-screen notifications\" and turn Vojo on.",
|
||||
"fsi_prompt_later": "Not now",
|
||||
"fsi_prompt_open": "Open settings",
|
||||
"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",
|
||||
"one_to_one": "1-to-1 Chats",
|
||||
|
|
@ -302,21 +307,11 @@
|
|||
"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",
|
||||
"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": "A messenger for everyone.",
|
||||
|
|
@ -333,7 +328,58 @@
|
|||
"notify_on_mention": "Mentions",
|
||||
"notify_on_mention_desc": "Notify me when someone mentions my name or username.",
|
||||
"room_announcements": "@room announcements",
|
||||
"room_announcements_desc": "Notify me about @room messages."
|
||||
"room_announcements_desc": "Notify me about @room messages.",
|
||||
"verification_close": "Close",
|
||||
"verification_accept_prompt": "Please accept the request from other device.",
|
||||
"verification_waiting_accept": "Waiting for request to be accepted...",
|
||||
"verification_click_accept": "Click accept to start the verification process.",
|
||||
"verification_accept": "Accept",
|
||||
"verification_request_accepted": "Verification request has been accepted.",
|
||||
"verification_waiting_response": "Waiting for the response from other device...",
|
||||
"verification_starting_emoji": "Starting verification using emoji comparison...",
|
||||
"verification_compare_emoji_prompt": "Confirm the emoji below are displayed on both devices, in the same order:",
|
||||
"verification_match": "They Match",
|
||||
"verification_no_match": "Do not Match",
|
||||
"verification_device_verified": "Your device is verified.",
|
||||
"verification_okay": "Okay",
|
||||
"verification_canceled": "Verification has been canceled.",
|
||||
"verification_unexpected_error": "Unexpected Error! Verification is started but verifier is missing.",
|
||||
"verification_setup_desc": "Generate a Recovery Key for verifying identity if you do not have access to other devices. Additionally, setup a passphrase as a memorable alternative.",
|
||||
"verification_passphrase_optional": "Passphrase (Optional)",
|
||||
"verification_continue": "Continue",
|
||||
"verification_error_generic": "Unexpected Error!",
|
||||
"verification_uia_unsupported": "Authentication steps to perform this action are not supported by client.",
|
||||
"verification_recovery_key_store_desc": "Store the Recovery Key in a safe place for future use, as you will need it to verify your identity if you do not have access to other devices.",
|
||||
"verification_recovery_key": "Recovery Key",
|
||||
"verification_show": "Show",
|
||||
"verification_hide": "Hide",
|
||||
"verification_copy": "Copy",
|
||||
"verification_download": "Download",
|
||||
"verification_setup_title": "Setup Device Verification",
|
||||
"verification_reset_title": "Reset Device Verification",
|
||||
"verification_reset_permanent": "Resetting device verification is permanent.",
|
||||
"verification_reset_warning": "Anyone you have verified with will see security alerts and your encryption backup will be lost. You almost certainly do not want to do this, unless you have lost Recovery Key or Recovery Passphrase and every device you can verify from.",
|
||||
"verification_recovery_passphrase": "Recovery Passphrase",
|
||||
"verification_select_method": "Select a verification method.",
|
||||
"verification_provide_key": "Provide recovery key.",
|
||||
"verification_verified_success": "Device verified!",
|
||||
"verification_step_open": "Open {{settings}}.",
|
||||
"verification_step_find": "Find this device in {{section}} section.",
|
||||
"backup_connected": "Connected",
|
||||
"backup_disconnected": "Disconnected",
|
||||
"backup_syncing": "Syncing ({{amount}})",
|
||||
"backup_restoring_percent": "Restoring: {{percent}}%",
|
||||
"backup_trusted_decryption_key": "Backup has trusted decryption key.",
|
||||
"backup_untrusted_decryption_key": "Backup does not have trusted decryption key!",
|
||||
"backup_trusted_signature": "Backup is trusted by signature.",
|
||||
"backup_untrusted_signature": "Backup does not have trusted signature!",
|
||||
"backup_encryption_title": "Encryption Backup",
|
||||
"backup_details": "Backup Details",
|
||||
"backup_version": "Version: {{version}}",
|
||||
"backup_keys_count": "Keys: {{amount}}",
|
||||
"backup_none_value": "NIL",
|
||||
"backup_restore": "Restore Backup",
|
||||
"backup_none_on_server": "No backup present on server!"
|
||||
},
|
||||
"Search": {
|
||||
"search": "Search",
|
||||
|
|
@ -521,7 +567,20 @@
|
|||
"bubble_cancelled_count_one": "{{count}} cancelled call",
|
||||
"bubble_cancelled_count_other": "{{count}} cancelled calls",
|
||||
"duration_minutes_seconds": "{{minutes}} min {{seconds}} sec",
|
||||
"duration_seconds": "{{seconds}} sec"
|
||||
"duration_seconds": "{{seconds}} sec",
|
||||
"view_grid": "Grid View",
|
||||
"view_spotlight": "Spotlight View",
|
||||
"reactions": "Reactions",
|
||||
"settings": "Settings",
|
||||
"homeserver_no_calls": "Your homeserver does not support calling. But you can still join call started by others.",
|
||||
"empty_be_first": "Voice chat’s empty — be the first to hop in!",
|
||||
"no_permission_to_join": "You don't have permission to join!",
|
||||
"already_in_other_call": "Already in another call — end the current call to join!",
|
||||
"participant": "Participants",
|
||||
"live_count": "{{count}} Live",
|
||||
"collapse": "Collapse",
|
||||
"others_count_one": "+{{count}} Other",
|
||||
"others_count_other": "+{{count}} Others"
|
||||
},
|
||||
"Room": {
|
||||
"delivery": {
|
||||
|
|
@ -534,7 +593,6 @@
|
|||
"collapse_avatar": "Collapse avatar",
|
||||
"expand_avatar": "Open avatar",
|
||||
"new_messages": "New Messages",
|
||||
"jump_to_unread": "Jump to Unread",
|
||||
"mark_as_read": "Mark as Read",
|
||||
"jump_to_latest": "Jump to Latest",
|
||||
"message_render_error": "This message could not be displayed.",
|
||||
|
|
@ -542,6 +600,13 @@
|
|||
"yesterday": "Yesterday",
|
||||
"view_reactions": "View Reactions",
|
||||
"read_receipts": "Read Receipts",
|
||||
"seen_by": "Seen by",
|
||||
"seen_by_count_one": "{{count}} person",
|
||||
"seen_by_count_other": "{{count}} people",
|
||||
"seen_by_empty": "No one has seen this message yet.",
|
||||
"editing_message": "Editing message",
|
||||
"editing_cancel": "Cancel editing",
|
||||
"no_post_permission": "You do not have permission to post in this room",
|
||||
"view_source": "View Source",
|
||||
"source_code": "Source Code",
|
||||
"copy_link": "Copy Link",
|
||||
|
|
@ -652,7 +717,6 @@
|
|||
"thread_summary_unread_other": "{{count}} unread",
|
||||
"thread_summary_highlight_one": "{{count}} mention",
|
||||
"thread_summary_highlight_other": "{{count}} mentions",
|
||||
"no_post_permission": "You do not have permission to post in this room",
|
||||
"empty_dm": "The hardest part is the first message.",
|
||||
"empty_dm_alt_1": "You have to start somewhere.",
|
||||
"empty_dm_alt_2": "Someone has to go first.",
|
||||
|
|
@ -693,7 +757,43 @@
|
|||
"autocomplete_rooms": "Rooms",
|
||||
"autocomplete_emojis": "Emojis",
|
||||
"autocomplete_commands": "Commands",
|
||||
"autocomplete_unknown_room": "Unknown Room"
|
||||
"autocomplete_unknown_room": "Unknown Room",
|
||||
"lobby_join": "Join",
|
||||
"lobby_unknown": "Unknown",
|
||||
"lobby_suggested": "Suggested",
|
||||
"lobby_inaccessible": "Inaccessible",
|
||||
"lobby_open_room": "Open Room",
|
||||
"lobby_rooms": "Rooms",
|
||||
"lobby_scroll_to_top": "Scroll to Top",
|
||||
"lobby_reordering": "Reordering",
|
||||
"lobby_space_settings": "Space Settings",
|
||||
"lobby_leave_space": "Leave Space",
|
||||
"lobby_members_count_one": "{{formattedCount}} Member",
|
||||
"lobby_members_count_other": "{{formattedCount}} Members",
|
||||
"msg_search_scroll_to_top": "Scroll to Top",
|
||||
"leave_space_title": "Leave Space",
|
||||
"leave_space_confirm": "Are you sure you want to leave this space?",
|
||||
"leave_space_error": "Failed to leave space! {{error}}",
|
||||
"upload_spoiler": "Spoiler",
|
||||
"upload_retry": "Retry",
|
||||
"upload_retry_label": "Retry Upload",
|
||||
"upload_cancel_label": "Cancel Upload",
|
||||
"upload_too_large": "File too large",
|
||||
"format_bold": "Bold",
|
||||
"format_italic": "Italic",
|
||||
"format_underline": "Underline",
|
||||
"format_strikethrough": "Strike Through",
|
||||
"format_inline_code": "Inline Code",
|
||||
"format_spoiler": "Spoiler",
|
||||
"format_block_quote": "Block Quote",
|
||||
"format_block_code": "Block Code",
|
||||
"format_ordered_list": "Ordered List",
|
||||
"format_unordered_list": "Unordered List",
|
||||
"format_heading": "Heading {{level}}",
|
||||
"format_exit_formatting": "Exit Formatting",
|
||||
"format_exit": "Exit",
|
||||
"format_markdown_disable": "Disable Markdown",
|
||||
"format_markdown_enable": "Enable Markdown"
|
||||
},
|
||||
"Inbox": {
|
||||
"invite_title": "Invite",
|
||||
|
|
@ -890,38 +990,19 @@
|
|||
"sort_z_to_a": "Z to A",
|
||||
"sort_newest": "Newest",
|
||||
"sort_oldest": "Oldest",
|
||||
"perm_messages": "Messages",
|
||||
"perm_send_messages": "Send Messages",
|
||||
"perm_send_stickers": "Send Stickers",
|
||||
"perm_send_reactions": "Send Reactions",
|
||||
"perm_ping_room": "Ping @room",
|
||||
"perm_pin_messages": "Pin Messages",
|
||||
"perm_other_message_events": "Other Message Events",
|
||||
"perm_calls": "Calls",
|
||||
"perm_join_call": "Join Call",
|
||||
"perm_moderation": "Moderation",
|
||||
"perm_invite": "Invite",
|
||||
"perm_kick": "Kick",
|
||||
"perm_ban": "Ban",
|
||||
"perm_delete_others_messages": "Delete Others' Messages",
|
||||
"perm_delete_self_messages": "Delete Self Messages",
|
||||
"perm_room_overview": "Room Overview",
|
||||
"perm_room_avatar": "Room Avatar",
|
||||
"perm_room_name": "Room Name",
|
||||
"perm_room_topic": "Room Topic",
|
||||
"perm_settings": "Settings",
|
||||
"perm_change_room_access": "Change Room Access",
|
||||
"perm_publish_address": "Publish Address",
|
||||
"perm_change_all_permission": "Change All Permissions",
|
||||
"perm_edit_power_levels": "Edit Power Levels",
|
||||
"perm_enable_encryption": "Enable Encryption",
|
||||
"perm_history_visibility": "History Visibility",
|
||||
"perm_upgrade_room": "Upgrade Room",
|
||||
"perm_other_settings": "Other Settings",
|
||||
"perm_other": "Other",
|
||||
"perm_manage_emojis_stickers": "Manage Emojis & Stickers",
|
||||
"perm_change_server_acls": "Change Server ACLs",
|
||||
"perm_modify_widgets": "Modify Widgets",
|
||||
"founders": "Founders",
|
||||
"founders_desc": "Founding members have all permissions and can only be changed during a room upgrade.",
|
||||
"power_levels": "Power Levels",
|
||||
|
|
@ -1113,6 +1194,8 @@
|
|||
"blocked_title": "Blocked User",
|
||||
"blocked_description": "You do not receive any messages or invites from this user.",
|
||||
"profile_title": "Profile",
|
||||
"back": "Back",
|
||||
"manage_powers": "Manage roles",
|
||||
"presence_online": "Online",
|
||||
"presence_unavailable": "Idle",
|
||||
"presence_offline": "Offline",
|
||||
|
|
@ -1123,7 +1206,7 @@
|
|||
"last_seen_hours_other": "Last seen {{count}} hours ago",
|
||||
"last_seen_yesterday": "Last seen yesterday at {{time}}",
|
||||
"last_seen_date": "Last seen {{date}}",
|
||||
"row_id": "id",
|
||||
"row_id": "nick",
|
||||
"row_server": "server",
|
||||
"row_role": "role",
|
||||
"row_mutual": "shared",
|
||||
|
|
@ -1137,7 +1220,22 @@
|
|||
"copy_user_link": "Copy user link",
|
||||
"copy_server": "Copy server",
|
||||
"explore_community": "Explore community",
|
||||
"open_in_browser": "Open in browser"
|
||||
"open_in_browser": "Open in browser",
|
||||
"moderation_title": "Moderation",
|
||||
"moderation_reason": "Reason",
|
||||
"moderation_reason_label": "Reason:",
|
||||
"moderation_no_reason": "No reason provided.",
|
||||
"moderation_kicked_user": "Kicked User",
|
||||
"moderation_banned_user": "Banned User",
|
||||
"moderation_invited_user": "Invited User",
|
||||
"moderation_kicked_by": "Kicked by:",
|
||||
"moderation_banned_by": "Banned by:",
|
||||
"moderation_invited_by": "Invited by:",
|
||||
"moderation_unban": "Unban",
|
||||
"moderation_cancel_invite": "Cancel Invite",
|
||||
"moderation_invite": "Invite",
|
||||
"moderation_kick": "Kick",
|
||||
"moderation_ban": "Ban"
|
||||
},
|
||||
"Share": {
|
||||
"share_text": "Ready to share text",
|
||||
|
|
@ -1148,5 +1246,16 @@
|
|||
"share_files": "Ready to share {{count}} files",
|
||||
"tap_chat_to_send": "Open a chat to drop it in",
|
||||
"cancel": "Cancel share"
|
||||
},
|
||||
"Common": {
|
||||
"close": "Close",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"MediaViewer": {
|
||||
"zoom_out": "Zoom out",
|
||||
"zoom_in": "Zoom in",
|
||||
"download": "Download",
|
||||
"previous": "Previous",
|
||||
"next": "Next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,15 +103,24 @@
|
|||
"reset_button": "Сбросить пароль",
|
||||
"reset_error_fallback": "Не удалось сбросить пароль.",
|
||||
"reset_success_message": "Пароль успешно сброшен. Войдите с новым паролем.",
|
||||
"reset_success_login": "Войти"
|
||||
"reset_success_login": "Войти",
|
||||
"uia_email_label": "Эл. почта",
|
||||
"uia_email_send_button": "Отправить письмо для подтверждения",
|
||||
"uia_email_cancel": "Отмена",
|
||||
"uia_email_sending": "Отправка письма для подтверждения...",
|
||||
"uia_email_verify_title": "Подтверждение почты",
|
||||
"uia_email_send_error": "Не удалось отправить письмо для подтверждения.",
|
||||
"uia_email_sent_title": "Письмо для подтверждения отправлено",
|
||||
"uia_email_sent_message": "Проверьте почту \"{{email}}\" и подтвердите адрес, прежде чем продолжить.",
|
||||
"uia_email_continue": "Продолжить",
|
||||
"uia_email_provide_title": "Укажите эл. почту",
|
||||
"uia_email_provide_message": "Укажите адрес эл. почты, чтобы отправить письмо для подтверждения."
|
||||
},
|
||||
"Settings": {
|
||||
"title": "Настройки",
|
||||
"menu_general": "Общие",
|
||||
"menu_account": "Аккаунт",
|
||||
"menu_notifications": "Уведомления",
|
||||
"menu_devices": "Устройства",
|
||||
"menu_emojis_stickers": "Эмодзи и стикеры",
|
||||
"menu_about": "О приложении",
|
||||
"network": {
|
||||
"menu": "Соединение",
|
||||
|
|
@ -224,10 +233,6 @@
|
|||
"fsi_prompt_body": "Разрешите Vojo показывать полноэкранные уведомления — тогда входящие звонки будут будить экран и появляться поверх блокировки, как в WhatsApp или Telegram. Откройте «Уведомления поверх экрана блокировки» и включите переключатель для Vojo.",
|
||||
"fsi_prompt_later": "Позже",
|
||||
"fsi_prompt_open": "Открыть настройки",
|
||||
"email_notification": "Уведомления по почте",
|
||||
"email_no_email": "К вашему аккаунту не привязана электронная почта.",
|
||||
"email_send_notif": "Отправлять уведомления на вашу почту.",
|
||||
"email_send_notif_to": "Отправлять уведомления на вашу почту. (\"{{email}}\")",
|
||||
"unexpected_error": "Непредвиденная ошибка!",
|
||||
"all_messages": "Все сообщения",
|
||||
"one_to_one": "Личные чаты",
|
||||
|
|
@ -302,21 +307,11 @@
|
|||
"import_desc": "Загрузите защищённую паролем копию ключей шифрования с устройства для расшифровки сообщений.",
|
||||
"import": "Импорт",
|
||||
"decrypt": "Расшифровать",
|
||||
"emojis_stickers_title": "Эмодзи и стикеры",
|
||||
"default_pack": "Пакет по умолчанию",
|
||||
"unknown": "Неизвестно",
|
||||
"view": "Открыть",
|
||||
"favorite_packs": "Избранные пакеты",
|
||||
"select_pack": "Выбрать пакет",
|
||||
"select_pack_desc": "Выберите пакеты эмодзи и стикеров из комнат для использования во всех комнатах.",
|
||||
"select": "Выбрать",
|
||||
"room_packs": "Пакеты комнат",
|
||||
"select_all": "Выбрать все",
|
||||
"unselect_all": "Снять выделение",
|
||||
"no_packs": "Нет пакетов",
|
||||
"no_packs_desc": "Здесь появятся пакеты из комнат. У вас пока нет комнат с пакетами.",
|
||||
"apply_error": "Не удалось применить изменения! Попробуйте снова.",
|
||||
"apply_ready": "Изменения сохранены! Примените, когда будете готовы.",
|
||||
"apply_changes": "Применить изменения",
|
||||
"about_title": "О приложении",
|
||||
"about_tagline": "Вседоступный мессенджер.",
|
||||
|
|
@ -333,7 +328,58 @@
|
|||
"notify_on_mention": "Упоминания",
|
||||
"notify_on_mention_desc": "Уведомлять, когда упоминают моё имя или ник.",
|
||||
"room_announcements": "Объявления @room",
|
||||
"room_announcements_desc": "Уведомлять о сообщениях с @room."
|
||||
"room_announcements_desc": "Уведомлять о сообщениях с @room.",
|
||||
"verification_close": "Закрыть",
|
||||
"verification_accept_prompt": "Примите запрос на другом устройстве.",
|
||||
"verification_waiting_accept": "Ожидание подтверждения запроса...",
|
||||
"verification_click_accept": "Нажмите «Принять», чтобы начать верификацию.",
|
||||
"verification_accept": "Принять",
|
||||
"verification_request_accepted": "Запрос на верификацию принят.",
|
||||
"verification_waiting_response": "Ожидание ответа от другого устройства...",
|
||||
"verification_starting_emoji": "Начинаем верификацию сравнением эмодзи...",
|
||||
"verification_compare_emoji_prompt": "Убедитесь, что на обоих устройствах показаны одни и те же эмодзи в одинаковом порядке:",
|
||||
"verification_match": "Совпадают",
|
||||
"verification_no_match": "Не совпадают",
|
||||
"verification_device_verified": "Ваше устройство верифицировано.",
|
||||
"verification_okay": "Понятно",
|
||||
"verification_canceled": "Верификация отменена.",
|
||||
"verification_unexpected_error": "Непредвиденная ошибка! Верификация началась, но обработчик проверки не найден.",
|
||||
"verification_setup_desc": "Создайте ключ восстановления — он подтвердит вашу личность, если другие устройства недоступны. Дополнительно можно задать парольную фразу, которую проще запомнить.",
|
||||
"verification_passphrase_optional": "Парольная фраза (необязательно)",
|
||||
"verification_continue": "Продолжить",
|
||||
"verification_error_generic": "Непредвиденная ошибка!",
|
||||
"verification_uia_unsupported": "Клиент не поддерживает шаги аутентификации, необходимые для этого действия.",
|
||||
"verification_recovery_key_store_desc": "Сохраните ключ восстановления в надёжном месте: он понадобится для подтверждения личности, если другие устройства будут недоступны.",
|
||||
"verification_recovery_key": "Ключ восстановления",
|
||||
"verification_show": "Показать",
|
||||
"verification_hide": "Скрыть",
|
||||
"verification_copy": "Скопировать",
|
||||
"verification_download": "Скачать",
|
||||
"verification_setup_title": "Настройка верификации устройства",
|
||||
"verification_reset_title": "Сброс верификации устройства",
|
||||
"verification_reset_permanent": "Сброс верификации устройства необратим.",
|
||||
"verification_reset_warning": "Все, с кем вы проходили верификацию, увидят предупреждения безопасности, а резервная копия шифрования будет потеряна. Делайте это, только если вы потеряли ключ восстановления или парольную фразу и все устройства, с которых можно пройти верификацию.",
|
||||
"verification_recovery_passphrase": "Парольная фраза восстановления",
|
||||
"verification_select_method": "Выберите способ верификации.",
|
||||
"verification_provide_key": "Введите ключ восстановления.",
|
||||
"verification_verified_success": "Устройство верифицировано!",
|
||||
"verification_step_open": "Откройте «{{settings}}».",
|
||||
"verification_step_find": "Найдите это устройство в разделе «{{section}}».",
|
||||
"backup_connected": "Подключено",
|
||||
"backup_disconnected": "Отключено",
|
||||
"backup_syncing": "Синхронизация ({{amount}})",
|
||||
"backup_restoring_percent": "Восстановление: {{percent}}%",
|
||||
"backup_trusted_decryption_key": "У резервной копии есть доверенный ключ расшифровки.",
|
||||
"backup_untrusted_decryption_key": "У резервной копии нет доверенного ключа расшифровки!",
|
||||
"backup_trusted_signature": "Резервная копия заверена доверенной подписью.",
|
||||
"backup_untrusted_signature": "У резервной копии нет доверенной подписи!",
|
||||
"backup_encryption_title": "Резервная копия шифрования",
|
||||
"backup_details": "Сведения о резервной копии",
|
||||
"backup_version": "Версия: {{version}}",
|
||||
"backup_keys_count": "Ключей: {{amount}}",
|
||||
"backup_none_value": "нет",
|
||||
"backup_restore": "Восстановить резервную копию",
|
||||
"backup_none_on_server": "На сервере нет резервной копии!"
|
||||
},
|
||||
"Search": {
|
||||
"search": "Поиск",
|
||||
|
|
@ -523,11 +569,28 @@
|
|||
"bubble_missed_count_one": "{{count}} пропущенный звонок",
|
||||
"bubble_missed_count_few": "{{count}} пропущенных звонка",
|
||||
"bubble_missed_count_many": "{{count}} пропущенных звонков",
|
||||
"bubble_missed_count_other": "{{count}} пропущенных звонков",
|
||||
"bubble_cancelled_count_one": "{{count}} отменённый звонок",
|
||||
"bubble_cancelled_count_few": "{{count}} отменённых звонка",
|
||||
"bubble_cancelled_count_many": "{{count}} отменённых звонков",
|
||||
"bubble_cancelled_count_other": "{{count}} отменённых звонков",
|
||||
"duration_minutes_seconds": "{{minutes}} мин {{seconds}} сек",
|
||||
"duration_seconds": "{{seconds}} сек"
|
||||
"duration_seconds": "{{seconds}} сек",
|
||||
"view_grid": "Вид галереи",
|
||||
"view_spotlight": "Вид докладчика",
|
||||
"reactions": "Реакции",
|
||||
"settings": "Настройки",
|
||||
"homeserver_no_calls": "Ваш сервер не поддерживает звонки, но вы можете присоединиться к звонку, который начал кто-то другой.",
|
||||
"empty_be_first": "В голосовом чате пусто — присоединяйтесь первым!",
|
||||
"no_permission_to_join": "У вас нет прав, чтобы присоединиться!",
|
||||
"already_in_other_call": "Вы уже в другом звонке — завершите его, чтобы присоединиться!",
|
||||
"participant": "Участники",
|
||||
"live_count": "{{count}} в эфире",
|
||||
"collapse": "Свернуть",
|
||||
"others_count_one": "Ещё {{count}} участник",
|
||||
"others_count_few": "Ещё {{count}} участника",
|
||||
"others_count_many": "Ещё {{count}} участников",
|
||||
"others_count_other": "Ещё {{count}} участников"
|
||||
},
|
||||
"Room": {
|
||||
"delivery": {
|
||||
|
|
@ -540,7 +603,6 @@
|
|||
"collapse_avatar": "Свернуть аватар",
|
||||
"expand_avatar": "Развернуть аватар",
|
||||
"new_messages": "Новые сообщения",
|
||||
"jump_to_unread": "К непрочитанным",
|
||||
"mark_as_read": "Отметить прочитанным",
|
||||
"jump_to_latest": "К последним",
|
||||
"message_render_error": "Не удалось показать это сообщение.",
|
||||
|
|
@ -548,6 +610,15 @@
|
|||
"yesterday": "Вчера",
|
||||
"view_reactions": "Реакции",
|
||||
"read_receipts": "Подтверждения прочтения",
|
||||
"seen_by": "Прочитали",
|
||||
"seen_by_count_one": "{{count}} человек",
|
||||
"seen_by_count_few": "{{count}} человека",
|
||||
"seen_by_count_many": "{{count}} человек",
|
||||
"seen_by_count_other": "{{count}} человек",
|
||||
"seen_by_empty": "Это сообщение пока никто не прочитал.",
|
||||
"editing_message": "Редактирование",
|
||||
"editing_cancel": "Отменить редактирование",
|
||||
"no_post_permission": "У вас нет разрешения на отправку сообщений в этой комнате",
|
||||
"view_source": "Исходный код",
|
||||
"source_code": "Исходный код",
|
||||
"copy_link": "Копировать ссылку",
|
||||
|
|
@ -668,7 +739,6 @@
|
|||
"thread_summary_highlight_few": "{{count}} упоминания",
|
||||
"thread_summary_highlight_many": "{{count}} упоминаний",
|
||||
"thread_summary_highlight_other": "{{count}} упоминания",
|
||||
"no_post_permission": "У вас нет разрешения на отправку сообщений в этой комнате",
|
||||
"empty_dm": "Самое сложное — первое сообщение.",
|
||||
"empty_dm_alt_1": "С чего-то надо начать.",
|
||||
"empty_dm_alt_2": "Кто-то должен написать первым.",
|
||||
|
|
@ -709,7 +779,45 @@
|
|||
"autocomplete_rooms": "Комнаты",
|
||||
"autocomplete_emojis": "Эмодзи",
|
||||
"autocomplete_commands": "Команды",
|
||||
"autocomplete_unknown_room": "Неизвестная комната"
|
||||
"autocomplete_unknown_room": "Неизвестная комната",
|
||||
"lobby_join": "Присоединиться",
|
||||
"lobby_unknown": "Неизвестно",
|
||||
"lobby_suggested": "Рекомендуется",
|
||||
"lobby_inaccessible": "Недоступно",
|
||||
"lobby_open_room": "Открыть комнату",
|
||||
"lobby_rooms": "Комнаты",
|
||||
"lobby_scroll_to_top": "Наверх",
|
||||
"lobby_reordering": "Перемещение",
|
||||
"lobby_space_settings": "Настройки сообщества",
|
||||
"lobby_leave_space": "Покинуть сообщество",
|
||||
"lobby_members_count_one": "{{formattedCount}} участник",
|
||||
"lobby_members_count_few": "{{formattedCount}} участника",
|
||||
"lobby_members_count_many": "{{formattedCount}} участников",
|
||||
"lobby_members_count_other": "{{formattedCount}} участников",
|
||||
"msg_search_scroll_to_top": "Наверх",
|
||||
"leave_space_title": "Покинуть сообщество",
|
||||
"leave_space_confirm": "Покинуть это сообщество?",
|
||||
"leave_space_error": "Не удалось покинуть сообщество! {{error}}",
|
||||
"upload_spoiler": "Спойлер",
|
||||
"upload_retry": "Повторить",
|
||||
"upload_retry_label": "Повторить загрузку",
|
||||
"upload_cancel_label": "Отменить загрузку",
|
||||
"upload_too_large": "Файл слишком большой",
|
||||
"format_bold": "Жирный",
|
||||
"format_italic": "Курсив",
|
||||
"format_underline": "Подчёркнутый",
|
||||
"format_strikethrough": "Зачёркнутый",
|
||||
"format_inline_code": "Код",
|
||||
"format_spoiler": "Спойлер",
|
||||
"format_block_quote": "Цитата",
|
||||
"format_block_code": "Блок кода",
|
||||
"format_ordered_list": "Нумерованный список",
|
||||
"format_unordered_list": "Маркированный список",
|
||||
"format_heading": "Заголовок {{level}}",
|
||||
"format_exit_formatting": "Выйти из форматирования",
|
||||
"format_exit": "Выйти",
|
||||
"format_markdown_disable": "Отключить Markdown",
|
||||
"format_markdown_enable": "Включить Markdown"
|
||||
},
|
||||
"Inbox": {
|
||||
"invite_title": "Пригласить",
|
||||
|
|
@ -908,38 +1016,19 @@
|
|||
"sort_z_to_a": "Я — А",
|
||||
"sort_newest": "Новые",
|
||||
"sort_oldest": "Старые",
|
||||
"perm_messages": "Сообщения",
|
||||
"perm_send_messages": "Отправка сообщений",
|
||||
"perm_send_stickers": "Отправка стикеров",
|
||||
"perm_send_reactions": "Отправка реакций",
|
||||
"perm_ping_room": "Упоминание @room",
|
||||
"perm_pin_messages": "Закрепление сообщений",
|
||||
"perm_other_message_events": "Прочие события сообщений",
|
||||
"perm_calls": "Звонки",
|
||||
"perm_join_call": "Присоединиться к звонку",
|
||||
"perm_moderation": "Модерация",
|
||||
"perm_invite": "Приглашение",
|
||||
"perm_kick": "Исключение",
|
||||
"perm_ban": "Бан",
|
||||
"perm_delete_others_messages": "Удаление чужих сообщений",
|
||||
"perm_delete_self_messages": "Удаление своих сообщений",
|
||||
"perm_room_overview": "Обзор комнаты",
|
||||
"perm_room_avatar": "Аватар комнаты",
|
||||
"perm_room_name": "Название комнаты",
|
||||
"perm_room_topic": "Тема комнаты",
|
||||
"perm_settings": "Настройки",
|
||||
"perm_change_room_access": "Изменение доступа к комнате",
|
||||
"perm_publish_address": "Публикация адреса",
|
||||
"perm_change_all_permission": "Изменение всех прав",
|
||||
"perm_edit_power_levels": "Редактирование уровней власти",
|
||||
"perm_enable_encryption": "Включение шифрования",
|
||||
"perm_history_visibility": "Видимость истории",
|
||||
"perm_upgrade_room": "Обновление комнаты",
|
||||
"perm_other_settings": "Прочие настройки",
|
||||
"perm_other": "Прочее",
|
||||
"perm_manage_emojis_stickers": "Управление эмодзи и стикерами",
|
||||
"perm_change_server_acls": "Изменение ACL серверов",
|
||||
"perm_modify_widgets": "Изменение виджетов",
|
||||
"founders": "Основатели",
|
||||
"founders_desc": "Основатели имеют все права. Изменить их состав можно только при обновлении комнаты.",
|
||||
"power_levels": "Уровни власти",
|
||||
|
|
@ -1131,6 +1220,8 @@
|
|||
"blocked_title": "Заблокирован",
|
||||
"blocked_description": "Сообщения и приглашения от этого пользователя не приходят.",
|
||||
"profile_title": "Профиль",
|
||||
"back": "Назад",
|
||||
"manage_powers": "Управление ролями",
|
||||
"presence_online": "В сети",
|
||||
"presence_unavailable": "Не активен",
|
||||
"presence_offline": "Не в сети",
|
||||
|
|
@ -1138,12 +1229,14 @@
|
|||
"last_seen_minutes_one": "Был в сети {{count}} минуту назад",
|
||||
"last_seen_minutes_few": "Был в сети {{count}} минуты назад",
|
||||
"last_seen_minutes_many": "Был в сети {{count}} минут назад",
|
||||
"last_seen_minutes_other": "Был в сети {{count}} минут назад",
|
||||
"last_seen_hours_one": "Был в сети {{count}} час назад",
|
||||
"last_seen_hours_few": "Был в сети {{count}} часа назад",
|
||||
"last_seen_hours_many": "Был в сети {{count}} часов назад",
|
||||
"last_seen_hours_other": "Был в сети {{count}} часов назад",
|
||||
"last_seen_yesterday": "Был в сети вчера в {{time}}",
|
||||
"last_seen_date": "Был в сети {{date}}",
|
||||
"row_id": "id",
|
||||
"row_id": "ник",
|
||||
"row_server": "сервер",
|
||||
"row_role": "роль",
|
||||
"row_mutual": "общие",
|
||||
|
|
@ -1154,11 +1247,27 @@
|
|||
"row_mutual_count_one": "{{count}} чат",
|
||||
"row_mutual_count_few": "{{count}} чата",
|
||||
"row_mutual_count_many": "{{count}} чатов",
|
||||
"row_mutual_count_other": "{{count}} чатов",
|
||||
"copy_user_id": "Скопировать ID",
|
||||
"copy_user_link": "Скопировать ссылку",
|
||||
"copy_server": "Скопировать сервер",
|
||||
"explore_community": "Открыть сервер",
|
||||
"open_in_browser": "Открыть в браузере"
|
||||
"open_in_browser": "Открыть в браузере",
|
||||
"moderation_title": "Модерация",
|
||||
"moderation_reason": "Причина",
|
||||
"moderation_reason_label": "Причина:",
|
||||
"moderation_no_reason": "Причина не указана.",
|
||||
"moderation_kicked_user": "Пользователь исключён",
|
||||
"moderation_banned_user": "Пользователь забанен",
|
||||
"moderation_invited_user": "Пользователь приглашён",
|
||||
"moderation_kicked_by": "Исключил(а):",
|
||||
"moderation_banned_by": "Забанил(а):",
|
||||
"moderation_invited_by": "Пригласил(а):",
|
||||
"moderation_unban": "Разбанить",
|
||||
"moderation_cancel_invite": "Отменить приглашение",
|
||||
"moderation_invite": "Пригласить",
|
||||
"moderation_kick": "Исключить",
|
||||
"moderation_ban": "Забанить"
|
||||
},
|
||||
"Share": {
|
||||
"share_text": "Готово к пересылке: текст",
|
||||
|
|
@ -1169,5 +1278,16 @@
|
|||
"share_files": "Готово к пересылке: {{count}} файлов",
|
||||
"tap_chat_to_send": "Откройте чат, чтобы отправить",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"Common": {
|
||||
"close": "Закрыть",
|
||||
"retry": "Повторить"
|
||||
},
|
||||
"MediaViewer": {
|
||||
"zoom_out": "Уменьшить",
|
||||
"zoom_in": "Увеличить",
|
||||
"download": "Скачать",
|
||||
"previous": "Назад",
|
||||
"next": "Вперёд"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
Text,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BackupProgressStatus, backupRestoreProgressAtom } from '../state/backupRestore';
|
||||
import { InfoCard } from './info-card';
|
||||
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||
|
|
@ -35,6 +36,7 @@ type BackupStatusProps = {
|
|||
enabled: boolean;
|
||||
};
|
||||
function BackupStatus({ enabled }: BackupStatusProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box as="span" gap="100" alignItems="Center">
|
||||
<Badge variant={enabled ? 'Success' : 'Critical'} fill="Solid" size="200" radii="Pill" />
|
||||
|
|
@ -43,7 +45,7 @@ function BackupStatus({ enabled }: BackupStatusProps) {
|
|||
size="L400"
|
||||
style={{ color: enabled ? color.Success.Main : color.Critical.Main }}
|
||||
>
|
||||
{enabled ? 'Connected' : 'Disconnected'}
|
||||
{enabled ? t('Settings.backup_connected') : t('Settings.backup_disconnected')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -52,21 +54,23 @@ type BackupSyncingProps = {
|
|||
count: number;
|
||||
};
|
||||
function BackupSyncing({ count }: BackupSyncingProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box as="span" gap="100" alignItems="Center">
|
||||
<Spinner size="50" variant="Primary" fill="Soft" />
|
||||
<Text as="span" size="L400" style={{ color: color.Primary.Main }}>
|
||||
Syncing ({count})
|
||||
{t('Settings.backup_syncing', { amount: count })}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function BackupProgressFetching() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box grow="Yes" gap="200" alignItems="Center">
|
||||
<Badge variant="Secondary" fill="Solid" radii="300">
|
||||
<Text size="L400">Restoring: 0%</Text>
|
||||
<Text size="L400">{t('Settings.backup_restoring_percent', { percent: 0 })}</Text>
|
||||
</Badge>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<ProgressBar variant="Secondary" size="300" min={0} max={1} value={0} />
|
||||
|
|
@ -81,10 +85,15 @@ type BackupProgressProps = {
|
|||
downloaded: number;
|
||||
};
|
||||
function BackupProgress({ total, downloaded }: BackupProgressProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box grow="Yes" gap="200" alignItems="Center">
|
||||
<Badge variant="Secondary" fill="Solid" radii="300">
|
||||
<Text size="L400">Restoring: {`${Math.round(percent(0, total, downloaded))}%`}</Text>
|
||||
<Text size="L400">
|
||||
{t('Settings.backup_restoring_percent', {
|
||||
percent: Math.round(percent(0, total, downloaded)),
|
||||
})}
|
||||
</Text>
|
||||
</Badge>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<ProgressBar variant="Secondary" size="300" min={0} max={total} value={downloaded} />
|
||||
|
|
@ -103,6 +112,7 @@ type BackupTrustInfoProps = {
|
|||
backupInfo: KeyBackupInfo;
|
||||
};
|
||||
function BackupTrustInfo({ crypto, backupInfo }: BackupTrustInfoProps) {
|
||||
const { t } = useTranslation();
|
||||
const trust = useKeyBackupTrust(crypto, backupInfo);
|
||||
|
||||
if (!trust) return null;
|
||||
|
|
@ -111,20 +121,20 @@ function BackupTrustInfo({ crypto, backupInfo }: BackupTrustInfoProps) {
|
|||
<Box direction="Column">
|
||||
{trust.matchesDecryptionKey ? (
|
||||
<Text size="T200" style={{ color: color.Success.Main }}>
|
||||
<b>Backup has trusted decryption key.</b>
|
||||
<b>{t('Settings.backup_trusted_decryption_key')}</b>
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
<b>Backup does not have trusted decryption key!</b>
|
||||
<b>{t('Settings.backup_untrusted_decryption_key')}</b>
|
||||
</Text>
|
||||
)}
|
||||
{trust.trusted ? (
|
||||
<Text size="T200" style={{ color: color.Success.Main }}>
|
||||
<b>Backup has trusted by signature.</b>
|
||||
<b>{t('Settings.backup_trusted_signature')}</b>
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
<b>Backup does not have trusted signature!</b>
|
||||
<b>{t('Settings.backup_untrusted_signature')}</b>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -135,6 +145,7 @@ type BackupRestoreTileProps = {
|
|||
crypto: CryptoApi;
|
||||
};
|
||||
export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
||||
const { t } = useTranslation();
|
||||
const [restoreProgress, setRestoreProgress] = useAtom(backupRestoreProgressAtom);
|
||||
const restoring =
|
||||
restoreProgress.status === BackupProgressStatus.Fetching ||
|
||||
|
|
@ -168,7 +179,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
|||
return (
|
||||
<InfoCard
|
||||
variant="Surface"
|
||||
title="Encryption Backup"
|
||||
title={t('Settings.backup_encryption_title')}
|
||||
after={
|
||||
<Box alignItems="Center" gap="200">
|
||||
{remainingSession === 0 ? (
|
||||
|
|
@ -212,12 +223,20 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
|||
<Box direction="Column" gap="200">
|
||||
<InfoCard
|
||||
variant="SurfaceVariant"
|
||||
title="Backup Details"
|
||||
title={t('Settings.backup_details')}
|
||||
description={
|
||||
<>
|
||||
<span>Version: {backupInfo?.version ?? 'NIL'}</span>
|
||||
<span>
|
||||
{t('Settings.backup_version', {
|
||||
version: backupInfo?.version ?? t('Settings.backup_none_value'),
|
||||
})}
|
||||
</span>
|
||||
<br />
|
||||
<span>Keys: {backupInfo?.count ?? 'NIL'}</span>
|
||||
<span>
|
||||
{t('Settings.backup_keys_count', {
|
||||
amount: backupInfo?.count ?? t('Settings.backup_none_value'),
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
@ -234,7 +253,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
|||
}
|
||||
before={<Icon size="100" src={Icons.Download} />}
|
||||
>
|
||||
<Text size="B300">Restore Backup</Text>
|
||||
<Text size="B300">{t('Settings.backup_restore')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Menu>
|
||||
|
|
@ -251,7 +270,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
|||
)}
|
||||
{!backupEnabled && backupInfo === null && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
<b>No backup present on server!</b>
|
||||
<b>{t('Settings.backup_none_on_server')}</b>
|
||||
</Text>
|
||||
)}
|
||||
{!syncFailure && !backupEnabled && backupInfo && (
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
Text,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useVerificationRequestPhase,
|
||||
useVerificationRequestReceived,
|
||||
|
|
@ -50,21 +51,23 @@ function WaitingMessage({ message }: WaitingMessageProps) {
|
|||
|
||||
type VerificationUnexpectedProps = { message: string; onClose: () => void };
|
||||
function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text>{message}</Text>
|
||||
<Button variant="Secondary" fill="Soft" onClick={onClose}>
|
||||
<Text size="B400">Close</Text>
|
||||
<Text size="B400">{t('Settings.verification_close')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function VerificationWaitAccept() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text>Please accept the request from other device.</Text>
|
||||
<WaitingMessage message="Waiting for request to be accepted..." />
|
||||
<Text>{t('Settings.verification_accept_prompt')}</Text>
|
||||
<WaitingMessage message={t('Settings.verification_waiting_accept')} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -73,12 +76,13 @@ type VerificationAcceptProps = {
|
|||
onAccept: () => Promise<void>;
|
||||
};
|
||||
function VerificationAccept({ onAccept }: VerificationAcceptProps) {
|
||||
const { t } = useTranslation();
|
||||
const [acceptState, accept] = useAsyncCallback(onAccept);
|
||||
|
||||
const accepting = acceptState.status === AsyncStatus.Loading;
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text>Click accept to start the verification process.</Text>
|
||||
<Text>{t('Settings.verification_click_accept')}</Text>
|
||||
<Button
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
|
|
@ -86,17 +90,18 @@ function VerificationAccept({ onAccept }: VerificationAcceptProps) {
|
|||
before={accepting && <Spinner size="100" variant="Primary" fill="Solid" />}
|
||||
disabled={accepting}
|
||||
>
|
||||
<Text size="B400">Accept</Text>
|
||||
<Text size="B400">{t('Settings.verification_accept')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function VerificationWaitStart() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text>Verification request has been accepted.</Text>
|
||||
<WaitingMessage message="Waiting for the response from other device..." />
|
||||
<Text>{t('Settings.verification_request_accepted')}</Text>
|
||||
<WaitingMessage message={t('Settings.verification_waiting_response')} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -105,18 +110,20 @@ type VerificationStartProps = {
|
|||
onStart: () => Promise<void>;
|
||||
};
|
||||
function AutoVerificationStart({ onStart }: VerificationStartProps) {
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
onStart();
|
||||
}, [onStart]);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<WaitingMessage message="Starting verification using emoji comparison..." />
|
||||
<WaitingMessage message={t('Settings.verification_starting_emoji')} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
||||
const { t } = useTranslation();
|
||||
const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData]));
|
||||
|
||||
const confirming =
|
||||
|
|
@ -124,7 +131,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
|||
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
|
||||
<Text>{t('Settings.verification_compare_emoji_prompt')}</Text>
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'SurfaceVariant' })}
|
||||
style={{
|
||||
|
|
@ -157,7 +164,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
|||
disabled={confirming}
|
||||
before={confirming && <Spinner size="100" variant="Primary" />}
|
||||
>
|
||||
<Text size="B400">They Match</Text>
|
||||
<Text size="B400">{t('Settings.verification_match')}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
variant="Primary"
|
||||
|
|
@ -165,7 +172,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
|||
onClick={() => sasData.mismatch()}
|
||||
disabled={confirming}
|
||||
>
|
||||
<Text size="B400">Do not Match</Text>
|
||||
<Text size="B400">{t('Settings.verification_no_match')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -177,6 +184,7 @@ type SasVerificationProps = {
|
|||
onCancel: () => void;
|
||||
};
|
||||
function SasVerification({ verifier, onCancel }: SasVerificationProps) {
|
||||
const { t } = useTranslation();
|
||||
const [sasData, setSasData] = useState<ShowSasCallbacks>();
|
||||
|
||||
useVerifierShowSas(verifier, setSasData);
|
||||
|
|
@ -192,7 +200,7 @@ function SasVerification({ verifier, onCancel }: SasVerificationProps) {
|
|||
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<WaitingMessage message="Starting verification using emoji comparison..." />
|
||||
<WaitingMessage message={t('Settings.verification_starting_emoji')} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -201,13 +209,14 @@ type VerificationDoneProps = {
|
|||
onExit: () => void;
|
||||
};
|
||||
function VerificationDone({ onExit }: VerificationDoneProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<div>
|
||||
<Text>Your device is verified.</Text>
|
||||
<Text>{t('Settings.verification_device_verified')}</Text>
|
||||
</div>
|
||||
<Button variant="Primary" fill="Solid" onClick={onExit}>
|
||||
<Text size="B400">Okay</Text>
|
||||
<Text size="B400">{t('Settings.verification_okay')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -217,11 +226,12 @@ type VerificationCanceledProps = {
|
|||
onClose: () => void;
|
||||
};
|
||||
function VerificationCanceled({ onClose }: VerificationCanceledProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text>Verification has been canceled.</Text>
|
||||
<Text>{t('Settings.verification_canceled')}</Text>
|
||||
<Button variant="Secondary" fill="Soft" onClick={onClose}>
|
||||
<Text size="B400">Close</Text>
|
||||
<Text size="B400">{t('Settings.verification_close')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -232,6 +242,7 @@ type DeviceVerificationProps = {
|
|||
onExit: () => void;
|
||||
};
|
||||
export function DeviceVerification({ request, onExit }: DeviceVerificationProps) {
|
||||
const { t } = useTranslation();
|
||||
const phase = useVerificationRequestPhase(request);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
|
|
@ -259,7 +270,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
|
|||
<Dialog variant="Surface">
|
||||
<Header style={DialogHeaderStyles} variant="Surface" size="500">
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Device Verification</Text>
|
||||
<Text size="H4">{t('Settings.device_verification')}</Text>
|
||||
</Box>
|
||||
<IconButton size="300" radii="300" onClick={handleCancel}>
|
||||
<Icon src={Icons.Cross} />
|
||||
|
|
@ -283,7 +294,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
|
|||
<SasVerification verifier={request.verifier} onCancel={handleCancel} />
|
||||
) : (
|
||||
<VerificationUnexpected
|
||||
message="Unexpected Error! Verification is started but verifier is missing."
|
||||
message={t('Settings.verification_unexpected_error')}
|
||||
onClose={handleCancel}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import FileSaver from 'file-saver';
|
||||
import to from 'await-to-js';
|
||||
import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PasswordInput } from './password-input';
|
||||
import { ContainerColor } from '../styles/ContainerColor.css';
|
||||
import { copyToClipboard } from '../utils/dom';
|
||||
|
|
@ -71,6 +72,7 @@ type SetupVerificationProps = {
|
|||
onComplete: (recoveryKey: string) => void;
|
||||
};
|
||||
function SetupVerification({ onComplete }: SetupVerificationProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const alive = useAlive();
|
||||
|
||||
|
|
@ -181,12 +183,9 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
|||
|
||||
return (
|
||||
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="400">
|
||||
<Text size="T300">
|
||||
Generate a <b>Recovery Key</b> for verifying identity if you do not have access to other
|
||||
devices. Additionally, setup a passphrase as a memorable alternative.
|
||||
</Text>
|
||||
<Text size="T300">{t('Settings.verification_setup_desc')}</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Passphrase (Optional)</Text>
|
||||
<Text size="L400">{t('Settings.verification_passphrase_optional')}</Text>
|
||||
<PasswordInput name="passphraseInput" size="400" readOnly={loading} />
|
||||
</Box>
|
||||
<Button
|
||||
|
|
@ -194,21 +193,19 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
|||
disabled={loading}
|
||||
before={loading && <Spinner size="200" variant="Primary" fill="Solid" />}
|
||||
>
|
||||
<Text size="B400">Continue</Text>
|
||||
<Text size="B400">{t('Settings.verification_continue')}</Text>
|
||||
</Button>
|
||||
{setupState.status === AsyncStatus.Error && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
<b>{setupState.error ? setupState.error.message : 'Unexpected Error!'}</b>
|
||||
<b>
|
||||
{setupState.error ? setupState.error.message : t('Settings.verification_error_generic')}
|
||||
</b>
|
||||
</Text>
|
||||
)}
|
||||
{nextAuthData !== null && uiaAction && (
|
||||
<ActionUIAFlowsLoader
|
||||
authData={nextAuthData ?? uiaAction.authData}
|
||||
unsupported={() => (
|
||||
<Text size="T200">
|
||||
Authentication steps to perform this action are not supported by client.
|
||||
</Text>
|
||||
)}
|
||||
unsupported={() => <Text size="T200">{t('Settings.verification_uia_unsupported')}</Text>}
|
||||
>
|
||||
{(ongoingFlow) => (
|
||||
<ActionUIA
|
||||
|
|
@ -228,6 +225,7 @@ type RecoveryKeyDisplayProps = {
|
|||
recoveryKey: string;
|
||||
};
|
||||
function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
|
||||
const { t } = useTranslation();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
|
|
@ -245,12 +243,9 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
|
|||
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text size="T300">
|
||||
Store the Recovery Key in a safe place for future use, as you will need it to verify your
|
||||
identity if you do not have access to other devices.
|
||||
</Text>
|
||||
<Text size="T300">{t('Settings.verification_recovery_key_store_desc')}</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Recovery Key</Text>
|
||||
<Text size="L400">{t('Settings.verification_recovery_key')}</Text>
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'SurfaceVariant' })}
|
||||
style={{
|
||||
|
|
@ -265,16 +260,18 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
|
|||
{safeToDisplayKey}
|
||||
</Text>
|
||||
<Chip onClick={() => setShow(!show)} variant="Secondary" radii="Pill">
|
||||
<Text size="B300">{show ? 'Hide' : 'Show'}</Text>
|
||||
<Text size="B300">
|
||||
{show ? t('Settings.verification_hide') : t('Settings.verification_show')}
|
||||
</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box direction="Column" gap="200">
|
||||
<Button onClick={handleCopy}>
|
||||
<Text size="B400">Copy</Text>
|
||||
<Text size="B400">{t('Settings.verification_copy')}</Text>
|
||||
</Button>
|
||||
<Button onClick={handleDownload} fill="Soft">
|
||||
<Text size="B400">Download</Text>
|
||||
<Text size="B400">{t('Settings.verification_download')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -286,6 +283,7 @@ type DeviceVerificationSetupProps = {
|
|||
};
|
||||
export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerificationSetupProps>(
|
||||
({ onCancel }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [recoveryKey, setRecoveryKey] = useState<string>();
|
||||
|
||||
return (
|
||||
|
|
@ -299,7 +297,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
|||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Setup Device Verification</Text>
|
||||
<Text size="H4">{t('Settings.verification_setup_title')}</Text>
|
||||
</Box>
|
||||
<IconButton size="300" radii="300" onClick={onCancel}>
|
||||
<Icon src={Icons.Cross} />
|
||||
|
|
@ -321,6 +319,7 @@ type DeviceVerificationResetProps = {
|
|||
};
|
||||
export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerificationResetProps>(
|
||||
({ onCancel }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [reset, setReset] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -334,7 +333,7 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
|||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Reset Device Verification</Text>
|
||||
<Text size="H4">{t('Settings.verification_reset_title')}</Text>
|
||||
</Box>
|
||||
<IconButton size="300" radii="300" onClick={onCancel}>
|
||||
<Icon src={Icons.Cross} />
|
||||
|
|
@ -356,16 +355,11 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
|||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="H1">✋🧑🚒🤚</Text>
|
||||
<Text size="T300">Resetting device verification is permanent.</Text>
|
||||
<Text size="T300">
|
||||
Anyone you have verified with will see security alerts and your encryption backup
|
||||
will be lost. You almost certainly do not want to do this, unless you have lost{' '}
|
||||
<b>Recovery Key</b> or <b>Recovery Passphrase</b> and every device you can verify
|
||||
from.
|
||||
</Text>
|
||||
<Text size="T300">{t('Settings.verification_reset_permanent')}</Text>
|
||||
<Text size="T300">{t('Settings.verification_reset_warning')}</Text>
|
||||
</Box>
|
||||
<Button variant="Critical" onClick={() => setReset(true)}>
|
||||
<Text size="B400">Reset</Text>
|
||||
<Text size="B400">{t('Settings.reset')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
color,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { stopPropagation } from '../utils/keyboard';
|
||||
import { SettingTile } from './setting-tile';
|
||||
import { SecretStorageKeyContent } from '../../types/matrix/accountData';
|
||||
|
|
@ -33,6 +34,7 @@ export function ManualVerificationMethodSwitcher({
|
|||
value,
|
||||
onChange,
|
||||
}: ManualVerificationMethodSwitcherProps) {
|
||||
const { t } = useTranslation();
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
|
|
@ -55,8 +57,10 @@ export function ManualVerificationMethodSwitcher({
|
|||
onClick={handleMenu}
|
||||
>
|
||||
<Text as="span" size="B300">
|
||||
{value === ManualVerificationMethod.RecoveryPassphrase && 'Recovery Passphrase'}
|
||||
{value === ManualVerificationMethod.RecoveryKey && 'Recovery Key'}
|
||||
{value === ManualVerificationMethod.RecoveryPassphrase &&
|
||||
t('Settings.verification_recovery_passphrase')}
|
||||
{value === ManualVerificationMethod.RecoveryKey &&
|
||||
t('Settings.verification_recovery_key')}
|
||||
</Text>
|
||||
</Chip>
|
||||
<PopOut
|
||||
|
|
@ -87,7 +91,7 @@ export function ManualVerificationMethodSwitcher({
|
|||
onClick={() => handleSelect(ManualVerificationMethod.RecoveryPassphrase)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T300">Recovery Passphrase</Text>
|
||||
<Text size="T300">{t('Settings.verification_recovery_passphrase')}</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
|
|
@ -98,7 +102,7 @@ export function ManualVerificationMethodSwitcher({
|
|||
onClick={() => handleSelect(ManualVerificationMethod.RecoveryKey)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T300">Recovery Key</Text>
|
||||
<Text size="T300">{t('Settings.verification_recovery_key')}</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
|
|
@ -120,6 +124,7 @@ export function ManualVerificationTile({
|
|||
secretStorageKeyContent,
|
||||
options,
|
||||
}: ManualVerificationTileProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const hasPassphrase = !!secretStorageKeyContent.passphrase;
|
||||
|
|
@ -154,8 +159,12 @@ export function ManualVerificationTile({
|
|||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<SettingTile
|
||||
title="Verify Manually"
|
||||
description={hasPassphrase ? 'Select a verification method.' : 'Provide recovery key.'}
|
||||
title={t('Settings.verify_manually')}
|
||||
description={
|
||||
hasPassphrase
|
||||
? t('Settings.verification_select_method')
|
||||
: t('Settings.verification_provide_key')
|
||||
}
|
||||
after={
|
||||
<Box alignItems="Center" gap="200">
|
||||
{hasPassphrase && (
|
||||
|
|
@ -167,7 +176,7 @@ export function ManualVerificationTile({
|
|||
/>
|
||||
{verifyState.status === AsyncStatus.Success ? (
|
||||
<Text size="T200" style={{ color: color.Success.Main }}>
|
||||
<b>Device verified!</b>
|
||||
<b>{t('Settings.verification_verified_success')}</b>
|
||||
</Text>
|
||||
) : (
|
||||
<Box direction="Column" gap="100">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
toRem,
|
||||
} from 'folds';
|
||||
import React, { MouseEventHandler, ReactNode, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import {
|
||||
headingLevel,
|
||||
|
|
@ -119,6 +120,7 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
|
|||
}
|
||||
|
||||
export function HeadingBlockButton() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlate();
|
||||
const level = headingLevel(editor);
|
||||
const [anchor, setAnchor] = useState<RectCords>();
|
||||
|
|
@ -158,7 +160,12 @@ export function HeadingBlockButton() {
|
|||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Box gap="100">
|
||||
<TooltipProvider
|
||||
tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + 1`} />}
|
||||
tooltip={
|
||||
<BtnTooltip
|
||||
text={t('Room.format_heading', { level: 1 })}
|
||||
shortCode={`${modKey} + 1`}
|
||||
/>
|
||||
}
|
||||
delay={500}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
|
|
@ -173,7 +180,12 @@ export function HeadingBlockButton() {
|
|||
)}
|
||||
</TooltipProvider>
|
||||
<TooltipProvider
|
||||
tooltip={<BtnTooltip text="Heading 2" shortCode={`${modKey} + 2`} />}
|
||||
tooltip={
|
||||
<BtnTooltip
|
||||
text={t('Room.format_heading', { level: 2 })}
|
||||
shortCode={`${modKey} + 2`}
|
||||
/>
|
||||
}
|
||||
delay={500}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
|
|
@ -188,7 +200,12 @@ export function HeadingBlockButton() {
|
|||
)}
|
||||
</TooltipProvider>
|
||||
<TooltipProvider
|
||||
tooltip={<BtnTooltip text="Heading 3" shortCode={`${modKey} + 3`} />}
|
||||
tooltip={
|
||||
<BtnTooltip
|
||||
text={t('Room.format_heading', { level: 3 })}
|
||||
shortCode={`${modKey} + 3`}
|
||||
/>
|
||||
}
|
||||
delay={500}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
|
|
@ -224,6 +241,7 @@ export function HeadingBlockButton() {
|
|||
|
||||
type ExitFormattingProps = { tooltip: ReactNode };
|
||||
export function ExitFormatting({ tooltip }: ExitFormattingProps) {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlate();
|
||||
|
||||
const handleClick = () => {
|
||||
|
|
@ -245,7 +263,7 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
|
|||
size="400"
|
||||
radii="300"
|
||||
>
|
||||
<Text size="B400">{`Exit ${KeySymbol.Hyper}`}</Text>
|
||||
<Text size="B400">{`${t('Room.format_exit')} ${KeySymbol.Hyper}`}</Text>
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
|
|
@ -253,6 +271,7 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
|
|||
}
|
||||
|
||||
export function Toolbar() {
|
||||
const { t } = useTranslation();
|
||||
const editor = useSlate();
|
||||
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
||||
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
|
||||
|
|
@ -271,32 +290,38 @@ export function Toolbar() {
|
|||
<MarkButton
|
||||
format={MarkType.Bold}
|
||||
icon={Icons.Bold}
|
||||
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
|
||||
tooltip={<BtnTooltip text={t('Room.format_bold')} shortCode={`${modKey} + B`} />}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Italic}
|
||||
icon={Icons.Italic}
|
||||
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
|
||||
tooltip={<BtnTooltip text={t('Room.format_italic')} shortCode={`${modKey} + I`} />}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Underline}
|
||||
icon={Icons.Underline}
|
||||
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />}
|
||||
tooltip={
|
||||
<BtnTooltip text={t('Room.format_underline')} shortCode={`${modKey} + U`} />
|
||||
}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.StrikeThrough}
|
||||
icon={Icons.Strike}
|
||||
tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`} />}
|
||||
tooltip={
|
||||
<BtnTooltip text={t('Room.format_strikethrough')} shortCode={`${modKey} + S`} />
|
||||
}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Code}
|
||||
icon={Icons.Code}
|
||||
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} />}
|
||||
tooltip={
|
||||
<BtnTooltip text={t('Room.format_inline_code')} shortCode={`${modKey} + [`} />
|
||||
}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Spoiler}
|
||||
icon={Icons.EyeBlind}
|
||||
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} />}
|
||||
tooltip={<BtnTooltip text={t('Room.format_spoiler')} shortCode={`${modKey} + H`} />}
|
||||
/>
|
||||
</Box>
|
||||
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
||||
|
|
@ -305,22 +330,30 @@ export function Toolbar() {
|
|||
<BlockButton
|
||||
format={BlockType.BlockQuote}
|
||||
icon={Icons.BlockQuote}
|
||||
tooltip={<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`} />}
|
||||
tooltip={
|
||||
<BtnTooltip text={t('Room.format_block_quote')} shortCode={`${modKey} + '`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.CodeBlock}
|
||||
icon={Icons.BlockCode}
|
||||
tooltip={<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`} />}
|
||||
tooltip={
|
||||
<BtnTooltip text={t('Room.format_block_code')} shortCode={`${modKey} + ;`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.OrderedList}
|
||||
icon={Icons.OrderList}
|
||||
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />}
|
||||
tooltip={
|
||||
<BtnTooltip text={t('Room.format_ordered_list')} shortCode={`${modKey} + 7`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.UnorderedList}
|
||||
icon={Icons.UnorderList}
|
||||
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />}
|
||||
tooltip={
|
||||
<BtnTooltip text={t('Room.format_unordered_list')} shortCode={`${modKey} + 8`} />
|
||||
}
|
||||
/>
|
||||
<HeadingBlockButton />
|
||||
</Box>
|
||||
|
|
@ -330,7 +363,10 @@ export function Toolbar() {
|
|||
<Box shrink="No" gap="100">
|
||||
<ExitFormatting
|
||||
tooltip={
|
||||
<BtnTooltip text="Exit Formatting" shortCode={`Escape, ${modKey} + E`} />
|
||||
<BtnTooltip
|
||||
text={t('Room.format_exit_formatting')}
|
||||
shortCode={`Escape, ${modKey} + E`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
|
@ -339,7 +375,15 @@ export function Toolbar() {
|
|||
<Box className={css.MarkdownBtnBox} shrink="No" grow="Yes" justifyContent="End">
|
||||
<TooltipProvider
|
||||
align="End"
|
||||
tooltip={<BtnTooltip text={isMarkdown ? 'Disable Markdown' : 'Enable Markdown'} />}
|
||||
tooltip={
|
||||
<BtnTooltip
|
||||
text={
|
||||
isMarkdown
|
||||
? t('Room.format_markdown_disable')
|
||||
: t('Room.format_markdown_enable')
|
||||
}
|
||||
/>
|
||||
}
|
||||
delay={500}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Spinner,
|
||||
} from 'folds';
|
||||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
|
@ -27,6 +28,7 @@ type LeaveSpacePromptProps = {
|
|||
onCancel: () => void;
|
||||
};
|
||||
export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
|
||||
|
|
@ -66,7 +68,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
|||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Leave Space</Text>
|
||||
<Text size="H4">{t('Room.leave_space_title')}</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
|
@ -74,10 +76,10 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
|||
</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 leave this space?</Text>
|
||||
<Text priority="400">{t('Room.leave_space_confirm')}</Text>
|
||||
{leaveState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} size="T300">
|
||||
Failed to leave space! {leaveState.error.message}
|
||||
{t('Room.leave_space_error', { error: leaveState.error.message })}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -96,7 +98,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
|||
}
|
||||
>
|
||||
<Text size="B400">
|
||||
{leaveState.status === AsyncStatus.Loading ? 'Leaving...' : 'Leave'}
|
||||
{leaveState.status === AsyncStatus.Loading ? t('Room.leaving') : t('Room.leave')}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect, useCallback, FormEventHandler } from 'react';
|
||||
import { Dialog, Text, Box, Button, config, Input, color, Spinner } from 'folds';
|
||||
import { AuthType, MatrixError } from 'matrix-js-sdk';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StageComponentProps } from './types';
|
||||
import { AsyncState, AsyncStatus } from '../../hooks/useAsyncCallback';
|
||||
import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../hooks/types';
|
||||
|
|
@ -18,13 +19,14 @@ function EmailErrorDialog({
|
|||
onRetry: (email: string) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFormSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { retryEmailInput } = evt.target as HTMLFormElement & {
|
||||
retryEmailInput: HTMLInputElement;
|
||||
};
|
||||
const t = retryEmailInput.value;
|
||||
onRetry(t);
|
||||
onRetry(retryEmailInput.value);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -40,7 +42,7 @@ function EmailErrorDialog({
|
|||
<Text size="H4">{title}</Text>
|
||||
<Text>{message}</Text>
|
||||
<Text as="label" size="L400" style={{ paddingTop: config.space.S400 }}>
|
||||
Email
|
||||
{t('Auth.uia_email_label')}
|
||||
</Text>
|
||||
<Input
|
||||
name="retryEmailInput"
|
||||
|
|
@ -53,12 +55,12 @@ function EmailErrorDialog({
|
|||
</Box>
|
||||
<Button variant="Primary" type="submit">
|
||||
<Text as="span" size="B400">
|
||||
Send Verification Email
|
||||
{t('Auth.uia_email_send_button')}
|
||||
</Text>
|
||||
</Button>
|
||||
<Button variant="Critical" fill="None" outlined type="button" onClick={onCancel}>
|
||||
<Text as="span" size="B400">
|
||||
Cancel
|
||||
{t('Auth.uia_email_cancel')}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
|
|
@ -80,6 +82,7 @@ export function EmailStageDialog({
|
|||
emailTokenState: AsyncState<RequestEmailTokenResponse, MatrixError>;
|
||||
requestEmailToken: RequestEmailTokenCallback;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { errorCode, error, session } = stageData;
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
|
|
@ -115,7 +118,7 @@ export function EmailStageDialog({
|
|||
return (
|
||||
<Box direction="Column" alignItems="Center" gap="400">
|
||||
<Spinner variant="Secondary" size="600" />
|
||||
<Text style={{ color: color.Secondary.Main }}>Sending verification email...</Text>
|
||||
<Text style={{ color: color.Secondary.Main }}>{t('Auth.uia_email_sending')}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -123,11 +126,11 @@ export function EmailStageDialog({
|
|||
if (emailTokenState.status === AsyncStatus.Error) {
|
||||
return (
|
||||
<EmailErrorDialog
|
||||
title={emailTokenState.error.errcode ?? 'Verify Email'}
|
||||
title={emailTokenState.error.errcode ?? t('Auth.uia_email_verify_title')}
|
||||
message={
|
||||
emailTokenState.error?.data?.error ??
|
||||
emailTokenState.error.message ??
|
||||
'Failed to send verification Email request.'
|
||||
t('Auth.uia_email_send_error')
|
||||
}
|
||||
onRetry={handleEmailSubmit}
|
||||
onCancel={onCancel}
|
||||
|
|
@ -140,8 +143,8 @@ export function EmailStageDialog({
|
|||
<Dialog>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="H4">Verification Request Sent</Text>
|
||||
<Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
|
||||
<Text size="H4">{t('Auth.uia_email_sent_title')}</Text>
|
||||
<Text>{t('Auth.uia_email_sent_message', { email: emailTokenState.data.email })}</Text>
|
||||
|
||||
{errorCode && (
|
||||
<Text style={{ color: color.Critical.Main }}>{`${errorCode}: ${error}`}</Text>
|
||||
|
|
@ -149,7 +152,7 @@ export function EmailStageDialog({
|
|||
</Box>
|
||||
<Button variant="Primary" onClick={() => handleSubmit(emailTokenState.data.result.sid)}>
|
||||
<Text as="span" size="B400">
|
||||
Continue
|
||||
{t('Auth.uia_email_continue')}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
|
|
@ -160,8 +163,8 @@ export function EmailStageDialog({
|
|||
if (!email) {
|
||||
return (
|
||||
<EmailErrorDialog
|
||||
title="Provide Email"
|
||||
message="Please provide email to send verification request."
|
||||
title={t('Auth.uia_email_provide_title')}
|
||||
message={t('Auth.uia_email_provide_message')}
|
||||
onRetry={handleEmailSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Box, Button, color, config, Icon, Icons, Spinner, Text, Input } from 'folds';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { CutoutCard } from '../cutout-card';
|
||||
import { SettingTile } from '../setting-tile';
|
||||
|
|
@ -14,6 +15,7 @@ type UserKickAlertProps = {
|
|||
ts?: number;
|
||||
};
|
||||
export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
|
||||
const { t } = useTranslation();
|
||||
const time = ts ? timeHourMinute(ts) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts) : undefined;
|
||||
|
||||
|
|
@ -22,7 +24,7 @@ export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
|
|||
<SettingTile>
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Kicked User</Text>
|
||||
<Text size="L400">{t('User.moderation_kicked_user')}</Text>
|
||||
{time && date && (
|
||||
<Text size="T200">
|
||||
{date} {time}
|
||||
|
|
@ -32,16 +34,16 @@ export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
|
|||
<Box direction="Column">
|
||||
{kickedBy && (
|
||||
<Text size="T200">
|
||||
Kicked by: <b>{kickedBy}</b>
|
||||
{t('User.moderation_kicked_by')} <b>{kickedBy}</b>
|
||||
</Text>
|
||||
)}
|
||||
<Text size="T200">
|
||||
{reason ? (
|
||||
<>
|
||||
Reason: <b>{reason}</b>
|
||||
{t('User.moderation_reason_label')} <b>{reason}</b>
|
||||
</>
|
||||
) : (
|
||||
<i>No Reason Provided.</i>
|
||||
<i>{t('User.moderation_no_reason')}</i>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
@ -59,6 +61,7 @@ type UserBanAlertProps = {
|
|||
ts?: number;
|
||||
};
|
||||
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const time = ts ? timeHourMinute(ts) : undefined;
|
||||
|
|
@ -77,7 +80,7 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan
|
|||
<SettingTile>
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Banned User</Text>
|
||||
<Text size="L400">{t('User.moderation_banned_user')}</Text>
|
||||
{time && date && (
|
||||
<Text size="T200">
|
||||
{date} {time}
|
||||
|
|
@ -87,16 +90,16 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan
|
|||
<Box direction="Column">
|
||||
{bannedBy && (
|
||||
<Text size="T200">
|
||||
Banned by: <b>{bannedBy}</b>
|
||||
{t('User.moderation_banned_by')} <b>{bannedBy}</b>
|
||||
</Text>
|
||||
)}
|
||||
<Text size="T200">
|
||||
{reason ? (
|
||||
<>
|
||||
Reason: <b>{reason}</b>
|
||||
{t('User.moderation_reason_label')} <b>{reason}</b>
|
||||
</>
|
||||
) : (
|
||||
<i>No Reason Provided.</i>
|
||||
<i>{t('User.moderation_no_reason')}</i>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
@ -114,7 +117,7 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan
|
|||
before={banning && <Spinner size="100" variant="Critical" fill="Solid" />}
|
||||
disabled={banning}
|
||||
>
|
||||
<Text size="B300">Unban</Text>
|
||||
<Text size="B300">{t('User.moderation_unban')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -131,6 +134,7 @@ type UserInviteAlertProps = {
|
|||
ts?: number;
|
||||
};
|
||||
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const time = ts ? timeHourMinute(ts) : undefined;
|
||||
|
|
@ -149,7 +153,7 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User
|
|||
<SettingTile>
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Invited User</Text>
|
||||
<Text size="L400">{t('User.moderation_invited_user')}</Text>
|
||||
{time && date && (
|
||||
<Text size="T200">
|
||||
{date} {time}
|
||||
|
|
@ -159,16 +163,16 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User
|
|||
<Box direction="Column">
|
||||
{invitedBy && (
|
||||
<Text size="T200">
|
||||
Invited by: <b>{invitedBy}</b>
|
||||
{t('User.moderation_invited_by')} <b>{invitedBy}</b>
|
||||
</Text>
|
||||
)}
|
||||
<Text size="T200">
|
||||
{reason ? (
|
||||
<>
|
||||
Reason: <b>{reason}</b>
|
||||
{t('User.moderation_reason_label')} <b>{reason}</b>
|
||||
</>
|
||||
) : (
|
||||
<i>No Reason Provided.</i>
|
||||
<i>{t('User.moderation_no_reason')}</i>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
@ -188,7 +192,7 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User
|
|||
before={kicking && <Spinner size="100" variant="Success" fill="Soft" />}
|
||||
disabled={kicking}
|
||||
>
|
||||
<Text size="B300">Cancel Invite</Text>
|
||||
<Text size="B300">{t('User.moderation_cancel_invite')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -204,6 +208,7 @@ type UserModerationProps = {
|
|||
canInvite: boolean;
|
||||
};
|
||||
export function UserModeration({ userId, canKick, canBan, canInvite }: UserModerationProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const reasonInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -245,10 +250,10 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
|
|||
<Box direction="Column" gap="400">
|
||||
<Box direction="Column" gap="200">
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="L400">Moderation</Text>
|
||||
<Text size="L400">{t('User.moderation_title')}</Text>
|
||||
<Input
|
||||
ref={reasonInputRef}
|
||||
placeholder="Reason"
|
||||
placeholder={t('User.moderation_reason')}
|
||||
size="300"
|
||||
variant="Background"
|
||||
radii="300"
|
||||
|
|
@ -288,7 +293,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
|
|||
onClick={invite}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">Invite</Text>
|
||||
<Text size="B300">{t('User.moderation_invite')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
{canKick && (
|
||||
|
|
@ -308,7 +313,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
|
|||
onClick={kick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">Kick</Text>
|
||||
<Text size="B300">{t('User.moderation_kick')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
{canBan && (
|
||||
|
|
@ -328,7 +333,7 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
|
|||
onClick={ban}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">Ban</Text>
|
||||
<Text size="B300">{t('User.moderation_ban')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
toRem,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import * as css from './styles.css';
|
||||
import {
|
||||
|
|
@ -33,6 +34,7 @@ type CallControlsProps = {
|
|||
callEmbed: CallEmbed;
|
||||
};
|
||||
export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const controlRef = useRef<HTMLDivElement>(null);
|
||||
const [compact, setCompact] = useState(document.body.clientWidth < 500);
|
||||
|
||||
|
|
@ -140,7 +142,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||
onClick={handleSpotlightClick}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
{spotlight ? 'Grid View' : 'Spotlight View'}
|
||||
{spotlight ? t('Call.view_grid') : t('Call.view_spotlight')}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
|
|
@ -150,7 +152,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||
onClick={handleReactionsClick}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
Reactions
|
||||
{t('Call.reactions')}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</>
|
||||
|
|
@ -162,7 +164,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||
onClick={handleSettingsClick}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
Settings
|
||||
{t('Call.settings')}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
|
|
@ -198,7 +200,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||
}
|
||||
disabled={exiting}
|
||||
>
|
||||
<Text size="B400">End</Text>
|
||||
<Text size="B400">{t('Call.end_call')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { CallMembership, SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc';
|
||||
import React, { useState } from 'react';
|
||||
import { Avatar, Box, Icon, Icons, Text } from 'folds';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||
|
|
@ -84,6 +85,7 @@ export function CallMemberRenderer({
|
|||
members: CallMembership[];
|
||||
max?: number;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
|
||||
const truncatedMembers = viewMore ? members : members.slice(0, 4);
|
||||
|
|
@ -105,11 +107,11 @@ export function CallMemberRenderer({
|
|||
<Box grow="Yes" gap="300" alignItems="Center">
|
||||
{viewMore ? (
|
||||
<Text size="L400" truncate>
|
||||
Collapse
|
||||
{t('Call.collapse')}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="L400" truncate>
|
||||
{remaining === 0 ? `+${remaining} Other` : `+${remaining} Others`}
|
||||
{t('Call.others_count', { count: remaining })}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { RefObject, useRef } from 'react';
|
||||
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { PrescreenControls } from './PrescreenControls';
|
||||
|
|
@ -16,9 +17,10 @@ import { CallControls } from './CallControls';
|
|||
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||
|
||||
function LivekitServerMissingMessage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
|
||||
Your homeserver does not support calling. But you can still join call started by others.
|
||||
{t('Call.homeserver_no_calls')}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
|
@ -30,6 +32,7 @@ function JoinMessage({
|
|||
hasParticipant?: boolean;
|
||||
livekitSupported?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (hasParticipant) return null;
|
||||
|
||||
if (livekitSupported === false) {
|
||||
|
|
@ -38,28 +41,31 @@ function JoinMessage({
|
|||
|
||||
return (
|
||||
<Text style={{ margin: 'auto' }} size="L400" align="Center">
|
||||
Voice chat’s empty — Be the first to hop in!
|
||||
{t('Call.empty_be_first')}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function NoPermissionMessage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Text style={{ margin: 'auto' }} size="L400" align="Center">
|
||||
You don't have permission to join!
|
||||
{t('Call.no_permission_to_join')}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function AlreadyInCallMessage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Text style={{ margin: 'auto', color: color.Warning.Main }} size="L400" align="Center">
|
||||
Already in another call — End the current call to join!
|
||||
{t('Call.already_in_other_call')}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function CallPrescreen() {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const livekitSupported = useLivekitSupport();
|
||||
|
|
@ -86,11 +92,11 @@ function CallPrescreen() {
|
|||
{hasParticipant && (
|
||||
<Header size="300">
|
||||
<Box grow="Yes" alignItems="Center">
|
||||
<Text size="L400">Participant</Text>
|
||||
<Text size="L400">{t('Call.participant')}</Text>
|
||||
</Box>
|
||||
<Badge variant="Critical" fill="Solid" size="400">
|
||||
<Text as="span" size="L400" truncate>
|
||||
{callMembers.length} Live
|
||||
{t('Call.live_count', { count: callMembers.length })}
|
||||
</Text>
|
||||
</Badge>
|
||||
</Header>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import * as css from './styles.css';
|
||||
import { ChatButton, ControlDivider, MicrophoneButton, VideoButton } from './Controls';
|
||||
|
|
@ -11,6 +12,7 @@ type PrescreenControlsProps = {
|
|||
canJoin?: boolean;
|
||||
};
|
||||
export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const room = useRoom();
|
||||
const callEmbed = useCallEmbed();
|
||||
const callJoined = useCallJoined(callEmbed);
|
||||
|
|
@ -57,7 +59,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
|||
)
|
||||
}
|
||||
>
|
||||
<Text size="B400">Join</Text>
|
||||
<Text size="B400">{t('Call.join')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Box, Chip, Icon, IconButton, Icons, Line, Scroll, Spinner, Text, config
|
|||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
|
||||
import type { AccountDataEvents, StateEvents } from 'matrix-js-sdk';
|
||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||
|
|
@ -150,6 +151,7 @@ const useCanDropLobbyItem = (
|
|||
};
|
||||
|
||||
export function Lobby() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const mx = useMatrixClient();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
|
@ -459,7 +461,7 @@ export function Lobby() {
|
|||
radii="Pill"
|
||||
outlined
|
||||
size="300"
|
||||
aria-label="Scroll to Top"
|
||||
aria-label={t('Room.lobby_scroll_to_top')}
|
||||
>
|
||||
<Icon src={Icons.ChevronTop} size="300" />
|
||||
</IconButton>
|
||||
|
|
@ -535,7 +537,7 @@ export function Lobby() {
|
|||
radii="Pill"
|
||||
before={<Spinner variant="Secondary" fill="Soft" size="100" />}
|
||||
>
|
||||
<Text size="L400">Reordering</Text>
|
||||
<Text size="L400">{t('Room.lobby_reordering')}</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
toRem,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PageHeader } from '../../components/page';
|
||||
import { useSetSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
|
@ -45,6 +46,7 @@ type LobbyMenuProps = {
|
|||
};
|
||||
const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
||||
({ powerLevels, requestClose }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const space = useSpace();
|
||||
const creators = useRoomCreators(space);
|
||||
|
|
@ -87,7 +89,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
|||
disabled={!canInvite}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Invite
|
||||
{t('Room.invite')}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
|
|
@ -97,7 +99,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
|||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Space Settings
|
||||
{t('Room.lobby_space_settings')}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
|
|
@ -116,7 +118,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
|||
aria-pressed={promptLeave}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Leave Space
|
||||
{t('Room.lobby_leave_space')}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptLeave && (
|
||||
|
|
@ -140,6 +142,7 @@ type LobbyHeaderProps = {
|
|||
powerLevels: IPowerLevels;
|
||||
};
|
||||
export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const space = useSpace();
|
||||
|
|
@ -213,7 +216,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Members</Text>
|
||||
<Text>{t('Room.members')}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
|
|
@ -234,7 +237,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>More Options</Text>
|
||||
<Text>{t('Room.more_options')}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
toRem,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
|
||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
|
|
@ -44,6 +45,7 @@ type RoomJoinButtonProps = {
|
|||
via?: string[];
|
||||
};
|
||||
function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
||||
|
|
@ -91,7 +93,7 @@ function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
|
|||
onClick={join}
|
||||
disabled={!canJoin}
|
||||
>
|
||||
<Text size="B300">Join</Text>
|
||||
<Text size="B300">{t('Room.lobby_join')}</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -127,6 +129,7 @@ type RoomProfileErrorProps = {
|
|||
via?: string[];
|
||||
};
|
||||
function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProfileErrorProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box grow="Yes" gap="300">
|
||||
<Avatar>
|
||||
|
|
@ -146,12 +149,12 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
|
|||
<Box grow="Yes" direction="Column" className={css.ErrorNameContainer}>
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Text size="H5" truncate>
|
||||
Unknown
|
||||
{t('Room.lobby_unknown')}
|
||||
</Text>
|
||||
{suggested && (
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
<Text size="L400">{t('Room.lobby_suggested')}</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
|
|
@ -159,7 +162,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
|
|||
<Box gap="200" alignItems="Center">
|
||||
{inaccessibleRoom ? (
|
||||
<Badge variant="Secondary" fill="Soft" radii="300" size="500">
|
||||
<Text size="L400">Inaccessible</Text>
|
||||
<Text size="L400">{t('Room.lobby_inaccessible')}</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Text size="T200" truncate>
|
||||
|
|
@ -195,6 +198,7 @@ function RoomProfile({
|
|||
joinRule,
|
||||
options,
|
||||
}: RoomProfileProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box grow="Yes" gap="300">
|
||||
<Avatar>
|
||||
|
|
@ -213,7 +217,7 @@ function RoomProfile({
|
|||
{suggested && (
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
<Text size="L400">{t('Room.lobby_suggested')}</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
|
|
@ -221,7 +225,12 @@ function RoomProfile({
|
|||
<Box gap="200" alignItems="Center">
|
||||
{memberCount && (
|
||||
<Box shrink="No" gap="200">
|
||||
<Text size="T200" priority="300">{`${millify(memberCount)} Members`}</Text>
|
||||
<Text size="T200" priority="300">
|
||||
{t('Room.lobby_members_count', {
|
||||
count: memberCount,
|
||||
formattedCount: millify(memberCount),
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{memberCount && topic && (
|
||||
|
|
@ -311,6 +320,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
|
|||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const { roomId, content } = item;
|
||||
|
|
@ -359,7 +369,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
|
|||
fill="None"
|
||||
size="400"
|
||||
radii="Pill"
|
||||
aria-label="Open Room"
|
||||
aria-label={t('Room.lobby_open_room')}
|
||||
>
|
||||
<Icon size="50" src={Icons.ArrowRight} />
|
||||
</Chip>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ type InaccessibleSpaceProfileProps = {
|
|||
suggested?: boolean;
|
||||
};
|
||||
function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfileProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Chip
|
||||
as="span"
|
||||
|
|
@ -72,7 +73,7 @@ function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfil
|
|||
roomId={roomId}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H6">
|
||||
U
|
||||
{nameInitials(t('Room.lobby_unknown'))}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -81,15 +82,15 @@ function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfil
|
|||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Text size="H4" truncate>
|
||||
Unknown
|
||||
{t('Room.lobby_unknown')}
|
||||
</Text>
|
||||
|
||||
<Badge variant="Secondary" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Inaccessible</Text>
|
||||
<Text size="L400">{t('Room.lobby_inaccessible')}</Text>
|
||||
</Badge>
|
||||
{suggested && (
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
<Text size="L400">{t('Room.lobby_suggested')}</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -111,6 +112,7 @@ function UnjoinedSpaceProfile({
|
|||
avatarUrl,
|
||||
suggested,
|
||||
}: UnjoinedSpaceProfileProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
||||
|
|
@ -145,11 +147,11 @@ function UnjoinedSpaceProfile({
|
|||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Text size="H4" truncate>
|
||||
{name || 'Unknown'}
|
||||
{name || t('Room.lobby_unknown')}
|
||||
</Text>
|
||||
{suggested && (
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
<Text size="L400">{t('Room.lobby_suggested')}</Text>
|
||||
</Badge>
|
||||
)}
|
||||
{joinState.status === AsyncStatus.Error && (
|
||||
|
|
@ -182,6 +184,7 @@ function SpaceProfile({
|
|||
categoryId,
|
||||
handleClose,
|
||||
}: SpaceProfileProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Chip
|
||||
data-category-id={categoryId}
|
||||
|
|
@ -211,7 +214,7 @@ function SpaceProfile({
|
|||
</Text>
|
||||
{suggested && (
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
<Text size="L400">{t('Room.lobby_suggested')}</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -225,6 +228,7 @@ type RootSpaceProfileProps = {
|
|||
handleClose?: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Chip
|
||||
data-category-id={categoryId}
|
||||
|
|
@ -236,7 +240,7 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
|
|||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Text size="H4" truncate>
|
||||
Rooms
|
||||
{t('Room.lobby_rooms')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Chip>
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export function MessageSearch({
|
|||
radii="Pill"
|
||||
outlined
|
||||
size="300"
|
||||
aria-label="Scroll to Top"
|
||||
aria-label={t('Room.msg_search_scroll_to_top')}
|
||||
>
|
||||
<Icon src={Icons.ChevronTop} size="300" />
|
||||
</IconButton>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { EventType } from 'matrix-js-sdk';
|
|||
import { ReactEditor } from 'slate-react';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
|
|
@ -54,6 +55,7 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
|||
};
|
||||
|
||||
export function RoomView({ eventId }: { eventId?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||
const composerWrapRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -200,7 +202,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
|||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text align="Center">You do not have permission to post in this room</Text>
|
||||
<Text align="Center">{t('Room.no_post_permission')}</Text>
|
||||
</RoomInputPlaceholder>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -85,10 +85,14 @@ function LearnStartVerificationFromOtherDevice() {
|
|||
<ul style={{ margin: `${config.space.S100} 0` }}>
|
||||
<li>{t('Settings.verify_step_1')}</li>
|
||||
<li>
|
||||
Open <i>{t('Settings.verify_step_2_settings')}</i>.
|
||||
{t('Settings.verification_step_open', {
|
||||
settings: t('Settings.verify_step_2_settings'),
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
Find this device in <i>{t('Settings.verify_step_3_section')}</i> section.
|
||||
{t('Settings.verification_step_find', {
|
||||
section: t('Settings.verify_step_3_section'),
|
||||
})}
|
||||
</li>
|
||||
<li>{t('Settings.verify_step_4')}</li>
|
||||
</ul>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue