Localize room intro, membership events, call controls, and leave-room dialog across en/ru via i18next.

This commit is contained in:
heaven 2026-04-25 01:28:07 +03:00
parent 8463180c04
commit f96c80f829
9 changed files with 246 additions and 90 deletions

View file

@ -577,3 +577,16 @@ App foreground, incoming DM call. JS strip + audio начинают отраба
- **Capacitor sync risk:** confirmed safe — `cap sync` регенерит только `capacitor.build.gradle`, не трогает `build.gradle`.
- **Связь:** 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'а.

View file

@ -390,7 +390,18 @@
"incoming": "Incoming call…",
"answer": "Answer",
"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": {
"new_messages": "New Messages",
@ -476,7 +487,40 @@
"not_decrypted_yet": "This message is not decrypted yet",
"broken_message": "Broken 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",

View file

@ -390,7 +390,18 @@
"incoming": "Входящий звонок…",
"answer": "Ответить",
"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": {
"new_messages": "Новые сообщения",
@ -476,7 +487,40 @@
"not_decrypted_yet": "Сообщение ещё не расшифровано",
"broken_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": "Входящие",

View file

@ -17,6 +17,7 @@ import {
Spinner,
} from 'folds';
import { MatrixError } from 'matrix-js-sdk';
import { useTranslation } from 'react-i18next';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { stopPropagation } from '../../utils/keyboard';
@ -27,6 +28,7 @@ type LeaveRoomPromptProps = {
onCancel: () => void;
};
export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
@ -66,7 +68,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
size="500"
>
<Box grow="Yes">
<Text size="H4">Leave Room</Text>
<Text size="H4">{t('Room.leave_room_title')}</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300">
<Icon src={Icons.Cross} />
@ -74,10 +76,10 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="200">
<Text priority="400">Are you sure you want to leave this room?</Text>
<Text priority="400">{t('Room.leave_room_confirm')}</Text>
{leaveState.status === AsyncStatus.Error && (
<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>
)}
</Box>
@ -96,7 +98,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
}
>
<Text size="B400">
{leaveState.status === AsyncStatus.Loading ? 'Leaving...' : 'Leave'}
{leaveState.status === AsyncStatus.Loading ? t('Room.leaving') : t('Room.leave')}
</Text>
</Button>
</Box>

View file

@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
import { Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai';
import { Trans, useTranslation } from 'react-i18next';
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
import { getMemberDisplayName, getStateEvent } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';
@ -23,6 +24,7 @@ export type RoomIntroProps = {
};
export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { navigateRoom } = useRoomNavigate();
@ -66,19 +68,21 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
{name}
</Text>
<Text size="T400" priority="400">
{typeof topic === 'string' ? topic : 'This is the beginning of conversation.'}
{typeof topic === 'string' ? topic : t('Room.conversation_beginning')}
</Text>
{creatorName && ts && (
<Text size="T200" priority="300">
{'Created by '}
<b>@{creatorName}</b>
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
<Trans
i18nKey="Room.created_by"
values={{ creator: creatorName, date: timeDayMonthYear(ts), time: timeHourMinute(ts, hour24Clock) }}
components={{ bold: <b /> }}
/>
</Text>
)}
</Box>
<Box gap="200" wrap="Wrap">
<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>
{invitePrompt && (
@ -93,7 +97,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
fill="Soft"
radii="300"
>
<Text size="B300">Open Old Room</Text>
<Text size="B300">{t('Room.open_old_room')}</Text>
</Button>
) : (
<Button
@ -109,7 +113,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
) : undefined
}
>
<Text size="B300">Join Old Room</Text>
<Text size="B300">{t('Room.join_old_room')}</Text>
</Button>
))}
</Box>

View file

