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:
2026-09-26 11:06:28 +03:00
parent 5f21c9d069
commit 70eae99873
5 changed files with 131 additions and 0 deletions
+5
View File
@@ -356,6 +356,11 @@ export function parseChannelPayload(value: unknown): Channel | null {
if (typeof record['slowmode_seconds'] === 'number') {
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') {
channel.can_send = record['can_send'];
}
+2
View File
@@ -91,6 +91,8 @@ export interface Channel {
name: string;
type: ChannelType;
position: number;
/** Статус комнаты: описание показывается в шапке рядом с названием. */
description?: string;
parent_id?: string;
user_limit?: number;
slowmode_seconds?: number;
+11
View File
@@ -110,6 +110,17 @@ export default function GuildView() {
{isVoice ? '🔊' : '#'}
</span>
<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 : (
<span className="text-xs text-fg-muted">
{t('channels.slowmode', { seconds: channel.slowmode_seconds })}
+110
View File
@@ -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('Новый статус');
});
});
});
+3
View File
@@ -257,6 +257,8 @@ interface SnapshotGuildInput {
parent_id?: string;
can_send?: boolean;
can_view?: boolean;
/** Статус комнаты: описание в шапке рядом с названием. */
description?: string;
/** Фон комнаты: файл-картинка за лентой (AGENT.md 7.5). */
background_file_id?: string;
}[];
@@ -306,6 +308,7 @@ export function makeReadySnapshot(
...(channel.parent_id === undefined ? {} : { parent_id: channel.parent_id }),
...(channel.can_send === undefined ? {} : { can_send: channel.can_send }),
...(channel.can_view === undefined ? {} : { can_view: channel.can_view }),
...(channel.description === undefined ? {} : { description: channel.description }),
...(channel.background_file_id === undefined
? {}
: { background_file_id: channel.background_file_id }),