localize direct messages

This commit is contained in:
heaven 2026-04-14 01:20:26 +03:00
parent fc8346e50d
commit 77fda29820
17 changed files with 429 additions and 159 deletions

View file

@ -347,5 +347,109 @@
"toggle_chat": "Toggle Chat",
"live_count": "{{count}} Live",
"open": "Open"
},
"Direct": {
"direct_messages": "Direct Messages",
"mark_as_read": "Mark as Read",
"no_direct_messages": "No Direct Messages",
"no_direct_messages_desc": "You do not have any direct messages yet.",
"direct_message": "Direct Message",
"create_chat": "Create Chat",
"create_chat_subtitle": "Start a private, encrypted chat by entering a user ID.",
"chats": "Chats",
"user_id": "User ID",
"user_id_placeholder": "@username:server",
"invalid_user_id": "Please enter a valid User ID.",
"options": "Options",
"e2e_encryption": "End-to-End Encryption",
"e2e_encryption_desc": "Once this feature is enabled, it can't be disabled after the room is created.",
"rate_limited": "Server rate-limited your request for {{minutes}} minutes!",
"create": "Create"
},
"Room": {
"new_messages": "New Messages",
"jump_to_unread": "Jump to Unread",
"mark_as_read": "Mark as Read",
"jump_to_latest": "Jump to Latest",
"today": "Today",
"yesterday": "Yesterday",
"view_reactions": "View Reactions",
"read_receipts": "Read Receipts",
"view_source": "View Source",
"source_code": "Source Code",
"copy_link": "Copy Link",
"pin_message": "Pin Message",
"unpin_message": "Unpin Message",
"add_reaction": "Add Reaction",
"reply": "Reply",
"reply_in_thread": "Reply in Thread",
"edit_message": "Edit Message",
"delete_message": "Delete Message",
"delete_confirm": "This action is irreversible! Are you sure that you want to delete this message?",
"reason": "Reason",
"optional": "optional",
"delete_error": "Failed to delete message! Please try again.",
"deleting": "Deleting...",
"delete": "Delete",
"report_message": "Report Message",
"report_desc": "Report this message to server, which may then notify the appropriate people to take action.",
"report_reason": "Reason",
"report_error": "Failed to report message! Please try again.",
"report_success": "Message has been reported to server.",
"reporting": "Reporting...",
"report": "Report",
"no_reason": "No reason provided",
"is_typing": " is typing...",
"and": " and ",
"are_typing": " are typing...",
"others_count": "{{count}} others",
"drop_typing": "Dismiss typing indicator",
"members": "Members",
"members_count": "{{count}} Members",
"hide_members": "Hide Members",
"show_members": "Show Members",
"more_options": "More Options",
"close": "Close",
"search": "Search",
"notifications": "Notifications",
"invite": "Invite",
"room_settings": "Room Settings",
"jump_to_time": "Jump to Time",
"leave_room": "Leave Room",
"send_message": "Send a message...",
"drop_files": "Drop Files in \"{{name}}\"",
"drag_drop_desc": "Drag and drop files here or click for selection dialog",
"is_following": " is following the conversation.",
"are_following": " are following the conversation.",
"others_following_count": "{{count}} others",
"pinned_messages": "Pinned Messages",
"no_pinned_messages": "No Pinned Messages",
"no_pinned_messages_desc": "Users with sufficient permissions can pin messages from the message context menu.",
"open": "Open",
"failed_to_load": "Failed to load message!",
"time_label": "Time",
"date_label": "Date",
"preset": "Preset",
"beginning": "Beginning",
"open_timeline": "Open Timeline",
"message_deleted": "This message has been deleted",
"message_deleted_reason": "This message has been deleted. {{reason}}",
"unsupported_message": "Unsupported message",
"failed_to_load_message": "Failed to load message",
"unable_to_decrypt": "Unable to decrypt message",
"not_decrypted_yet": "This message is not decrypted yet",
"broken_message": "Broken message",
"empty_message": "Empty message",
"edited": " (edited)"
}
}

View file