@ -1,6 +1,7 @@
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
import React, { useCallback } from 'react';
import { useSetAtom, useStore } from 'jotai';
import { useTranslation } from 'react-i18next';
import { StatusDivider } from './components';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
@ -20,12 +21,13 @@ type MicrophoneButtonProps = {
disabled?: boolean;
};
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
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>
}
>
@ -53,12 +55,13 @@ type SoundButtonProps = {
disabled?: boolean;
};
function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
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>
}
>
@ -90,12 +93,13 @@ type VideoButtonProps = {
disabled?: boolean;
};
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
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>
}
>
@ -127,12 +131,13 @@ type ScreenShareButtonProps = {
disabled?: boolean;
};
function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
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>
}
>
@ -163,6 +168,7 @@ export function CallControl({
compact: boolean;
callJoined: boolean;
}) {
const { t } = useTranslation();
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
const setCallEmbed = useSetAtom(callEmbedAtom);
const store = useStore();
@ -250,7 +256,7 @@ export function CallControl({
>
{!compact && (
<Text as="span" size="L400">
End
{t('Call.end_call')}
</Text>
)}
</Chip>

View file

@ -1,6 +1,7 @@
import React from 'react';
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
import * as css from './styles.css';
import { callChatAtom } from '../../state/callEmbed';
@ -15,13 +16,14 @@ type MicrophoneButtonProps = {
onToggle: () => void;
};
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
delay={500}
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>
}
>
@ -47,13 +49,14 @@ type SoundButtonProps = {
onToggle: () => void;
};
export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
delay={500}
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>
}
>
@ -83,13 +86,14 @@ type VideoButtonProps = {
onToggle: () => void;
};
export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
delay={500}
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>
}
>
@ -119,13 +123,14 @@ type ScreenShareButtonProps = {
onToggle: () => void;
};
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
const { t } = useTranslation();
return (
<TooltipProvider
position="Top"
delay={500}
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>
}
>
@ -147,6 +152,7 @@ export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps)
}
export function ChatButton() {
const { t } = useTranslation();
const [chat, setChat] = useAtom(callChatAtom);
return (
@ -155,7 +161,7 @@ export function ChatButton() {
delay={500}
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>
}
>

View file

@ -47,7 +47,7 @@ import {
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
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 { useMatrixClient } from '../../hooks/useMatrixClient';
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
@ -1534,8 +1534,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<b>{senderName}</b>
{callJoined ? ' joined the call' : ' ended the call'}
<Trans
i18nKey={callJoined ? 'Room.member_joined_call' : 'Room.member_ended_call'}
values={{ user: senderName }}
components={{ bold: <b /> }}
/>
</Text>
</Box>
}

View file

@ -1,6 +1,7 @@
import React, { ReactNode } from 'react';
import { IconSrc, Icons } from 'folds';
import { MatrixEvent } from 'matrix-js-sdk';
import { Trans } from 'react-i18next';
import { IMemberContent, Membership } from '../../types/matrix/room';
import { getMxIdLocalPart } from '../utils/matrix';
import { isMembershipChanged } from '../utils/room';
@ -12,6 +13,8 @@ export type ParsedResult = {
export type MemberEventParser = (mEvent: MatrixEvent) => ParsedResult;
const B = <b />;
export const useMemberEventParser = (): MemberEventParser => {
const parseMemberEvent: MemberEventParser = (mEvent) => {
const content = mEvent.getContent<IMemberContent>();
@ -23,7 +26,7 @@ export const useMemberEventParser = (): MemberEventParser => {
if (!senderId || !userId)
return {
icon: Icons.User,
body: 'Broken membership event',
body: <Trans i18nKey="Room.member_broken" />,
};
const senderName = getMxIdLocalPart(senderId);
@ -39,11 +42,12 @@ export const useMemberEventParser = (): MemberEventParser => {
icon: Icons.ArrowGoRightPlus,
body: (
<>
<b>{senderName}</b>
{' accepted '}
<b>{userName}</b>
{`'s join request `}
{reason}
<Trans
i18nKey="Room.member_accepted_knock"
values={{ sender: senderName, user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -53,9 +57,12 @@ export const useMemberEventParser = (): MemberEventParser => {
icon: Icons.ArrowGoRightPlus,
body: (
<>
<b>{senderName}</b>
{' invited '}
<b>{userName}</b> {reason}
<Trans
i18nKey="Room.member_invited"
values={{ sender: senderName, user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -66,9 +73,12 @@ export const useMemberEventParser = (): MemberEventParser => {
icon: Icons.ArrowGoRightPlus,
body: (
<>
<b>{userName}</b>
{' request to join room '}
{reason}
<Trans
i18nKey="Room.member_knock"
values={{ user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -78,10 +88,11 @@ export const useMemberEventParser = (): MemberEventParser => {
return {
icon: Icons.ArrowGoRight,
body: (
<>
<b>{userName}</b>
{' joined the room'}
</>
<Trans
i18nKey="Room.member_joined"
values={{ user: userName }}
components={{ bold: B }}
/>
),
};
}
@ -93,17 +104,21 @@ export const useMemberEventParser = (): MemberEventParser => {
body:
senderId === userId ? (
<>
<b>{userName}</b>
{' rejected the invitation '}
{reason}
<Trans
i18nKey="Room.member_rejected_invite"
values={{ user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
) : (
<>
<b>{senderName}</b>
{' rejected '}
<b>{userName}</b>
{`'s join request `}
{reason}
<Trans
i18nKey="Room.member_rejected_knock"
values={{ sender: senderName, user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -115,17 +130,21 @@ export const useMemberEventParser = (): MemberEventParser => {
body:
senderId === userId ? (
<>
<b>{userName}</b>
{' revoked joined request '}
{reason}
<Trans
i18nKey="Room.member_revoked_knock"
values={{ user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
) : (
<>
<b>{senderName}</b>
{' revoked '}
<b>{userName}</b>
{`'s invite `}
{reason}
<Trans
i18nKey="Room.member_revoked_invite"
values={{ sender: senderName, user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -136,9 +155,12 @@ export const useMemberEventParser = (): MemberEventParser => {
icon: Icons.ArrowGoLeft,
body: (
<>
<b>{senderName}</b>
{' unbanned '}
<b>{userName}</b> {reason}
<Trans
i18nKey="Room.member_unbanned"
values={{ sender: senderName, user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -149,15 +171,21 @@ export const useMemberEventParser = (): MemberEventParser => {
body:
senderId === userId ? (
<>
<b>{userName}</b>
{' left the room '}
{reason}
<Trans
i18nKey="Room.member_left"
values={{ user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
) : (
<>
<b>{senderName}</b>
{' kicked '}
<b>{userName}</b> {reason}
<Trans
i18nKey="Room.member_kicked"
values={{ sender: senderName, user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -168,9 +196,12 @@ export const useMemberEventParser = (): MemberEventParser => {
icon: Icons.ArrowGoLeft,
body: (
<>
<b>{senderName}</b>
{' banned '}
<b>{userName}</b> {reason}
<Trans
i18nKey="Room.member_banned"
values={{ sender: senderName, user: userName }}
components={{ bold: B }}
/>
{reason ? ` ${reason}` : ''}
</>
),
};
@ -187,16 +218,17 @@ export const useMemberEventParser = (): MemberEventParser => {
icon: Icons.Mention,
body:
typeof content.displayname === 'string' ? (
<>
<b>{prevUserName}</b>
{' changed display name to '}
<b>{userName}</b>
</>
<Trans
i18nKey="Room.member_name_changed"
values={{ oldName: prevUserName, newName: userName }}
components={{ bold: B }}
/>
) : (
<>
<b>{prevUserName}</b>
{' removed their display name '}
</>
<Trans
i18nKey="Room.member_name_removed"
values={{ user: prevUserName }}
components={{ bold: B }}
/>
),
};
}
@ -205,22 +237,24 @@ export const useMemberEventParser = (): MemberEventParser => {
icon: Icons.User,
body:
content.avatar_url && typeof content.avatar_url === 'string' ? (
<>
<b>{userName}</b>
{' changed their avatar'}
</>
<Trans
i18nKey="Room.member_avatar_changed"
values={{ user: userName }}
components={{ bold: B }}
/>
) : (
<>
<b>{userName}</b>
{' removed their avatar '}
</>
<Trans
i18nKey="Room.member_avatar_removed"
values={{ user: userName }}
components={{ bold: B }}
/>
),
};
}
return {
icon: Icons.User,
body: 'Membership event with no changes',
body: <Trans i18nKey="Room.member_no_change" />,
};
};