feat(web): статус комнаты в шапке
Описание комнаты жило только в настройках: в шапке были имя и медленный режим, поэтому «статус комнаты» из матрицы 11.6 приходилось проверять по REST. Теперь описание показывается рядом с названием (длинное усекается, полный текст — в подсказке), а само поле приходит и в READY, и в событиях комнат (AGENT.md 7.5, 11.6). - `Channel.description` и разбор `description` в `parseChannelPayload`; - `channel-description` в шапке комнаты; - vitest `tests/channelHeader.test.tsx`: статус из снапшота, отсутствие статуса без описания и обновление по `CHANNEL_UPDATE` без перезагрузки.
This commit is contained in:
@@ -356,6 +356,11 @@ export function parseChannelPayload(value: unknown): Channel | null {
|
|||||||
if (typeof record['slowmode_seconds'] === 'number') {
|
if (typeof record['slowmode_seconds'] === 'number') {
|
||||||
channel.slowmode_seconds = record['slowmode_seconds'];
|
channel.slowmode_seconds = record['slowmode_seconds'];
|
||||||
}
|
}
|
||||||
|
// Статус комнаты: описание показывается в шапке рядом с названием (AGENT.md 7.5).
|
||||||
|
const description = asString(record['description']);
|
||||||
|
if (description !== undefined) {
|
||||||
|
channel.description = description;
|
||||||
|
}
|
||||||
if (typeof record['can_send'] === 'boolean') {
|
if (typeof record['can_send'] === 'boolean') {
|
||||||
channel.can_send = record['can_send'];
|
channel.can_send = record['can_send'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ export interface Channel {
|
|||||||
name: string;
|
name: string;
|
||||||
type: ChannelType;
|
type: ChannelType;
|
||||||
position: number;
|
position: number;
|
||||||
|
/** Статус комнаты: описание показывается в шапке рядом с названием. */
|
||||||
|
description?: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
user_limit?: number;
|
user_limit?: number;
|
||||||
slowmode_seconds?: number;
|
slowmode_seconds?: number;
|
||||||
|
|||||||
@@ -110,6 +110,17 @@ export default function GuildView() {
|
|||||||
{isVoice ? '🔊' : '#'}
|
{isVoice ? '🔊' : '#'}
|
||||||
</span>
|
</span>
|
||||||
<h2 className="text-sm font-semibold">{channel.name}</h2>
|
<h2 className="text-sm font-semibold">{channel.name}</h2>
|
||||||
|
{channel.description === undefined || channel.description === '' ? null : (
|
||||||
|
// Статус комнаты: описание рядом с названием; длинное усекается, а
|
||||||
|
// полный текст остаётся в подсказке (AGENT.md 7.5, 11.6).
|
||||||
|
<span
|
||||||
|
data-testid="channel-description"
|
||||||
|
title={channel.description}
|
||||||
|
className="min-w-0 truncate text-xs text-fg-muted"
|
||||||
|
>
|
||||||
|
{channel.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{channel.slowmode_seconds === undefined || channel.slowmode_seconds === 0 ? null : (
|
{channel.slowmode_seconds === undefined || channel.slowmode_seconds === 0 ? null : (
|
||||||
<span className="text-xs text-fg-muted">
|
<span className="text-xs text-fg-muted">
|
||||||
{t('channels.slowmode', { seconds: channel.slowmode_seconds })}
|
{t('channels.slowmode', { seconds: channel.slowmode_seconds })}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { act, screen, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
installFetch,
|
||||||
|
installGatewaySocket,
|
||||||
|
json,
|
||||||
|
makeReadySnapshot,
|
||||||
|
makeUser,
|
||||||
|
renderApp,
|
||||||
|
resetStores,
|
||||||
|
type FetchRoute,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Шапка комнаты показывает статус комнаты — описание рядом с названием
|
||||||
|
* (AGENT.md 7.5, 11.6): раньше описание жило только в настройках, и проверить
|
||||||
|
* «статус комнаты в шапке» было нечем.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const user = makeUser();
|
||||||
|
|
||||||
|
/** Текущий список комнат: его перечитывает клиент по CHANNEL_UPDATE. */
|
||||||
|
let channels: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
|
function routes(): FetchRoute[] {
|
||||||
|
return [
|
||||||
|
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
|
||||||
|
{ match: '/api/v1/users/@me', response: () => json({ user }) },
|
||||||
|
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) },
|
||||||
|
{
|
||||||
|
match: '/api/v1/guilds/g-1/members',
|
||||||
|
response: () => json({ members: [] }),
|
||||||
|
},
|
||||||
|
{ match: '/api/v1/guilds/g-1/roles', response: () => json({ roles: [] }) },
|
||||||
|
{ match: '/api/v1/guilds/g-1/channels', response: () => json({ channels }) },
|
||||||
|
{
|
||||||
|
match: '/api/v1/guilds/g-1',
|
||||||
|
response: () => json({ guild: { id: 'g-1', name: 'Сервер', owner_id: 'user-1' } }),
|
||||||
|
},
|
||||||
|
{ match: '/api/v1/channels/c-1/messages', response: () => json({ messages: [] }) },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Поднимает приложение на комнате `c-1` с заданными полями комнаты. */
|
||||||
|
async function boot(channel: Record<string, unknown>) {
|
||||||
|
channels = [{ id: 'c-1', name: 'общий', type: 'text', position: 0, ...channel }];
|
||||||
|
const snapshot = makeReadySnapshot([
|
||||||
|
{
|
||||||
|
id: 'g-1',
|
||||||
|
name: 'Сервер',
|
||||||
|
is_main: true,
|
||||||
|
owner_id: 'user-1',
|
||||||
|
my_permissions: ['MANAGE_CHANNELS', 'SEND_MESSAGES'],
|
||||||
|
channels: [channel as { id: string; name: string }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
installFetch(routes());
|
||||||
|
const gateway = installGatewaySocket(snapshot);
|
||||||
|
renderApp('/app/g-1/c-1');
|
||||||
|
await gateway.greet();
|
||||||
|
await screen.findByTestId('guild-rail');
|
||||||
|
return { gateway };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetStores();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('статус комнаты в шапке', () => {
|
||||||
|
it('показывает описание комнаты из снапшота', async () => {
|
||||||
|
await boot({ id: 'c-1', name: 'общий', description: 'Статус комнаты' });
|
||||||
|
|
||||||
|
const header = await screen.findByTestId('channel-header');
|
||||||
|
const description = await screen.findByTestId('channel-description');
|
||||||
|
expect(description).toHaveTextContent('Статус комнаты');
|
||||||
|
expect(description.getAttribute('title')).toBe('Статус комнаты');
|
||||||
|
expect(header).toHaveTextContent('общий');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('не показывает статус у комнаты без описания', async () => {
|
||||||
|
await boot({ id: 'c-1', name: 'общий' });
|
||||||
|
|
||||||
|
await screen.findByTestId('channel-header');
|
||||||
|
expect(screen.queryByTestId('channel-description')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('обновляет статус по CHANNEL_UPDATE без перезагрузки', async () => {
|
||||||
|
const { gateway } = await boot({ id: 'c-1', name: 'общий', description: 'Старый статус' });
|
||||||
|
await screen.findByTestId('channel-description');
|
||||||
|
|
||||||
|
// Сервер перечитал комнаты: клиент по событию запрашивает список заново.
|
||||||
|
channels = [
|
||||||
|
{ id: 'c-1', name: 'общий', type: 'text', position: 0, description: 'Новый статус' },
|
||||||
|
];
|
||||||
|
act(() => {
|
||||||
|
gateway.dispatch('CHANNEL_UPDATE', {
|
||||||
|
id: 'c-1',
|
||||||
|
guild_id: 'g-1',
|
||||||
|
name: 'общий',
|
||||||
|
type: 'text',
|
||||||
|
description: 'Новый статус',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('channel-description')).toHaveTextContent('Новый статус');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -257,6 +257,8 @@ interface SnapshotGuildInput {
|
|||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
can_send?: boolean;
|
can_send?: boolean;
|
||||||
can_view?: boolean;
|
can_view?: boolean;
|
||||||
|
/** Статус комнаты: описание в шапке рядом с названием. */
|
||||||
|
description?: string;
|
||||||
/** Фон комнаты: файл-картинка за лентой (AGENT.md 7.5). */
|
/** Фон комнаты: файл-картинка за лентой (AGENT.md 7.5). */
|
||||||
background_file_id?: string;
|
background_file_id?: string;
|
||||||
}[];
|
}[];
|
||||||
@@ -306,6 +308,7 @@ export function makeReadySnapshot(
|
|||||||
...(channel.parent_id === undefined ? {} : { parent_id: channel.parent_id }),
|
...(channel.parent_id === undefined ? {} : { parent_id: channel.parent_id }),
|
||||||
...(channel.can_send === undefined ? {} : { can_send: channel.can_send }),
|
...(channel.can_send === undefined ? {} : { can_send: channel.can_send }),
|
||||||
...(channel.can_view === undefined ? {} : { can_view: channel.can_view }),
|
...(channel.can_view === undefined ? {} : { can_view: channel.can_view }),
|
||||||
|
...(channel.description === undefined ? {} : { description: channel.description }),
|
||||||
...(channel.background_file_id === undefined
|
...(channel.background_file_id === undefined
|
||||||
? {}
|
? {}
|
||||||
: { background_file_id: channel.background_file_id }),
|
: { background_file_id: channel.background_file_id }),
|
||||||
|
|||||||
Reference in New Issue
Block a user