fix(web): баннер сервера из снапшота, приглашение, встроенные звуки
- READY-снапшот больше не теряет banner/splash/accent: шапка сервера показывает баннер сразу после подключения (AGENT.md 7.4) - GUILD_UPDATE несёт всё оформление, клиент применяет его без перезагрузки - кнопка приглашения в шапке сервера со ссылкой и копированием - встроенные звуки интерфейса на Web Audio с разблокировкой по жесту - object URL превью через безопасные обёртки (jsdom и урезанные webview)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
@@ -13,7 +13,9 @@ import { myGuildsQueryKey } from '@/api/users';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { Button } from '@/components/ui/primitives';
|
||||
import { avatarUrl } from '@/lib/identity';
|
||||
import { revokeObjectUrl, safeObjectUrl } from '@/lib/objectUrl';
|
||||
import { useApiErrorMessage } from '@/lib/useApiErrorMessage';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
import { SettingsSection } from '@/pages/settings/SettingsSection';
|
||||
import type { Guild } from '@/api/types';
|
||||
|
||||
@@ -51,11 +53,42 @@ export function GuildAppearanceSection({ guild, canEdit }: GuildAppearanceSectio
|
||||
const iconInput = useRef<HTMLInputElement | null>(null);
|
||||
const bannerInput = useRef<HTMLInputElement | null>(null);
|
||||
const splashInput = useRef<HTMLInputElement | null>(null);
|
||||
// Зеркало превью: нужно, чтобы освобождать object URL при закрытии настроек.
|
||||
const previewsRef = useRef<Partial<Record<GuildAppearanceKind, string>>>({});
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
for (const url of Object.values(previewsRef.current)) {
|
||||
revokeObjectUrl(url);
|
||||
}
|
||||
previewsRef.current = {};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/** Меняет превью вида, освобождая предыдущую ссылку на локальный файл. */
|
||||
const putPreview = (kind: GuildAppearanceKind, url: string | null): void => {
|
||||
revokeObjectUrl(previewsRef.current[kind]);
|
||||
const next = { ...previewsRef.current };
|
||||
if (url === null) {
|
||||
delete next[kind];
|
||||
} else {
|
||||
next[kind] = url;
|
||||
}
|
||||
previewsRef.current = next;
|
||||
setPreviews(next);
|
||||
};
|
||||
|
||||
const applyGuild = (updated: Guild): void => {
|
||||
// Карточка сервера и его список в рейке обновляются сразу: оформление
|
||||
// видно без перезагрузки (AGENT.md 11.6).
|
||||
// Карточка сервера, рейка и шапка обновляются сразу: рейка берёт иконку из
|
||||
// session-store, а не из запроса, поэтому обновляем оба места (AGENT.md 11.6).
|
||||
queryClient.setQueryData(guildQueryKey(guild.id), updated);
|
||||
useSessionStore.getState().applyGuildAppearance(guild.id, {
|
||||
icon_file_id: updated.icon_file_id ?? '',
|
||||
banner_file_id: updated.banner_file_id ?? '',
|
||||
splash_file_id: updated.splash_file_id ?? '',
|
||||
accent_color: updated.accent_color ?? 0,
|
||||
});
|
||||
void queryClient.invalidateQueries({ queryKey: myGuildsQueryKey });
|
||||
};
|
||||
|
||||
@@ -64,7 +97,8 @@ export function GuildAppearanceSection({ guild, canEdit }: GuildAppearanceSectio
|
||||
uploadGuildAppearance(guild.id, kind, file),
|
||||
onSuccess: (updated, variables) => {
|
||||
setError(null);
|
||||
setPreviews((current) => ({ ...current, [variables.kind]: undefined }));
|
||||
// Превью заменяет ответ сервера: локальная ссылка больше не нужна.
|
||||
putPreview(variables.kind, null);
|
||||
applyGuild(updated);
|
||||
},
|
||||
onError: (cause) => setError(cause),
|
||||
@@ -72,8 +106,9 @@ export function GuildAppearanceSection({ guild, canEdit }: GuildAppearanceSectio
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (kind: GuildAppearanceKind) => deleteGuildAppearance(guild.id, kind),
|
||||
onSuccess: (updated) => {
|
||||
onSuccess: (updated, kind) => {
|
||||
setError(null);
|
||||
putPreview(kind, null);
|
||||
applyGuild(updated);
|
||||
},
|
||||
onError: (cause) => setError(cause),
|
||||
@@ -104,7 +139,7 @@ export function GuildAppearanceSection({ guild, canEdit }: GuildAppearanceSectio
|
||||
setError(new Error(t('settings.servers.appearance.tooLarge')));
|
||||
return;
|
||||
}
|
||||
setPreviews((current) => ({ ...current, [kind]: URL.createObjectURL(file) }));
|
||||
putPreview(kind, safeObjectUrl(file));
|
||||
upload.mutate({ kind, file });
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user