@ -347,5 +347,109 @@
"toggle_chat": "Переключить чат",
"live_count": "{{count}} В эфире",
"open": "Открыть"
},
"Direct": {
"direct_messages": "Личные сообщения",
"mark_as_read": "Отметить прочитанным",
"no_direct_messages": "Нет личных сообщений",
"no_direct_messages_desc": "У вас ещё нет личных сообщений.",
"direct_message": "Новый чат",
"create_chat": "Новый чат",
"create_chat_subtitle": "Начните приватный зашифрованный чат, указав ID пользователя.",
"chats": "Чаты",
"user_id": "ID пользователя",
"user_id_placeholder": "@username:server",
"invalid_user_id": "Введите корректный ID пользователя.",
"options": "Параметры",
"e2e_encryption": "Сквозное шифрование",
"e2e_encryption_desc": "После включения эту функцию нельзя отключить после создания комнаты.",
"rate_limited": "Сервер ограничил частоту запросов на {{minutes}} мин.!",
"create": "Создать"
},
"Room": {
"new_messages": "Новые сообщения",
"jump_to_unread": "К непрочитанным",
"mark_as_read": "Отметить прочитанным",
"jump_to_latest": "К последним",
"today": "Сегодня",
"yesterday": "Вчера",
"view_reactions": "Реакции",
"read_receipts": "Подтверждения прочтения",
"view_source": "Исходный код",
"source_code": "Исходный код",
"copy_link": "Копировать ссылку",
"pin_message": "Закрепить сообщение",
"unpin_message": "Открепить сообщение",
"add_reaction": "Добавить реакцию",
"reply": "Ответить",
"reply_in_thread": "Ответить в треде",
"edit_message": "Редактировать",
"delete_message": "Удалить сообщение",
"delete_confirm": "Это действие необратимо! Вы уверены, что хотите удалить это сообщение?",
"reason": "Причина",
"optional": "необязательно",
"delete_error": "Не удалось удалить сообщение! Попробуйте снова.",
"deleting": "Удаление...",
"delete": "Удалить",
"report_message": "Пожаловаться",
"report_desc": "Сообщить о нарушении на сервер, который может уведомить ответственных лиц для принятия мер.",
"report_reason": "Причина",
"report_error": "Не удалось отправить жалобу! Попробуйте снова.",
"report_success": "Жалоба отправлена на сервер.",
"reporting": "Отправка...",
"report": "Пожаловаться",
"no_reason": "Причина не указана",
"is_typing": " печатает...",
"and": " и ",
"are_typing": " печатают...",
"others_count": "ещё {{count}}",
"drop_typing": "Скрыть индикатор набора",
"members": "Участники",
"members_count": "{{count}} участников",
"hide_members": "Скрыть участников",
"show_members": "Показать участников",
"more_options": "Ещё",
"close": "Закрыть",
"search": "Поиск",
"notifications": "Уведомления",
"invite": "Пригласить",
"room_settings": "Настройки комнаты",
"jump_to_time": "Перейти к дате",
"leave_room": "Покинуть комнату",
"send_message": "Написать сообщение...",
"drop_files": "Перетащите файлы в \"{{name}}\"",
"drag_drop_desc": "Перетащите файлы сюда или нажмите для выбора",
"is_following": " читает чат.",
"are_following": " читают чат.",
"others_following_count": "ещё {{count}}",
"pinned_messages": "Закреплённые сообщения",
"no_pinned_messages": "Нет закреплённых сообщений",
"no_pinned_messages_desc": "Пользователи с достаточным уровнем прав могут закреплять сообщения через контекстное меню.",
"open": "Открыть",
"failed_to_load": "Не удалось загрузить сообщение!",
"time_label": "Время",
"date_label": "Дата",
"preset": "Пресет",
"beginning": "Начало",
"open_timeline": "Открыть ленту",
"message_deleted": "Сообщение было удалено",
"message_deleted_reason": "Сообщение было удалено. {{reason}}",
"unsupported_message": "Неподдерживаемое сообщение",
"failed_to_load_message": "Не удалось загрузить сообщение",
"unable_to_decrypt": "Не удалось расшифровать сообщение",
"not_decrypted_yet": "Сообщение ещё не расшифровано",
"broken_message": "Повреждённое сообщение",
"empty_message": "Пустое сообщение",
"edited": " (изменено)"
}
}

View file

@ -1,5 +1,6 @@
import React, { ComponentProps } from 'react';
import { Text, as } from 'folds';
import { useTranslation } from 'react-i18next';
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
export type TimeProps = {
@ -23,6 +24,7 @@ export type TimeProps = {
*/
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
const { t } = useTranslation();
const formattedTime = timeHourMinute(ts, hour24Clock);
let time = '';
@ -31,7 +33,7 @@ export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
} else if (today(ts)) {
time = formattedTime;
} else if (yesterday(ts)) {
time = `Yesterday ${formattedTime}`;
time = `${t('Room.yesterday')} ${formattedTime}`;
} else {
time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
}

View file

@ -1,66 +1,91 @@
import { Box, Icon, Icons, Text, as, color, config } from 'folds';
import React from 'react';
import { useTranslation } from 'react-i18next';
const warningStyle = { color: color.Warning.Main, opacity: config.opacity.P300 };
const criticalStyle = { color: color.Critical.Main, opacity: config.opacity.P300 };
export const MessageDeletedContent = as<'div', { children?: never; reason?: string }>(
({ reason, ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Delete} />
{reason ? (
<i>This message has been deleted. {reason}</i>
) : (
<i>This message has been deleted</i>
)}
</Box>
)
({ reason, ...props }, ref) => {
const { t } = useTranslation();
return (
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Delete} />
{reason ? (
<i>{t('Room.message_deleted_reason', { reason })}</i>
) : (
<i>{t('Room.message_deleted')}</i>
)}
</Box>
);
}
);
export const MessageUnsupportedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>Unsupported message</i>
</Box>
));
export const MessageUnsupportedContent = as<'div', { children?: never }>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>{t('Room.unsupported_message')}</i>
</Box>
);
});
export const MessageFailedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>Failed to load message</i>
</Box>
));
export const MessageFailedContent = as<'div', { children?: never }>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>{t('Room.failed_to_load_message')}</i>
</Box>
);
});
export const MessageBadEncryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Lock} />
<i>Unable to decrypt message</i>
</Box>
));
export const MessageBadEncryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Lock} />
<i>{t('Room.unable_to_decrypt')}</i>
</Box>
);
});
export const MessageNotDecryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Lock} />
<i>This message is not decrypted yet</i>
</Box>
));
export const MessageNotDecryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Lock} />
<i>{t('Room.not_decrypted_yet')}</i>
</Box>
);
});
export const MessageBrokenContent = as<'div', { children?: never }>(({ ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>Broken message</i>
</Box>
));
export const MessageBrokenContent = as<'div', { children?: never }>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>{t('Room.broken_message')}</i>
</Box>
);
});
export const MessageEmptyContent = as<'div', { children?: never }>(({ ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>Empty message</i>
</Box>
));
export const MessageEmptyContent = as<'div', { children?: never }>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Box as="span" alignItems="Center" gap="100" style={criticalStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Warning} />
<i>{t('Room.empty_message')}</i>
</Box>
);
});
export const MessageEditedContent = as<'span', { children?: never }>(({ ...props }, ref) => (
<Text as="span" size="T200" priority="300" {...props} ref={ref}>
{' (edited)'}
</Text>
));
export const MessageEditedContent = as<'span', { children?: never }>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Text as="span" size="T200" priority="300" {...props} ref={ref}>
{t('Room.edited')}
</Text>
);
});

View file

@ -1,5 +1,6 @@
import { Box, Button, color, config, Icon, Icons, Input, Spinner, Switch, Text } from 'folds';
import React, { FormEventHandler, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ICreateRoomStateEvent, MatrixError, Preset, Visibility } from 'matrix-js-sdk';
import { useNavigate } from 'react-router-dom';
import { SettingTile } from '../../components/setting-tile';
@ -17,6 +18,7 @@ type CreateChatProps = {
defaultUserId?: string;
};
export function CreateChat({ defaultUserId }: CreateChatProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const alive = useAlive();
const navigate = useNavigate();
@ -75,10 +77,10 @@ export function CreateChat({ defaultUserId }: CreateChatProps) {
return (
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
<Box direction="Column" gap="100">
<Text size="L400">User ID</Text>
<Text size="L400">{t('Direct.user_id')}</Text>
<Input
defaultValue={defaultUserId}
placeholder="@username:server"
placeholder={t('Direct.user_id_placeholder')}
name="userIdInput"
variant="SurfaceVariant"
size="500"
@ -92,13 +94,13 @@ export function CreateChat({ defaultUserId }: CreateChatProps) {
<Box style={{ color: color.Critical.Main }} alignItems="Center" gap="100">
<Icon src={Icons.Warning} filled size="50" />
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>Please enter a valid User ID.</b>
<b>{t('Direct.invalid_user_id')}</b>
</Text>
</Box>
)}
</Box>
<Box shrink="No" direction="Column" gap="100">
<Text size="L400">Options</Text>
<Text size="L400">{t('Direct.options')}</Text>
<SequenceCard
style={{ padding: config.space.S300 }}
variant="SurfaceVariant"
@ -106,8 +108,8 @@ export function CreateChat({ defaultUserId }: CreateChatProps) {
gap="500"
>
<SettingTile
title="End-to-End Encryption"
description="Once this feature is enabled, it can't be disabled after the room is created."
title={t('Direct.e2e_encryption')}
description={t('Direct.e2e_encryption_desc')}
after={
<Switch
variant="Primary"
@ -125,9 +127,11 @@ export function CreateChat({ defaultUserId }: CreateChatProps) {
<Text size="T300" style={{ color: color.Critical.Main }}>
<b>
{error instanceof MatrixError && error.name === ErrorCode.M_LIMIT_EXCEEDED
? `Server rate-limited your request for ${millisecondsToMinutes(
(error.data.retry_after_ms as number | undefined) ?? 0
)} minutes!`
? t('Direct.rate_limited', {
minutes: millisecondsToMinutes(
(error.data.retry_after_ms as number | undefined) ?? 0
),
})
: error.message}
</b>
</Text>
@ -142,7 +146,7 @@ export function CreateChat({ defaultUserId }: CreateChatProps) {
disabled={disabled}
before={loading && <Spinner variant="Primary" fill="Solid" size="200" />}
>
<Text size="B500">Create</Text>
<Text size="B500">{t('Direct.create')}</Text>
</Button>
</Box>
</Box>

View file

@ -29,6 +29,7 @@ import {
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import * as css from './MembersDrawer.css';
import { useMatrixClient } from '../../hooks/useMatrixClient';
@ -64,14 +65,15 @@ type MemberDrawerHeaderProps = {
room: Room;
};
function MemberDrawerHeader({ room }: MemberDrawerHeaderProps) {
const { t } = useTranslation();
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
return (
<Header className={css.MembersDrawerHeader} variant="Background" size="600">
<Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text title={`${room.getJoinedMemberCount()} Members`} size="H5" truncate>
{`${millify(room.getJoinedMemberCount())} Members`}
<Text title={t('Room.members_count', { count: room.getJoinedMemberCount() })} size="H5" truncate>
{t('Room.members_count', { count: millify(room.getJoinedMemberCount()) })}
</Text>
</Box>
<Box shrink="No" alignItems="Center">
@ -81,7 +83,7 @@ function MemberDrawerHeader({ room }: MemberDrawerHeaderProps) {
offset={4}
tooltip={
<Tooltip>
<Text>Close</Text>
<Text>{t('Room.close')}</Text>
</Tooltip>
}
>

View file

@ -9,6 +9,7 @@ import React, {
} from 'react';
import { useAtom, useAtomValue } from 'jotai';
import { isKeyHotkey } from 'is-hotkey';
import { useTranslation } from 'react-i18next';
import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk';
import { ReactEditor } from 'slate-react';
import { Transforms, Editor } from 'slate';
@ -126,6 +127,7 @@ interface RoomInputProps {
}
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
({ editor, fileDropContainerRef, roomId, room }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
@ -497,9 +499,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
>
<Icon size="600" src={Icons.File} />
<Text size="H4" align="Center">
{`Drop Files in "${room?.name || 'Room'}"`}
{t('Room.drop_files', { name: room?.name || 'Room' })}
</Text>
<Text align="Center">Drag and drop files here or click for selection dialog</Text>
<Text align="Center">{t('Room.drag_drop_desc')}</Text>
</Box>
</Dialog>
</OverlayCenter>
@ -539,7 +541,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<CustomEditor
editableName="RoomInput"
editor={editor}
placeholder="Send a message..."
placeholder={t('Room.send_message')}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onPaste={handlePaste}
@ -624,12 +626,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
onCustomEmojiSelect={handleEmoticonSelect}
onStickerSelect={handleStickerSelect}
requestClose={() => {
setEmojiBoardTab((t) => {
if (t) {
setEmojiBoardTab((tab) => {
if (tab) {
if (!mobileOrTablet()) ReactEditor.focus(editor);
return undefined;
}
return t;
return tab;
});
}}
/>

View file

@ -1676,7 +1676,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<MessageBase space={messageSpacing}>
<TimelineDivider style={{ color: color.Success.Main }} variant="Inherit">
<Badge as="span" size="500" variant="Success" fill="Solid" radii="300">
<Text size="L400">New Messages</Text>
<Text size="L400">{t('Room.new_messages')}</Text>
</Badge>
</TimelineDivider>
</MessageBase>
@ -1689,8 +1689,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Badge as="span" size="500" variant="Secondary" fill="None" radii="300">
<Text size="L400">
{(() => {
if (today(mEvent.getTs())) return 'Today';
if (yesterday(mEvent.getTs())) return 'Yesterday';
if (today(mEvent.getTs())) return t('Room.today');
if (yesterday(mEvent.getTs())) return t('Room.yesterday');
return timeDayMonthYear(mEvent.getTs());
})()}
</Text>
@ -1726,7 +1726,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
before={<Icon size="50" src={Icons.MessageUnread} />}
onClick={handleJumpToUnread}
>
<Text size="L400">Jump to Unread</Text>
<Text size="L400">{t('Room.jump_to_unread')}</Text>
</Chip>
<Chip
@ -1736,7 +1736,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
before={<Icon size="50" src={Icons.CheckTwice} />}
onClick={handleMarkAsRead}
>
<Text size="L400">Mark as Read</Text>
<Text size="L400">{t('Room.mark_as_read')}</Text>
</Chip>
</TimelineFloat>
)}
@ -1836,7 +1836,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
before={<Icon size="50" src={Icons.ArrowBottom} />}
onClick={handleJumpToLatest}
>
<Text size="L400">Jump to Latest</Text>
<Text size="L400">{t('Room.jump_to_latest')}</Text>
</Chip>
</TimelineFloat>
)}

View file

@ -14,6 +14,7 @@ import {
import { Room } from 'matrix-js-sdk';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix';
@ -33,6 +34,7 @@ export type RoomViewFollowingProps = {
};
export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
({ className, room, ...props }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const [open, setOpen] = useState(false);
const latestEvent = useRoomLatestRenderedEvent(room);
@ -83,7 +85,7 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
<>
<b>{names[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' is following the conversation.'}
{t('Room.is_following')}
</Text>
</>
)}
@ -91,11 +93,11 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
<>
<b>{names[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
{t('Room.and')}
</Text>
<b>{names[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' are following the conversation.'}
{t('Room.are_following')}
</Text>
</>
)}
@ -107,11 +109,11 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
</Text>
<b>{names[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
{t('Room.and')}
</Text>
<b>{names[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' are following the conversation.'}
{t('Room.are_following')}
</Text>
</>
)}
@ -127,11 +129,11 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
</Text>
<b>{names[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
{t('Room.and')}
</Text>
<b>{names.length - 3} others</b>
<b>{t('Room.others_following_count', { count: names.length - 3 })}</b>
<Text as="span" size="Inherit" priority="300">
{' are following the conversation.'}
{t('Room.are_following')}
</Text>
</>
)}

View file

@ -23,6 +23,7 @@ import {
Spinner,
} from 'folds';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Room } from 'matrix-js-sdk';
import { useStateEvent } from '../../hooks/useStateEvent';
import { PageHeader } from '../../components/page';
@ -74,6 +75,7 @@ type RoomMenuProps = {
requestClose: () => void;
};
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
@ -131,7 +133,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
disabled={!unread}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Mark as Read
{t('Room.mark_as_read')}
</Text>
</MenuItem>
<RoomNotificationModeSwitcher roomId={room.roomId} value={notificationMode}>
@ -150,7 +152,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
onClick={handleOpen}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Notifications
{t('Room.notifications')}
</Text>
</MenuItem>
)}
@ -169,7 +171,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
disabled={!canInvite}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Invite
{t('Room.invite')}
</Text>
</MenuItem>
<MenuItem
@ -179,7 +181,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Copy Link
{t('Room.copy_link')}
</Text>
</MenuItem>
<MenuItem
@ -189,7 +191,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Room Settings
{t('Room.room_settings')}
</Text>
</MenuItem>
<UseStateProvider initial={false}>
@ -203,7 +205,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
aria-pressed={promptJump}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Jump to Time
{t('Room.jump_to_time')}
</Text>
</MenuItem>
{promptJump && (
@ -235,7 +237,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
aria-pressed={promptLeave}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Leave Room
{t('Room.leave_room')}
</Text>
</MenuItem>
{promptLeave && (
@ -254,6 +256,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
});
export function RoomViewHeader({ callView }: { callView?: boolean }) {
const { t } = useTranslation();
const navigate = useNavigate();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
@ -385,7 +388,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
offset={4}
tooltip={
<Tooltip>
<Text>Search</Text>
<Text>{t('Room.search')}</Text>
</Tooltip>
}
>
@ -401,7 +404,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
offset={4}
tooltip={
<Tooltip>
<Text>Pinned Messages</Text>
<Text>{t('Room.pinned_messages')}</Text>
</Tooltip>
}
>
@ -461,9 +464,9 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
tooltip={
<Tooltip>
{callView ? (
<Text>Members</Text>
<Text>{t('Room.members')}</Text>
) : (
<Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
<Text>{peopleDrawer ? t('Room.hide_members') : t('Room.show_members')}</Text>
)}
</Tooltip>
}
@ -482,7 +485,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
offset={4}
tooltip={
<Tooltip>
<Text>More Options</Text>
<Text>{t('Room.more_options')}</Text>
</Tooltip>
}
>

View file

@ -2,6 +2,7 @@ import React from 'react';
import { Box, Icon, IconButton, Icons, Text, as } from 'folds';
import { Room } from 'matrix-js-sdk';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useSetAtom } from 'jotai';
import { roomIdToTypingMembersAtom } from '../../state/typingMembers';
import { TypingIndicator } from '../../components/typing-indicator';
@ -16,6 +17,7 @@ export type RoomViewTypingProps = {
};
export const RoomViewTyping = as<'div', RoomViewTypingProps>(
({ className, room, ...props }, ref) => {
const { t } = useTranslation();
const setTypingMembers = useSetAtom(roomIdToTypingMembersAtom);
const mx = useMatrixClient();
const typingMembers = useRoomTypingMember(room.roomId);
@ -58,7 +60,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
<>
<b>{typingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' is typing...'}
{t('Room.is_typing')}
</Text>
</>
)}
@ -66,11 +68,11 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
<>
<b>{typingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
{t('Room.and')}
</Text>
<b>{typingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' are typing...'}
{t('Room.are_typing')}
</Text>
</>
)}
@ -82,11 +84,11 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
</Text>
<b>{typingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
{t('Room.and')}
</Text>
<b>{typingNames[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' are typing...'}
{t('Room.are_typing')}
</Text>
</>
)}
@ -102,16 +104,16 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
</Text>
<b>{typingNames[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
{t('Room.and')}
</Text>
<b>{typingNames.length - 3} others</b>
<b>{t('Room.others_count', { count: typingNames.length - 3 })}</b>
<Text as="span" size="Inherit" priority="300">
{' are typing...'}
{t('Room.are_typing')}
</Text>
</>
)}
</Text>
<IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
<IconButton title={t('Room.drop_typing')} size="300" radii="Pill" onClick={handleDropAll}>
<Icon size="50" src={Icons.Cross} />
</IconButton>
</Box>

View file

@ -20,6 +20,7 @@ import {
RectCords,
} from 'folds';
import { Direction, 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';
@ -37,6 +38,7 @@ type JumpToTimeProps = {
onSubmit: (eventId: string) => void;
};
export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const room = useRoom();
const alive = useAlive();
@ -106,7 +108,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
size="500"
>
<Box grow="Yes">
<Text size="H4">Jump to Time</Text>
<Text size="H4">{t('Room.jump_to_time')}</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300">
<Icon src={Icons.Cross} />
@ -116,7 +118,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
<Box direction="Row" gap="300">
<Box direction="Column" gap="100">
<Text size="L400" priority="400">
Time
{t('Room.time_label')}
</Text>
<Box gap="100" alignItems="Center">
<Chip
@ -157,7 +159,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
</Box>
<Box direction="Column" gap="100">
<Text size="L400" priority="400">
Date
{t('Room.date_label')}
</Text>
<Box gap="100" alignItems="Center">
<Chip
@ -198,7 +200,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
</Box>
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Preset</Text>
<Text size="L400">{t('Room.preset')}</Text>
<Box gap="200">
{createTs < todayTs && (
<Chip
@ -207,7 +209,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
aria-pressed={ts === todayTs}
onClick={handleToday}
>
<Text size="B300">Today</Text>
<Text size="B300">{t('Room.today')}</Text>
</Chip>
)}
{createTs < yesterdayTs && (
@ -217,7 +219,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
aria-pressed={ts === yesterdayTs}
onClick={handleYesterday}
>
<Text size="B300">Yesterday</Text>
<Text size="B300">{t('Room.yesterday')}</Text>
</Chip>
)}
<Chip
@ -226,7 +228,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
aria-pressed={ts === createTs}
onClick={handleBeginning}
>
<Text size="B300">Beginning</Text>
<Text size="B300">{t('Room.beginning')}</Text>
</Chip>
</Box>
</Box>
@ -249,7 +251,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
}
onClick={handleSubmit}
>
<Text size="B400">Open Timeline</Text>
<Text size="B400">{t('Room.open_timeline')}</Text>
</Button>
</Box>
</Dialog>

View file

@ -31,6 +31,7 @@ import React, {
useState,
} from 'react';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { useHover, useFocusWithin } from 'react-aria';
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { Relations } from 'matrix-js-sdk/lib/models/relations';
@ -53,9 +54,7 @@ import {
getMemberDisplayName,
} from '../../../utils/room';
import {
getCanonicalAliasOrRoomId,
getMxIdLocalPart,
isRoomAlias,
mxcUrlToHttp,
} from '../../../utils/matrix';
import { MessageLayout, MessageSpacing } from '../../../state/settings';
@ -130,6 +129,7 @@ export const MessageAllReactionItem = as<
onClose?: () => void;
}
>(({ room, relations, onClose, ...props }, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const handleClose = () => {
@ -176,7 +176,7 @@ export const MessageAllReactionItem = as<
aria-pressed={open}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
View Reactions
{t('Room.view_reactions')}
</Text>
</MenuItem>
</>
@ -191,6 +191,7 @@ export const MessageReadReceiptItem = as<
onClose?: () => void;
}
>(({ room, eventId, onClose, ...props }, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const handleClose = () => {
@ -226,7 +227,7 @@ export const MessageReadReceiptItem = as<
aria-pressed={open}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Read Receipts
{t('Room.read_receipts')}
</Text>
</MenuItem>
</>
@ -241,6 +242,7 @@ export const MessageSourceCodeItem = as<
onClose?: () => void;
}
>(({ room, mEvent, onClose, ...props }, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const getContent = (evt: MatrixEvent) =>
@ -290,7 +292,7 @@ export const MessageSourceCodeItem = as<
>
<Modal variant="Surface" size="500">
<TextViewer
name="Source Code"
name={t('Room.source_code')}
langName="json"
text={getText()}
requestClose={handleClose}
@ -309,7 +311,7 @@ export const MessageSourceCodeItem = as<
aria-pressed={open}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
View Source
{t('Room.view_source')}
</Text>
</MenuItem>
</>
@ -324,7 +326,7 @@ export const MessageCopyLinkItem = as<
onClose?: () => void;
}
>(({ room, mEvent, onClose, ...props }, ref) => {
const mx = useMatrixClient();
const { t } = useTranslation();
const handleCopy = () => {
const eventId = mEvent.getId();
@ -343,7 +345,7 @@ export const MessageCopyLinkItem = as<
ref={ref}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Copy Link
{t('Room.copy_link')}
</Text>
</MenuItem>
);
@ -357,6 +359,7 @@ export const MessagePinItem = as<
onClose?: () => void;
}
>(({ room, mEvent, onClose, ...props }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const pinnedEvents = useRoomPinnedEvents(room);
const isPinned = pinnedEvents.includes(mEvent.getId() ?? '');
@ -383,7 +386,7 @@ export const MessagePinItem = as<
ref={ref}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
{isPinned ? 'Unpin Message' : 'Pin Message'}
{isPinned ? t('Room.unpin_message') : t('Room.pin_message')}
</Text>
</MenuItem>
);
@ -397,6 +400,7 @@ export const MessageDeleteItem = as<
onClose?: () => void;
}
>(({ room, mEvent, onClose, ...props }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const [open, setOpen] = useState(false);
@ -450,7 +454,7 @@ export const MessageDeleteItem = as<
size="500"
>
<Box grow="Yes">
<Text size="H4">Delete Message</Text>
<Text size="H4">{t('Room.delete_message')}</Text>
</Box>
<IconButton size="300" onClick={handleClose} radii="300">
<Icon src={Icons.Cross} />
@ -464,19 +468,19 @@ export const MessageDeleteItem = as<
gap="400"
>
<Text priority="400">
This action is irreversible! Are you sure that you want to delete this message?
{t('Room.delete_confirm')}
</Text>
<Box direction="Column" gap="100">
<Text size="L400">
Reason{' '}
{t('Room.reason')}{' '}
<Text as="span" size="T200">
(optional)
({t('Room.optional')})
</Text>
</Text>
<Input name="reasonInput" variant="Background" />
{deleteState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to delete message! Please try again.
{t('Room.delete_error')}
</Text>
)}
</Box>
@ -491,7 +495,7 @@ export const MessageDeleteItem = as<
aria-disabled={deleteState.status === AsyncStatus.Loading}
>
<Text size="B400">
{deleteState.status === AsyncStatus.Loading ? 'Deleting...' : 'Delete'}
{deleteState.status === AsyncStatus.Loading ? t('Room.deleting') : t('Room.delete')}
</Text>
</Button>
</Box>
@ -511,7 +515,7 @@ export const MessageDeleteItem = as<
ref={ref}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Delete
{t('Room.delete')}
</Text>
</Button>
</>
@ -526,6 +530,7 @@ export const MessageReportItem = as<
onClose?: () => void;
}
>(({ room, mEvent, onClose, ...props }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const [open, setOpen] = useState(false);
@ -550,7 +555,7 @@ export const MessageReportItem = as<
const reasonInput = target?.reasonInput as HTMLInputElement | undefined;
const reason = reasonInput && reasonInput.value.trim();
if (reasonInput) reasonInput.value = '';
reportMessage(eventId, reason ? -100 : -50, reason || 'No reason provided');
reportMessage(eventId, reason ? -100 : -50, reason || t('Room.no_reason'));
};
const handleClose = () => {
@ -580,7 +585,7 @@ export const MessageReportItem = as<
size="500"
>
<Box grow="Yes">
<Text size="H4">Report Message</Text>
<Text size="H4">{t('Room.report_message')}</Text>
</Box>
<IconButton size="300" onClick={handleClose} radii="300">
<Icon src={Icons.Cross} />
@ -594,20 +599,19 @@ export const MessageReportItem = as<
gap="400"
>
<Text priority="400">
Report this message to server, which may then notify the appropriate people to
take action.
{t('Room.report_desc')}
</Text>
<Box direction="Column" gap="100">
<Text size="L400">Reason</Text>
<Text size="L400">{t('Room.report_reason')}</Text>
<Input name="reasonInput" variant="Background" required />
{reportState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to report message! Please try again.
{t('Room.report_error')}
</Text>
)}
{reportState.status === AsyncStatus.Success && (
<Text style={{ color: color.Success.Main }} size="T300">
Message has been reported to server.
{t('Room.report_success')}
</Text>
)}
</Box>
@ -625,7 +629,7 @@ export const MessageReportItem = as<
}
>
<Text size="B400">
{reportState.status === AsyncStatus.Loading ? 'Reporting...' : 'Report'}
{reportState.status === AsyncStatus.Loading ? t('Room.reporting') : t('Room.report')}
</Text>
</Button>
</Box>
@ -645,7 +649,7 @@ export const MessageReportItem = as<
ref={ref}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Report
{t('Room.report')}
</Text>
</Button>
</>
@ -718,6 +722,7 @@ export const Message = as<'div', MessageProps>(
},
ref
) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const senderId = mEvent.getSender() ?? '';
@ -998,7 +1003,7 @@ export const Message = as<'div', MessageProps>(
size="T300"
truncate
>
Add Reaction
{t('Room.add_reaction')}
</Text>
</MenuItem>
)}
@ -1025,7 +1030,7 @@ export const Message = as<'div', MessageProps>(
size="T300"
truncate
>
Reply
{t('Room.reply')}
</Text>
</MenuItem>
{!isThreadedMessage && (
@ -1045,7 +1050,7 @@ export const Message = as<'div', MessageProps>(
size="T300"
truncate
>
Reply in Thread
{t('Room.reply_in_thread')}
</Text>
</MenuItem>
)}
@ -1066,7 +1071,7 @@ export const Message = as<'div', MessageProps>(
size="T300"
truncate
>
Edit Message
{t('Room.edit_message')}
</Text>
</MenuItem>
)}

View file

@ -2,6 +2,7 @@
import React, { forwardRef, MouseEventHandler, useCallback, useMemo, useRef } from 'react';
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
import { useTranslation } from 'react-i18next';
import {
Avatar,
Box,
@ -111,6 +112,7 @@ function PinnedMessage({
hour24Clock,
dateFormatString,
}: PinnedMessageProps) {
const { t } = useTranslation();
const pinnedEvent = useRoomEvent(room, eventId);
const useAuthentication = useMediaAuthentication();
const mx = useMatrixClient();
@ -142,7 +144,7 @@ function PinnedMessage({
const renderOptions = () => (
<Box shrink="No" gap="200" alignItems="Center">
<Chip data-event-id={eventId} onClick={handleOpenClick} variant="Secondary" radii="Pill">
<Text size="T200">Open</Text>
<Text size="T200">{t('Room.open')}</Text>
</Chip>
{canPinEvent && (
<IconButton
@ -168,7 +170,7 @@ function PinnedMessage({
return (
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center">
<Box>
<Text style={{ color: color.Critical.Main }}>Failed to load message!</Text>
<Text style={{ color: color.Critical.Main }}>{t('Room.failed_to_load')}</Text>
</Box>
{renderOptions()}
</Box>
@ -249,6 +251,7 @@ type RoomPinMenuProps = {
};
export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
({ room, requestClose }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const powerLevels = usePowerLevelsContext();
@ -454,7 +457,7 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
<Box grow="Yes" direction="Column">
<Header className={css.PinMenuHeader} size="500">
<Box grow="Yes">
<Text size="H5">Pinned Messages</Text>
<Text size="H5">{t('Room.pinned_messages')}</Text>
</Box>
<Box shrink="No">
<IconButton size="300" onClick={requestClose} radii="300">
@ -527,10 +530,10 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
alignItems="Center"
>
<Text size="H4" align="Center">
No Pinned Messages
{t('Room.no_pinned_messages')}
</Text>
<Text size="T400" align="Center">
Users with sufficient power level can pin a messages from its context menu.
{t('Room.no_pinned_messages_desc')}
</Text>
</Box>
</Box>

View file

@ -1,4 +1,5 @@
import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAtom, useAtomValue } from 'jotai';
import {
Avatar,
@ -56,6 +57,7 @@ type DirectMenuProps = {
requestClose: () => void;
};
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const orphanRooms = useDirectRooms();
@ -78,7 +80,7 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
aria-disabled={!unread}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Mark as Read
{t('Direct.mark_as_read')}
</Text>
</MenuItem>
</Box>
@ -87,6 +89,7 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
});
function DirectHeader() {
const { t } = useTranslation();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
@ -103,7 +106,7 @@ function DirectHeader() {
<Box alignItems="Center" grow="Yes" gap="300">
<Box grow="Yes">
<Text size="H4" truncate>
Direct Messages
{t('Direct.direct_messages')}
</Text>
</Box>
<Box>
@ -139,6 +142,7 @@ function DirectHeader() {
}
function DirectEmpty() {
const { t } = useTranslation();
const navigate = useNavigate();
return (
@ -147,18 +151,18 @@ function DirectEmpty() {
icon={<Icon size="600" src={Icons.Mention} />}
title={
<Text size="H5" align="Center">
No Direct Messages
{t('Direct.no_direct_messages')}
</Text>
}
content={
<Text size="T300" align="Center">
You do not have any direct messages yet.
{t('Direct.no_direct_messages_desc')}
</Text>
}
options={
<Button variant="Secondary" size="300" onClick={() => navigate(getDirectCreatePath())}>
<Text size="B300" truncate>
Direct Message
{t('Direct.direct_message')}
</Text>
</Button>
}
@ -169,6 +173,7 @@ function DirectEmpty() {
const DEFAULT_CATEGORY_ID = makeNavCategoryId('direct', 'direct');
export function Direct() {
const { t } = useTranslation();
const mx = useMatrixClient();
useNavToActivePathMapper('direct');
const scrollRef = useRef<HTMLDivElement>(null);
@ -220,7 +225,7 @@ export function Direct() {
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Create Chat
{t('Direct.create_chat')}
</Text>
</Box>
</Box>
@ -235,7 +240,7 @@ export function Direct() {
data-category-id={DEFAULT_CATEGORY_ID}
onClick={handleCategoryClick}
>
Chats
{t('Direct.chats')}
</RoomNavCategoryButton>
</NavCategoryHeader>
<div

View file

@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Box, Icon, IconButton, Icons, Scroll } from 'folds';
import { useTranslation } from 'react-i18next';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getDirectCreateSearchParams } from '../../pathSearchParam';
import { getDirectRoomPath } from '../../pathUtils';
@ -19,6 +20,7 @@ import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { CreateChat } from '../../../features/create-chat';
export function DirectCreate() {
const { t } = useTranslation();
const mx = useMatrixClient();
const screenSize = useScreenSizeContext();
@ -60,8 +62,8 @@ export function DirectCreate() {
<Box direction="Column" gap="700">
<PageHero
icon={<Icon size="600" src={Icons.Mention} />}
title="Create Chat"
subTitle="Start a private, encrypted chat by entering a user ID."
title={t('Direct.create_chat')}
subTitle={t('Direct.create_chat_subtitle')}
/>
<CreateChat defaultUserId={userId} />
</Box>

View file

@ -1,6 +1,7 @@
import React, { MouseEventHandler, forwardRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds';
import { useTranslation } from 'react-i18next';
import FocusTrap from 'focus-trap-react';
import { useAtomValue } from 'jotai';
import { useDirects } from '../../../state/hooks/roomList';
@ -30,6 +31,7 @@ type DirectMenuProps = {
requestClose: () => void;
};
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }, ref) => {
const { t } = useTranslation();
const orphanRooms = useDirectRooms();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
@ -52,7 +54,7 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
aria-disabled={!unread}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Mark as Read
{t('Direct.mark_as_read')}
</Text>
</MenuItem>
</Box>
@ -61,6 +63,7 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
});
export function DirectTab() {
const { t } = useTranslation();
const navigate = useNavigate();
const mx = useMatrixClient();
const screenSize = useScreenSizeContext();
@ -93,7 +96,7 @@ export function DirectTab() {
};
return (
<SidebarItem active={directSelected}>
<SidebarItemTooltip tooltip="Direct Messages">
<SidebarItemTooltip tooltip={t('Direct.direct_messages')}>
{(triggerRef) => (
<SidebarAvatar
as="button"