diff --git a/web/src/pages/settings/GuildSoundsSection.tsx b/web/src/pages/settings/GuildSoundsSection.tsx index bd8ba7b..4e719ec 100644 --- a/web/src/pages/settings/GuildSoundsSection.tsx +++ b/web/src/pages/settings/GuildSoundsSection.tsx @@ -73,6 +73,9 @@ export function GuildSoundsSection({ guildId }: GuildSoundsSectionProps) { }); const sounds = list.data ?? []; + // Ошибка перезапроса не должна прятать уже полученный список: под нагрузкой + // ручка отвечает 429, а снапшот события шлюза при этом свежий (7.13). + const failed = list.isError && list.data === undefined; const soundboard = sounds.filter((sound) => sound.kind === 'soundboard'); const palette = sounds.filter((sound) => sound.kind === 'ui'); const current = kind === 'soundboard' ? soundboard : palette; @@ -269,7 +272,7 @@ export function GuildSoundsSection({ guildId }: GuildSoundsSectionProps) { {list.isPending ? ( - ) : list.isError ? ( + ) : failed ? ( void list.refetch()} /> ) : current.length === 0 ? (

{t('settings.servers.sounds.empty')}

diff --git a/web/src/stores/gateway.ts b/web/src/stores/gateway.ts index 0abd83d..f403c5a 100644 --- a/web/src/stores/gateway.ts +++ b/web/src/stores/gateway.ts @@ -30,7 +30,7 @@ import { parseTypingStart, parseUserUpdate, } from '@/api/gatewayEvents'; -import { guildSoundsQueryKey } from '@/api/sounds'; +import { guildSoundsQueryKey, type GuildSound } from '@/api/sounds'; import { useFriendsStore } from '@/stores/friends'; import { useMessagesStore } from '@/stores/messages'; import { useSessionStore, type SessionGuild } from '@/stores/session'; @@ -338,7 +338,7 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void { const update = parseGuildSoundsUpdate(event.d); if (update !== null) { session.setGuildSounds(update.guild_id, update.sounds); - void invalidateSoundQueries(update.guild_id); + void applyGuildSounds(update.guild_id, update.sounds); } return; } @@ -567,10 +567,14 @@ async function invalidateEmojiQueries(guildId: string): Promise { await getQueryClient().invalidateQueries({ queryKey: guildEmojisQueryKey(guildId) }); } -/** Инвалидация списка звуков сервера после `GUILD_SOUNDS_UPDATE` (7.13). */ -async function invalidateSoundQueries(guildId: string): Promise { +/** + * Кладёт набор звуков из `GUILD_SOUNDS_UPDATE` в кэш REST-запроса: событие + * несёт список целиком, поэтому повторный запрос не нужен, а его отказ под + * нагрузкой (429) оставлял раздел настроек со старым списком (AGENT.md 7.13). + */ +async function applyGuildSounds(guildId: string, sounds: GuildSound[]): Promise { const { getQueryClient } = await import('@/lib/queryClient'); - await getQueryClient().invalidateQueries({ queryKey: guildSoundsQueryKey(guildId) }); + getQueryClient().setQueryData(guildSoundsQueryKey(guildId), sounds); } /** Держит URL в согласии с выбранным сервером/комнатой после удалений. */ diff --git a/web/tests/soundSettings.test.tsx b/web/tests/soundSettings.test.tsx index 4544643..2e76270 100644 --- a/web/tests/soundSettings.test.tsx +++ b/web/tests/soundSettings.test.tsx @@ -3,8 +3,11 @@ import { fireEvent, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { + apiError, installFetch, + installGatewaySocket, json, + makeReadySnapshot, makeUser, renderApp, requestUrl, @@ -157,6 +160,33 @@ describe('настройки сервера: секция «Звуки»', () => expect(within(dialog).queryByTestId('guild-sounds-counter')).toBeNull(); }); + it('показывает звук из GUILD_SOUNDS_UPDATE, даже если список по REST не отдался', async () => { + // Под нагрузкой ручка списка отвечает 429 (AGENT.md 8.6), а раздел должен + // обновляться снапшотом из события шлюза, а не повтором запроса (7.13). + const gateway = installGatewaySocket( + makeReadySnapshot([{ id: 'g-1', name: 'Сервер', my_permissions: ['MANAGE_SOUNDS'] }]), + ); + installFetch( + routes({ + extra: [ + { + match: '/api/v1/guilds/g-1/sounds', + response: () => apiError('ratelimit.hit', 429), + }, + ], + }), + ); + renderApp('/settings/servers/g-1'); + const dialog = await screen.findByRole('dialog', { name: 'Настройки' }); + await gateway.greet(); + + const fresh: GuildSound = { id: 's-3', name: 'fresh', file_id: '920', kind: 'soundboard' }; + gateway.dispatch('GUILD_SOUNDS_UPDATE', { guild_id: 'g-1', sounds: [airhorn, fresh] }); + + expect(await within(dialog).findByTestId('guild-sound-s-1')).toHaveTextContent('airhorn'); + expect(within(dialog).getByTestId('guild-sound-s-3')).toHaveTextContent('fresh'); + }); + it('загружает звук палитры: kind в URL, событие и имя в multipart', async () => { const { dialog, fetchMock } = await openSoundsSection(); await userEvent.click(within(dialog).getByTestId('guild-sounds-tab-ui'));