From 324d2aac72a62762248a63a55ea47ad66c5c5d84 Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 26 Sep 2026 16:16:10 +0300 Subject: [PATCH] =?UTF-8?q?fix(web):=20DM=5FCHANNEL=5FUPDATE=20=D0=B8=20DM?= =?UTF-8?q?=5FCHANNEL=5FDELETE=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D1=8E=D1=82=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20?= =?UTF-8?q?=D0=B1=D0=B5=D1=81=D0=B5=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сервер присылал эти события и раньше, но клиент перечитывал список бесед только по `DM_CHANNEL_CREATE` и READY: добавление участника, переименование группы и выход из неё не появлялись в сайдбаре до перезагрузки, а удалённая беседа оставалась открытой. Теперь все три события перечитывают список (`DM_CHANNEL_UPDATE` меняет состав, имя и аватар группы сразу), а `DM_CHANNEL_DELETE` дополнительно убирает беседу из стора сессии и закрывает её, если она была открыта. Тесты в `web/tests/friends.test.tsx` падают на старом коде: переименование группы видно второму участнику, удалённая беседа исчезает и закрывается. --- web/src/stores/gateway.ts | 16 +++++++++-- web/src/stores/session.ts | 11 ++++++++ web/tests/friends.test.tsx | 58 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/web/src/stores/gateway.ts b/web/src/stores/gateway.ts index f403c5a..47d72e6 100644 --- a/web/src/stores/gateway.ts +++ b/web/src/stores/gateway.ts @@ -378,11 +378,21 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void { } return; } - case 'DM_CHANNEL_CREATE': { - // В событии только channel_id: список бесед забираем по REST. + case 'DM_CHANNEL_CREATE': + case 'DM_CHANNEL_UPDATE': + case 'DM_CHANNEL_DELETE': { + // В событии только channel_id: список бесед забираем по REST. CREATE — + // новая беседа, UPDATE — изменился состав или имя группы, DELETE — + // беседа больше не наша (нас удалили). Список читается целиком, потому + // что от состава зависят и имя, и аватар, и шапка беседы. const direct = parseDmChannelEvent(event.d); if (direct !== null) { - void session.reloadDmChannels(); + void session.reloadDmChannels().then(() => { + if (event.t === 'DM_CHANNEL_DELETE') { + // Удалённая беседа могла быть открыта: уводим в список бесед. + useSessionStore.getState().removeDmChannel(direct.channel_id); + } + }); } return; } diff --git a/web/src/stores/session.ts b/web/src/stores/session.ts index 628a67c..8fdf9b2 100644 --- a/web/src/stores/session.ts +++ b/web/src/stores/session.ts @@ -114,6 +114,8 @@ interface SessionState { reloadDmChannels: () => Promise; /** CHANNEL_DELETE: удаляет комнату и переносит выбор на первую доступную. */ removeChannel: (guildId: string, channelId: string) => void; + /** DM_CHANNEL_DELETE: беседа пропала — убирает её из списка и из выбора. */ + removeDmChannel: (channelId: string) => void; /** GUILD_CHANNELS_SYNC: перечитать видимые комнаты сервера (права изменились). */ reloadGuildChannels: (guildId: string) => Promise; /** То же, но с объединением частых вызовов в один запрос. */ @@ -582,6 +584,15 @@ export const useSessionStore = create((set, get) => ({ }); }, + removeDmChannel: (channelId) => { + const state = get(); + set({ + dmChannels: state.dmChannels.filter((channel) => channel.id !== channelId), + // Если пропала открытая беседа, показываем список бесед раздела друзей. + selectedChannelId: state.selectedChannelId === channelId ? null : state.selectedChannelId, + }); + }, + firstChannelId: (guildId) => { const guild = get().guilds.find((item) => item.id === guildId); return guild === undefined ? null : firstVisibleChannelId(guild); diff --git a/web/tests/friends.test.tsx b/web/tests/friends.test.tsx index 4e7a9d6..a2107ae 100644 --- a/web/tests/friends.test.tsx +++ b/web/tests/friends.test.tsx @@ -743,6 +743,64 @@ describe('события шлюза о друзьях', () => { expect(screen.getByTestId('dm-status-dm-1')).toHaveAttribute('data-status', 'online'); }); + it('DM_CHANNEL_UPDATE перечитывает список бесед', async () => { + // Состав и имя группы меняются событием: сервер шлёт только channel_id, + // поэтому список бесед перечитывается целиком (AGENT.md 7.8). + let renamed = false; + installFetch( + baseRoutes(empty, [ + { + match: '/api/v1/users/@me/channels', + response: () => + json({ + channels: renamed ? [directChannel('dm-1', 'Аня и Борис')] : [], + }), + }, + ]), + ); + const gateway = installGatewaySocket(makeReadySnapshot([])); + + renderApp('/app/friends'); + await screen.findByTestId('guild-rail'); + await gateway.greet(); + expect(await screen.findByText('Личных бесед пока нет.')).toBeVisible(); + + renamed = true; + await dispatchEvent(gateway, 'DM_CHANNEL_UPDATE', { channel_id: 'dm-1' }); + + expect(await screen.findByTestId('dm-link-dm-1')).toHaveTextContent('Аня и Борис'); + }); + + it('DM_CHANNEL_DELETE убирает беседу и закрывает её', async () => { + let removed = false; + installFetch( + baseRoutes(empty, [ + { + match: '/api/v1/users/@me/channels', + response: () => json({ channels: removed ? [] : [directChannel('dm-1')] }), + }, + ]), + ); + const gateway = installGatewaySocket( + makeReadySnapshot( + [], + [{ id: 'dm-1', name: 'Боб', recipient_id: 'user-2', status: 'online' }], + ), + ); + + renderApp('/app/friends/dm-1'); + await screen.findByTestId('guild-rail'); + await gateway.greet(); + expect(await screen.findByTestId('dm-link-dm-1')).toBeVisible(); + + removed = true; + await dispatchEvent(gateway, 'DM_CHANNEL_DELETE', { channel_id: 'dm-1' }); + + // Беседа исчезла из списка, а открытая беседа закрылась: показываем список. + await waitFor(() => expect(screen.queryByTestId('dm-link-dm-1')).not.toBeInTheDocument()); + expect(await screen.findByText('Личных бесед пока нет.')).toBeVisible(); + }); + it('RELATIONSHIP_UPDATE перечитывает друзей', async () => { let hasFriend = false; installFetch(baseRoutes(() => (hasFriend ? { ...empty, friends: [bob] } : empty)));