diff --git a/web/src/api/gatewayEvents.ts b/web/src/api/gatewayEvents.ts index e975805..295bab0 100644 --- a/web/src/api/gatewayEvents.ts +++ b/web/src/api/gatewayEvents.ts @@ -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']; } diff --git a/web/src/api/types.ts b/web/src/api/types.ts index d72fb48..d26830e 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -91,6 +91,8 @@ export interface Channel { name: string; type: ChannelType; position: number; + /** Статус комнаты: описание показывается в шапке рядом с названием. */ + description?: string; parent_id?: string; user_limit?: number; slowmode_seconds?: number; diff --git a/web/src/pages/app/GuildView.tsx b/web/src/pages/app/GuildView.tsx index 8066ba4..b9b9acc 100644 --- a/web/src/pages/app/GuildView.tsx +++ b/web/src/pages/app/GuildView.tsx @@ -110,6 +110,17 @@ export default function GuildView() { {isVoice ? '🔊' : '#'}

{channel.name}

+ {channel.description === undefined || channel.description === '' ? null : ( + // Статус комнаты: описание рядом с названием; длинное усекается, а + // полный текст остаётся в подсказке (AGENT.md 7.5, 11.6). + + {channel.description} + + )} {channel.slowmode_seconds === undefined || channel.slowmode_seconds === 0 ? null : ( {t('channels.slowmode', { seconds: channel.slowmode_seconds })} diff --git a/web/tests/channelHeader.test.tsx b/web/tests/channelHeader.test.tsx new file mode 100644 index 0000000..46f5b41 --- /dev/null +++ b/web/tests/channelHeader.test.tsx @@ -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[] = []; + +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) { + 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('Новый статус'); + }); + }); +}); diff --git a/web/tests/helpers.tsx b/web/tests/helpers.tsx index 53ed8f0..aec4fb9 100644 --- a/web/tests/helpers.tsx +++ b/web/tests/helpers.tsx @@ -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 }),