Localize room intro, membership events, call controls, and leave-room dialog across en/ru via i18next.
This commit is contained in:
parent
8463180c04
commit
f96c80f829
9 changed files with 246 additions and 90 deletions
|
|
@ -577,3 +577,16 @@ App foreground, incoming DM call. JS strip + audio начинают отраба
|
||||||
- **Capacitor sync risk:** confirmed safe — `cap sync` регенерит только `capacitor.build.gradle`, не трогает `build.gradle`.
|
- **Capacitor sync risk:** confirmed safe — `cap sync` регенерит только `capacitor.build.gradle`, не трогает `build.gradle`.
|
||||||
- **Связь:** follow-up §5.27 (landed 2026-04-24).
|
- **Связь:** follow-up §5.27 (landed 2026-04-24).
|
||||||
|
|
||||||
|
### 5.50. 🟡 LeaveRoomPrompt / LeaveSpacePrompt prematurely report success
|
||||||
|
|
||||||
|
- **Что:** [LeaveRoomPrompt.tsx](../../src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx) и [LeaveSpacePrompt.tsx](../../src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx) вызывают `mx.leave(roomId)` внутри `async` callback без `await`/`return`. `useAsyncCallback` ждёт callback, а не внутренний network promise, поэтому состояние становится `Success` сразу после запуска запроса.
|
||||||
|
- **Симптом:** prompt закрывается через `onDone()` до ответа homeserver. Если `POST /rooms/{roomId}/leave` падает, ошибка не попадает в `leaveState`, `Failed to leave room/space` не показывается, возможен unhandled promise rejection, user остаётся в комнате после refresh/sync.
|
||||||
|
- **Доказательство:** `matrix-js-sdk` `leave(roomId)` возвращает `Promise<EmptyObject>` и внутри делает `authedRequest(POST /rooms/$room_id/leave)`. Текущий callback возвращает `Promise<undefined>`, который resolve'ится сразу.
|
||||||
|
- **Live reproduce:**
|
||||||
|
1. `npm run dev`, залогиниться и открыть joined room.
|
||||||
|
2. DevTools → Network throttling → `Offline` (или block request URL `*rooms/*/leave*`).
|
||||||
|
3. Room menu → `Leave Room` → confirm.
|
||||||
|
4. Фактический pre-fix результат: prompt закрывается как success, error text не появляется; в Network `POST .../rooms/{roomId}/leave` failed/blocked; после возврата online + refresh/sync комната остаётся.
|
||||||
|
5. Ожидаемый результат после фикса: prompt остаётся loading до ответа, затем показывает localized error.
|
||||||
|
- **Фикс-направление:** `await mx.leave(roomId);` в обоих prompt'ах. Не `return mx.leave(roomId)`, если сохраняем `useAsyncCallback<undefined, MatrixError, []>` contract.
|
||||||
|
- **Scope:** не связано с i18n-диффом; pre-existing bug, найден при ревью локализации leave prompt'а.
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,18 @@
|
||||||
"incoming": "Incoming call…",
|
"incoming": "Incoming call…",
|
||||||
"answer": "Answer",
|
"answer": "Answer",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"unknown_caller": "Unknown caller"
|
"unknown_caller": "Unknown caller",
|
||||||
|
"mic_off": "Turn Off Microphone",
|
||||||
|
"mic_on": "Turn On Microphone",
|
||||||
|
"sound_off": "Turn Off Sound",
|
||||||
|
"sound_on": "Turn On Sound",
|
||||||
|
"camera_off": "Stop Camera",
|
||||||
|
"camera_on": "Start Camera",
|
||||||
|
"screenshare_off": "Stop Screenshare",
|
||||||
|
"screenshare_on": "Start Screenshare",
|
||||||
|
"chat_close": "Close Chat",
|
||||||
|
"chat_open": "Open Chat",
|
||||||
|
"end_call": "End"
|
||||||
},
|
},
|
||||||
"Room": {
|
"Room": {
|
||||||
"new_messages": "New Messages",
|
"new_messages": "New Messages",
|
||||||
|
|
@ -476,7 +487,40 @@
|
||||||
"not_decrypted_yet": "This message is not decrypted yet",
|
"not_decrypted_yet": "This message is not decrypted yet",
|
||||||
"broken_message": "Broken message",
|
"broken_message": "Broken message",
|
||||||
"empty_message": "Empty message",
|
"empty_message": "Empty message",
|
||||||
"edited": " (edited)"
|
"edited": " (edited)",
|
||||||
|
|
||||||
|
"conversation_beginning": "This is the beginning of conversation.",
|
||||||
|
"created_by": "Created by <bold>@{{creator}}</bold> on {{date}} {{time}}",
|
||||||
|
"invite_member": "Invite Member",
|
||||||
|
"open_old_room": "Open Old Room",
|
||||||
|
"join_old_room": "Join Old Room",
|
||||||
|
"leave_room_title": "Leave Room",
|
||||||
|
"leave_room_confirm": "Are you sure you want to leave this room?",
|
||||||
|
"leave_room_error": "Failed to leave room! {{error}}",
|
||||||
|
"leaving": "Leaving...",
|
||||||
|
"leave": "Leave",
|
||||||
|
|
||||||
|
"member_broken": "Broken membership event",
|
||||||
|
"member_accepted_knock": "<bold>{{sender}}</bold> accepted <bold>{{user}}</bold>'s join request",
|
||||||
|
"member_invited": "<bold>{{sender}}</bold> invited <bold>{{user}}</bold>",
|
||||||
|
"member_knock": "<bold>{{user}}</bold> requested to join room",
|
||||||
|
"member_joined": "<bold>{{user}}</bold> joined the room",
|
||||||
|
"member_rejected_invite": "<bold>{{user}}</bold> rejected the invitation",
|
||||||
|
"member_rejected_knock": "<bold>{{sender}}</bold> rejected <bold>{{user}}</bold>'s join request",
|
||||||
|
"member_revoked_knock": "<bold>{{user}}</bold> revoked join request",
|
||||||
|
"member_revoked_invite": "<bold>{{sender}}</bold> revoked <bold>{{user}}</bold>'s invite",
|
||||||
|
"member_unbanned": "<bold>{{sender}}</bold> unbanned <bold>{{user}}</bold>",
|
||||||
|
"member_left": "<bold>{{user}}</bold> left the room",
|
||||||
|
"member_kicked": "<bold>{{sender}}</bold> kicked <bold>{{user}}</bold>",
|
||||||
|
"member_banned": "<bold>{{sender}}</bold> banned <bold>{{user}}</bold>",
|
||||||
|
"member_name_changed": "<bold>{{oldName}}</bold> changed display name to <bold>{{newName}}</bold>",
|
||||||
|
"member_name_removed": "<bold>{{user}}</bold> removed their display name",
|
||||||
|
"member_avatar_changed": "<bold>{{user}}</bold> changed their avatar",
|
||||||
|
"member_avatar_removed": "<bold>{{user}}</bold> removed their avatar",
|
||||||
|
"member_no_change": "Membership event with no changes",
|
||||||
|
|
||||||
|
"member_ended_call": "<bold>{{user}}</bold> ended the call",
|
||||||
|
"member_joined_call": "<bold>{{user}}</bold> joined the call"
|
||||||
},
|
},
|
||||||
"Inbox": {
|
"Inbox": {
|
||||||
"inbox": "Inbox",
|
"inbox": "Inbox",
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,18 @@
|
||||||
"incoming": "Входящий звонок…",
|
"incoming": "Входящий звонок…",
|
||||||
"answer": "Ответить",
|
"answer": "Ответить",
|
||||||
"decline": "Отклонить",
|
"decline": "Отклонить",
|
||||||
"unknown_caller": "Неизвестный абонент"
|
"unknown_caller": "Неизвестный абонент",
|
||||||
|
"mic_off": "Выключить микрофон",
|
||||||
|
"mic_on": "Включить микрофон",
|
||||||
|
"sound_off": "Выключить звук",
|
||||||
|
"sound_on": "Включить звук",
|
||||||
|
"camera_off": "Выключить камеру",
|
||||||
|
"camera_on": "Включить камеру",
|
||||||
|
"screenshare_off": "Остановить показ экрана",
|
||||||
|
"screenshare_on": "Начать показ экрана",
|
||||||
|
"chat_close": "Закрыть чат",
|
||||||
|
"chat_open": "Открыть чат",
|
||||||
|
"end_call": "Завершить"
|
||||||
},
|
},
|
||||||
"Room": {
|
"Room": {
|
||||||
"new_messages": "Новые сообщения",
|
"new_messages": "Новые сообщения",
|
||||||
|
|
@ -476,7 +487,40 @@
|
||||||
"not_decrypted_yet": "Сообщение ещё не расшифровано",
|
"not_decrypted_yet": "Сообщение ещё не расшифровано",
|
||||||
"broken_message": "Повреждённое сообщение",
|
"broken_message": "Повреждённое сообщение",
|
||||||
"empty_message": "Пустое сообщение",
|
"empty_message": "Пустое сообщение",
|
||||||
"edited": " (изменено)"
|
"edited": " (изменено)",
|
||||||
|
|
||||||
|
"conversation_beginning": "Начало переписки.",
|
||||||
|
"created_by": "Комната создана <bold>@{{creator}}</bold> {{date}} {{time}}",
|
||||||
|
"invite_member": "Пригласить",
|
||||||
|
"open_old_room": "Открыть старую комнату",
|
||||||
|
"join_old_room": "Войти в старую комнату",
|
||||||
|
"leave_room_title": "Покинуть комнату",
|
||||||
|
"leave_room_confirm": "Покинуть эту комнату?",
|
||||||
|
"leave_room_error": "Не удалось покинуть комнату! {{error}}",
|
||||||
|
"leaving": "Выход...",
|
||||||
|
"leave": "Покинуть",
|
||||||
|
|
||||||
|
"member_broken": "Некорректное событие участия",
|
||||||
|
"member_accepted_knock": "<bold>{{sender}}</bold> одобряет вступление <bold>{{user}}</bold>",
|
||||||
|
"member_invited": "<bold>{{sender}}</bold> приглашает <bold>{{user}}</bold>",
|
||||||
|
"member_knock": "<bold>{{user}}</bold> просит вступить в комнату",
|
||||||
|
"member_joined": "<bold>{{user}}</bold> теперь в комнате",
|
||||||
|
"member_rejected_invite": "<bold>{{user}}</bold> отклоняет приглашение",
|
||||||
|
"member_rejected_knock": "<bold>{{sender}}</bold> отклоняет запрос <bold>{{user}}</bold>",
|
||||||
|
"member_revoked_knock": "<bold>{{user}}</bold> отзывает запрос на вступление",
|
||||||
|
"member_revoked_invite": "<bold>{{sender}}</bold> отзывает приглашение <bold>{{user}}</bold>",
|
||||||
|
"member_unbanned": "<bold>{{sender}}</bold> разблокирует <bold>{{user}}</bold>",
|
||||||
|
"member_left": "<bold>{{user}}</bold> больше не в комнате",
|
||||||
|
"member_kicked": "<bold>{{sender}}</bold> исключает <bold>{{user}}</bold>",
|
||||||
|
"member_banned": "<bold>{{sender}}</bold> блокирует <bold>{{user}}</bold>",
|
||||||
|
"member_name_changed": "<bold>{{oldName}}</bold> теперь <bold>{{newName}}</bold>",
|
||||||
|
"member_name_removed": "<bold>{{user}}</bold> убирает отображаемое имя",
|
||||||
|
"member_avatar_changed": "<bold>{{user}}</bold> меняет аватар",
|
||||||
|
"member_avatar_removed": "<bold>{{user}}</bold> убирает аватар",
|
||||||
|
"member_no_change": "Событие участия без изменений",
|
||||||
|
|
||||||
|
"member_ended_call": "<bold>{{user}}</bold> больше не в звонке",
|
||||||
|
"member_joined_call": "<bold>{{user}}</bold> теперь в звонке"
|
||||||
},
|
},
|
||||||
"Inbox": {
|
"Inbox": {
|
||||||
"inbox": "Входящие",
|
"inbox": "Входящие",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { MatrixError } from 'matrix-js-sdk';
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
@ -27,6 +28,7 @@ type LeaveRoomPromptProps = {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptProps) {
|
export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
|
const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
|
||||||
|
|
@ -66,7 +68,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
|
||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Leave Room</Text>
|
<Text size="H4">{t('Room.leave_room_title')}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300">
|
<IconButton size="300" onClick={onCancel} radii="300">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|
@ -74,10 +76,10 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
|
||||||
</Header>
|
</Header>
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Text priority="400">Are you sure you want to leave this room?</Text>
|
<Text priority="400">{t('Room.leave_room_confirm')}</Text>
|
||||||
{leaveState.status === AsyncStatus.Error && (
|
{leaveState.status === AsyncStatus.Error && (
|
||||||
<Text style={{ color: color.Critical.Main }} size="T300">
|
<Text style={{ color: color.Critical.Main }} size="T300">
|
||||||
Failed to leave room! {leaveState.error.message}
|
{t('Room.leave_room_error', { error: leaveState.error.message })}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -96,7 +98,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text size="B400">
|
<Text size="B400">
|
||||||
{leaveState.status === AsyncStatus.Loading ? 'Leaving...' : 'Leave'}
|
{leaveState.status === AsyncStatus.Loading ? t('Room.leaving') : t('Room.leave')}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
|
||||||
import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
|
import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
|
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
|
||||||
import { getMemberDisplayName, getStateEvent } from '../../utils/room';
|
import { getMemberDisplayName, getStateEvent } from '../../utils/room';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
@ -23,6 +24,7 @@ export type RoomIntroProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {
|
export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
@ -66,19 +68,21 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T400" priority="400">
|
<Text size="T400" priority="400">
|
||||||
{typeof topic === 'string' ? topic : 'This is the beginning of conversation.'}
|
{typeof topic === 'string' ? topic : t('Room.conversation_beginning')}
|
||||||
</Text>
|
</Text>
|
||||||
{creatorName && ts && (
|
{creatorName && ts && (
|
||||||
<Text size="T200" priority="300">
|
<Text size="T200" priority="300">
|
||||||
{'Created by '}
|
<Trans
|
||||||
<b>@{creatorName}</b>
|
i18nKey="Room.created_by"
|
||||||
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
|
values={{ creator: creatorName, date: timeDayMonthYear(ts), time: timeHourMinute(ts, hour24Clock) }}
|
||||||
|
components={{ bold: <b /> }}
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box gap="200" wrap="Wrap">
|
<Box gap="200" wrap="Wrap">
|
||||||
<Button onClick={() => setInvitePrompt(true)} variant="Secondary" size="300" radii="300">
|
<Button onClick={() => setInvitePrompt(true)} variant="Secondary" size="300" radii="300">
|
||||||
<Text size="B300">Invite Member</Text>
|
<Text size="B300">{t('Room.invite_member')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{invitePrompt && (
|
{invitePrompt && (
|
||||||
|
|
@ -93,7 +97,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
<Text size="B300">Open Old Room</Text>
|
<Text size="B300">{t('Room.open_old_room')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -109,7 +113,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text size="B300">Join Old Room</Text>
|
<Text size="B300">{t('Room.join_old_room')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
|
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSetAtom, useStore } from 'jotai';
|
import { useSetAtom, useStore } from 'jotai';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { StatusDivider } from './components';
|
import { StatusDivider } from './components';
|
||||||
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
|
@ -20,12 +21,13 @@ type MicrophoneButtonProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
|
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
|
<Text size="T200">{enabled ? t('Call.mic_off') : t('Call.mic_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -53,12 +55,13 @@ type SoundButtonProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
|
function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
|
<Text size="T200">{enabled ? t('Call.sound_off') : t('Call.sound_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -90,12 +93,13 @@ type VideoButtonProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
|
<Text size="T200">{enabled ? t('Call.camera_off') : t('Call.camera_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -127,12 +131,13 @@ type ScreenShareButtonProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
|
function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
|
<Text size="T200">{enabled ? t('Call.screenshare_off') : t('Call.screenshare_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -163,6 +168,7 @@ export function CallControl({
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
callJoined: boolean;
|
callJoined: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
||||||
const setCallEmbed = useSetAtom(callEmbedAtom);
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
@ -250,7 +256,7 @@ export function CallControl({
|
||||||
>
|
>
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<Text as="span" size="L400">
|
<Text as="span" size="L400">
|
||||||
End
|
{t('Call.end_call')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
|
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
import { callChatAtom } from '../../state/callEmbed';
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
|
|
||||||
|
|
@ -15,13 +16,14 @@ type MicrophoneButtonProps = {
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
};
|
};
|
||||||
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
|
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
delay={500}
|
delay={500}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
|
<Text size="T200">{enabled ? t('Call.mic_off') : t('Call.mic_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -47,13 +49,14 @@ type SoundButtonProps = {
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
};
|
};
|
||||||
export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
|
export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
delay={500}
|
delay={500}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
|
<Text size="T200">{enabled ? t('Call.sound_off') : t('Call.sound_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -83,13 +86,14 @@ type VideoButtonProps = {
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
};
|
};
|
||||||
export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
|
export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
delay={500}
|
delay={500}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
|
<Text size="T200">{enabled ? t('Call.camera_off') : t('Call.camera_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -119,13 +123,14 @@ type ScreenShareButtonProps = {
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
};
|
};
|
||||||
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
|
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
delay={500}
|
delay={500}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
|
<Text size="T200">{enabled ? t('Call.screenshare_off') : t('Call.screenshare_on')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -147,6 +152,7 @@ export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatButton() {
|
export function ChatButton() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [chat, setChat] = useAtom(callChatAtom);
|
const [chat, setChat] = useAtom(callChatAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -155,7 +161,7 @@ export function ChatButton() {
|
||||||
delay={500}
|
delay={500}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text size="T200">{chat ? 'Close Chat' : 'Open Chat'}</Text>
|
<Text size="T200">{chat ? t('Call.chat_close') : t('Call.chat_open')}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ import {
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../utils/matrix';
|
import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../utils/matrix';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
|
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
|
||||||
|
|
@ -1534,8 +1534,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
<Text size="T300" priority="300">
|
<Text size="T300" priority="300">
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{callJoined ? ' joined the call' : ' ended the call'}
|
i18nKey={callJoined ? 'Room.member_joined_call' : 'Room.member_ended_call'}
|
||||||
|
values={{ user: senderName }}
|
||||||
|
components={{ bold: <b /> }}
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { IconSrc, Icons } from 'folds';
|
import { IconSrc, Icons } from 'folds';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk';
|
import { MatrixEvent } from 'matrix-js-sdk';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
import { IMemberContent, Membership } from '../../types/matrix/room';
|
import { IMemberContent, Membership } from '../../types/matrix/room';
|
||||||
import { getMxIdLocalPart } from '../utils/matrix';
|
import { getMxIdLocalPart } from '../utils/matrix';
|
||||||
import { isMembershipChanged } from '../utils/room';
|
import { isMembershipChanged } from '../utils/room';
|
||||||
|
|
@ -12,6 +13,8 @@ export type ParsedResult = {
|
||||||
|
|
||||||
export type MemberEventParser = (mEvent: MatrixEvent) => ParsedResult;
|
export type MemberEventParser = (mEvent: MatrixEvent) => ParsedResult;
|
||||||
|
|
||||||
|
const B = <b />;
|
||||||
|
|
||||||
export const useMemberEventParser = (): MemberEventParser => {
|
export const useMemberEventParser = (): MemberEventParser => {
|
||||||
const parseMemberEvent: MemberEventParser = (mEvent) => {
|
const parseMemberEvent: MemberEventParser = (mEvent) => {
|
||||||
const content = mEvent.getContent<IMemberContent>();
|
const content = mEvent.getContent<IMemberContent>();
|
||||||
|
|
@ -23,7 +26,7 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
if (!senderId || !userId)
|
if (!senderId || !userId)
|
||||||
return {
|
return {
|
||||||
icon: Icons.User,
|
icon: Icons.User,
|
||||||
body: 'Broken membership event',
|
body: <Trans i18nKey="Room.member_broken" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const senderName = getMxIdLocalPart(senderId);
|
const senderName = getMxIdLocalPart(senderId);
|
||||||
|
|
@ -39,11 +42,12 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
icon: Icons.ArrowGoRightPlus,
|
icon: Icons.ArrowGoRightPlus,
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{' accepted '}
|
i18nKey="Room.member_accepted_knock"
|
||||||
<b>{userName}</b>
|
values={{ sender: senderName, user: userName }}
|
||||||
{`'s join request `}
|
components={{ bold: B }}
|
||||||
{reason}
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -53,9 +57,12 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
icon: Icons.ArrowGoRightPlus,
|
icon: Icons.ArrowGoRightPlus,
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{' invited '}
|
i18nKey="Room.member_invited"
|
||||||
<b>{userName}</b> {reason}
|
values={{ sender: senderName, user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -66,9 +73,12 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
icon: Icons.ArrowGoRightPlus,
|
icon: Icons.ArrowGoRightPlus,
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
<b>{userName}</b>
|
<Trans
|
||||||
{' request to join room '}
|
i18nKey="Room.member_knock"
|
||||||
{reason}
|
values={{ user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -78,10 +88,11 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
return {
|
return {
|
||||||
icon: Icons.ArrowGoRight,
|
icon: Icons.ArrowGoRight,
|
||||||
body: (
|
body: (
|
||||||
<>
|
<Trans
|
||||||
<b>{userName}</b>
|
i18nKey="Room.member_joined"
|
||||||
{' joined the room'}
|
values={{ user: userName }}
|
||||||
</>
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -93,17 +104,21 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
body:
|
body:
|
||||||
senderId === userId ? (
|
senderId === userId ? (
|
||||||
<>
|
<>
|
||||||
<b>{userName}</b>
|
<Trans
|
||||||
{' rejected the invitation '}
|
i18nKey="Room.member_rejected_invite"
|
||||||
{reason}
|
values={{ user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{' rejected '}
|
i18nKey="Room.member_rejected_knock"
|
||||||
<b>{userName}</b>
|
values={{ sender: senderName, user: userName }}
|
||||||
{`'s join request `}
|
components={{ bold: B }}
|
||||||
{reason}
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -115,17 +130,21 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
body:
|
body:
|
||||||
senderId === userId ? (
|
senderId === userId ? (
|
||||||
<>
|
<>
|
||||||
<b>{userName}</b>
|
<Trans
|
||||||
{' revoked joined request '}
|
i18nKey="Room.member_revoked_knock"
|
||||||
{reason}
|
values={{ user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{' revoked '}
|
i18nKey="Room.member_revoked_invite"
|
||||||
<b>{userName}</b>
|
values={{ sender: senderName, user: userName }}
|
||||||
{`'s invite `}
|
components={{ bold: B }}
|
||||||
{reason}
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -136,9 +155,12 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
icon: Icons.ArrowGoLeft,
|
icon: Icons.ArrowGoLeft,
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{' unbanned '}
|
i18nKey="Room.member_unbanned"
|
||||||
<b>{userName}</b> {reason}
|
values={{ sender: senderName, user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -149,15 +171,21 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
body:
|
body:
|
||||||
senderId === userId ? (
|
senderId === userId ? (
|
||||||
<>
|
<>
|
||||||
<b>{userName}</b>
|
<Trans
|
||||||
{' left the room '}
|
i18nKey="Room.member_left"
|
||||||
{reason}
|
values={{ user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{' kicked '}
|
i18nKey="Room.member_kicked"
|
||||||
<b>{userName}</b> {reason}
|
values={{ sender: senderName, user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -168,9 +196,12 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
icon: Icons.ArrowGoLeft,
|
icon: Icons.ArrowGoLeft,
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
<b>{senderName}</b>
|
<Trans
|
||||||
{' banned '}
|
i18nKey="Room.member_banned"
|
||||||
<b>{userName}</b> {reason}
|
values={{ sender: senderName, user: userName }}
|
||||||
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
|
{reason ? ` ${reason}` : ''}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -187,16 +218,17 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
icon: Icons.Mention,
|
icon: Icons.Mention,
|
||||||
body:
|
body:
|
||||||
typeof content.displayname === 'string' ? (
|
typeof content.displayname === 'string' ? (
|
||||||
<>
|
<Trans
|
||||||
<b>{prevUserName}</b>
|
i18nKey="Room.member_name_changed"
|
||||||
{' changed display name to '}
|
values={{ oldName: prevUserName, newName: userName }}
|
||||||
<b>{userName}</b>
|
components={{ bold: B }}
|
||||||
</>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Trans
|
||||||
<b>{prevUserName}</b>
|
i18nKey="Room.member_name_removed"
|
||||||
{' removed their display name '}
|
values={{ user: prevUserName }}
|
||||||
</>
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -205,22 +237,24 @@ export const useMemberEventParser = (): MemberEventParser => {
|
||||||
icon: Icons.User,
|
icon: Icons.User,
|
||||||
body:
|
body:
|
||||||
content.avatar_url && typeof content.avatar_url === 'string' ? (
|
content.avatar_url && typeof content.avatar_url === 'string' ? (
|
||||||
<>
|
<Trans
|
||||||
<b>{userName}</b>
|
i18nKey="Room.member_avatar_changed"
|
||||||
{' changed their avatar'}
|
values={{ user: userName }}
|
||||||
</>
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Trans
|
||||||
<b>{userName}</b>
|
i18nKey="Room.member_avatar_removed"
|
||||||
{' removed their avatar '}
|
values={{ user: userName }}
|
||||||
</>
|
components={{ bold: B }}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icon: Icons.User,
|
icon: Icons.User,
|
||||||
body: 'Membership event with no changes',
|
body: <Trans i18nKey="Room.member_no_change" />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue