localize direct messages
This commit is contained in:
parent
fc8346e50d
commit
77fda29820
17 changed files with 429 additions and 159 deletions
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": " (изменено)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
({ 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>This message has been deleted. {reason}</i>
|
||||
<i>{t('Room.message_deleted_reason', { reason })}</i>
|
||||
) : (
|
||||
<i>This message has been deleted</i>
|
||||
<i>{t('Room.message_deleted')}</i>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const MessageUnsupportedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
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>Unsupported message</i>
|
||||
<i>{t('Room.unsupported_message')}</i>
|
||||
</Box>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const MessageFailedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
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>Failed to load message</i>
|
||||
<i>{t('Room.failed_to_load_message')}</i>
|
||||
</Box>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const MessageBadEncryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
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>Unable to decrypt message</i>
|
||||
<i>{t('Room.unable_to_decrypt')}</i>
|
||||
</Box>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const MessageNotDecryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
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>This message is not decrypted yet</i>
|
||||
<i>{t('Room.not_decrypted_yet')}</i>
|
||||
</Box>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const MessageBrokenContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
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>Broken message</i>
|
||||
<i>{t('Room.broken_message')}</i>
|
||||
</Box>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const MessageEmptyContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
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>Empty message</i>
|
||||
<i>{t('Room.empty_message')}</i>
|
||||
</Box>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const MessageEditedContent = as<'span', { children?: never }>(({ ...props }, ref) => (
|
||||
export const MessageEditedContent = as<'span', { children?: never }>(({ ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Text as="span" size="T200" priority="300" {...props} ref={ref}>
|
||||
{' (edited)'}
|
||||
{t('Room.edited')}
|
||||
</Text>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
? t('Direct.rate_limited', {
|
||||
minutes: millisecondsToMinutes(
|
||||
(error.data.retry_after_ms as number | undefined) ?? 0
|
||||
)} minutes!`
|
||||
),
|
||||
})
|
||||
: 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue