fix(web): баннер сервера из снапшота, приглашение, встроенные звуки
- READY-снапшот больше не теряет banner/splash/accent: шапка сервера показывает баннер сразу после подключения (AGENT.md 7.4) - GUILD_UPDATE несёт всё оформление, клиент применяет его без перезагрузки - кнопка приглашения в шапке сервера со ссылкой и копированием - встроенные звуки интерфейса на Web Audio с разблокировкой по жесту - object URL превью через безопасные обёртки (jsdom и урезанные webview)
This commit is contained in:
@@ -164,9 +164,14 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void {
|
||||
}
|
||||
case 'GUILD_UPDATE': {
|
||||
const update = parseGuildUpdate(event.d);
|
||||
if (update?.name !== undefined) {
|
||||
if (update === null) {
|
||||
return;
|
||||
}
|
||||
if (update.name !== undefined) {
|
||||
session.renameGuild(update.guild_id, update.name);
|
||||
}
|
||||
// Оформление меняется мгновенно у всех: иконка, баннер, splash, акцент.
|
||||
session.applyGuildAppearance(update.guild_id, update);
|
||||
return;
|
||||
}
|
||||
case 'GUILD_DELETE': {
|
||||
|
||||
@@ -14,6 +14,25 @@ import type { Channel, GuildSummary, User, UserStatus } from '@/api/types';
|
||||
*/
|
||||
|
||||
/** Сервер снапшота: сводка для рейки + каналы/роли/эмодзи для интерфейса. */
|
||||
/** Изменение оформления сервера: пустая строка убирает файл. */
|
||||
export interface GuildAppearanceUpdate {
|
||||
icon_file_id?: string;
|
||||
banner_file_id?: string;
|
||||
splash_file_id?: string;
|
||||
accent_color?: number;
|
||||
}
|
||||
|
||||
/** pickAppearance собирает частичное обновление поля оформления. */
|
||||
function pickAppearance(
|
||||
field: 'icon_file_id' | 'banner_file_id' | 'splash_file_id',
|
||||
value: string | undefined,
|
||||
): Partial<GuildSummary> {
|
||||
if (value === undefined) {
|
||||
return {};
|
||||
}
|
||||
return value === '' ? { [field]: undefined } : { [field]: value };
|
||||
}
|
||||
|
||||
export interface SessionGuild extends GuildSummary {
|
||||
channels: Channel[];
|
||||
/** Кастомные эмодзи сервера: READY и `GUILD_EMOJIS_UPDATE` (AGENT.md 7.12). */
|
||||
@@ -50,6 +69,8 @@ interface SessionState {
|
||||
upsertGuild: (guild: GuildSummary) => void;
|
||||
/** GUILD_UPDATE: имя сервера (описание в снапшоте Фазы 1 не хранится). */
|
||||
renameGuild: (guildId: string, name: string) => void;
|
||||
/** Оформление сервера из `GUILD_UPDATE` или ответа API (AGENT.md 7.4). */
|
||||
applyGuildAppearance: (guildId: string, appearance: GuildAppearanceUpdate) => void;
|
||||
/** Свой никнейм на сервере (PATCH /guilds/{id}/members/{me}). */
|
||||
setMyNickname: (guildId: string, nickname: string) => void;
|
||||
removeGuild: (guildId: string) => void;
|
||||
@@ -134,9 +155,20 @@ function toSessionGuild(guild: GatewayGuild): SessionGuild {
|
||||
emojis: guild.emojis,
|
||||
sounds: guild.sounds,
|
||||
};
|
||||
// Оформление сервера из снапшота: без него шапка, рейка и акцент терялись
|
||||
// до первого переключения сервера (AGENT.md 7.4).
|
||||
if (guild.icon_file_id !== undefined) {
|
||||
summary.icon_file_id = guild.icon_file_id;
|
||||
}
|
||||
if (guild.banner_file_id !== undefined) {
|
||||
summary.banner_file_id = guild.banner_file_id;
|
||||
}
|
||||
if (guild.splash_file_id !== undefined) {
|
||||
summary.splash_file_id = guild.splash_file_id;
|
||||
}
|
||||
if (guild.accent_color !== undefined) {
|
||||
summary.accent_color = guild.accent_color;
|
||||
}
|
||||
if (guild.my_nickname !== undefined) {
|
||||
summary.my_nickname = guild.my_nickname;
|
||||
}
|
||||
@@ -250,6 +282,25 @@ export const useSessionStore = create<SessionState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
applyGuildAppearance: (guildId, appearance) => {
|
||||
set({
|
||||
guilds: get().guilds.map((guild) =>
|
||||
guild.id === guildId
|
||||
? {
|
||||
...guild,
|
||||
// Пустая строка означает «файла больше нет»: поле снимается.
|
||||
...pickAppearance('icon_file_id', appearance.icon_file_id),
|
||||
...pickAppearance('banner_file_id', appearance.banner_file_id),
|
||||
...pickAppearance('splash_file_id', appearance.splash_file_id),
|
||||
...(appearance.accent_color === undefined
|
||||
? {}
|
||||
: { accent_color: appearance.accent_color }),
|
||||
}
|
||||
: guild,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
setMyNickname: (guildId, nickname) => {
|
||||
set({
|
||||
guilds: get().guilds.map((guild) =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { create } from 'zustand';
|
||||
import type { GuildSound, SoundEvent } from '@/api/sounds';
|
||||
import { playSoundFileInBackground, stopAllSounds } from '@/lib/soundboard';
|
||||
import { guildSoundsOf, useSessionStore } from '@/stores/session';
|
||||
import { playInterfaceSound } from '@/lib/interfaceSounds';
|
||||
import { useVoiceStore } from '@/stores/voice';
|
||||
import type { SoundboardPlayEvent } from '@/api/gatewayEvents';
|
||||
|
||||
@@ -129,19 +130,21 @@ export function findEventSound(guildId: string | null, event: SoundEvent): Guild
|
||||
}
|
||||
|
||||
/**
|
||||
* Проигрывает звук события сервера, если он задан и звуки событий включены.
|
||||
* Вызывается из диспетчера шлюза и из голосовой сессии: молчание — норма,
|
||||
* когда звук не настроен или пользователь его выключил.
|
||||
* Проигрывает звук события: сначала свой звук сервера из палитры, а если его
|
||||
* нет — встроенный сигнал интерфейса (AGENT.md 7.13). Вызывается из диспетчера
|
||||
* шлюза и из голосовой сессии; молчание — норма, когда звук выключен.
|
||||
*/
|
||||
export function playEventSound(guildId: string | null, event: SoundEvent): void {
|
||||
if (guildId === null || !useVoiceStore.getState().eventSoundsEnabled) {
|
||||
if (!useVoiceStore.getState().eventSoundsEnabled) {
|
||||
return;
|
||||
}
|
||||
// Звук сервера важнее встроенного: администратор мог подобрать свой.
|
||||
const sound = findEventSound(guildId, event);
|
||||
if (sound === null) {
|
||||
if (sound !== null) {
|
||||
playSoundFileInBackground(sound.file_id);
|
||||
return;
|
||||
}
|
||||
playSoundFileInBackground(sound.file_id);
|
||||
playInterfaceSound(event);
|
||||
}
|
||||
|
||||
/** Полная остановка звуков: выход из комнаты, сброс сессии (тесты). */
|
||||
|
||||
@@ -64,6 +64,11 @@ export interface VoicePreferences {
|
||||
* (AGENT.md 7.14). Выключено — SDK не опрашивается вовсе.
|
||||
*/
|
||||
latencySharingEnabled: boolean;
|
||||
/**
|
||||
* Встроенные звуки интерфейса (AGENT.md 7.13): короткие сигналы событий,
|
||||
* когда на сервере нет своего звука. Палитру сервера не отключает.
|
||||
*/
|
||||
interfaceSoundsEnabled: boolean;
|
||||
/** Качество своей публикации видео. */
|
||||
videoQuality: VoicePublishQuality;
|
||||
/** Пресет разрешения и fps для демонстрации экрана. */
|
||||
@@ -87,6 +92,7 @@ interface VoiceState extends VoicePreferences {
|
||||
setEventSoundsEnabled: (enabled: boolean) => void;
|
||||
setSoundboardEnabled: (enabled: boolean) => void;
|
||||
setLatencySharingEnabled: (enabled: boolean) => void;
|
||||
setInterfaceSoundsEnabled: (enabled: boolean) => void;
|
||||
setVideoQuality: (quality: VoicePublishQuality) => void;
|
||||
setScreenSharePreset: (preset: ScreenSharePresetId) => void;
|
||||
/** Вернуть исходные предпочтения (используется в тестах). */
|
||||
@@ -110,6 +116,7 @@ const defaults: VoicePreferences = {
|
||||
eventSoundsEnabled: true,
|
||||
soundboardEnabled: true,
|
||||
latencySharingEnabled: true,
|
||||
interfaceSoundsEnabled: true,
|
||||
videoQuality: 'high',
|
||||
screenSharePreset: '1080p30',
|
||||
};
|
||||
@@ -195,6 +202,10 @@ function initialPreferences(): VoicePreferences {
|
||||
stored['latencySharingEnabled'],
|
||||
defaults.latencySharingEnabled,
|
||||
),
|
||||
interfaceSoundsEnabled: asBoolean(
|
||||
stored['interfaceSoundsEnabled'],
|
||||
defaults.interfaceSoundsEnabled,
|
||||
),
|
||||
videoQuality: asChoice(stored['videoQuality'], PUBLISH_QUALITIES, defaults.videoQuality),
|
||||
screenSharePreset: asChoice(
|
||||
stored['screenSharePreset'],
|
||||
@@ -225,6 +236,7 @@ function persist(state: VoicePreferences): void {
|
||||
eventSoundsEnabled: state.eventSoundsEnabled,
|
||||
soundboardEnabled: state.soundboardEnabled,
|
||||
latencySharingEnabled: state.latencySharingEnabled,
|
||||
interfaceSoundsEnabled: state.interfaceSoundsEnabled,
|
||||
videoQuality: state.videoQuality,
|
||||
screenSharePreset: state.screenSharePreset,
|
||||
} satisfies VoicePreferences),
|
||||
@@ -277,6 +289,7 @@ export const useVoiceStore = create<VoiceState>((set, get) => {
|
||||
setEventSoundsEnabled: (enabled) => patch({ eventSoundsEnabled: enabled }),
|
||||
setSoundboardEnabled: (enabled) => patch({ soundboardEnabled: enabled }),
|
||||
setLatencySharingEnabled: (enabled) => patch({ latencySharingEnabled: enabled }),
|
||||
setInterfaceSoundsEnabled: (enabled) => patch({ interfaceSoundsEnabled: enabled }),
|
||||
setVideoQuality: (quality) => patch({ videoQuality: quality }),
|
||||
setScreenSharePreset: (preset) => patch({ screenSharePreset: preset }),
|
||||
|
||||
@@ -306,6 +319,7 @@ function preferencesOf(state: VoiceState): VoicePreferences {
|
||||
eventSoundsEnabled: state.eventSoundsEnabled,
|
||||
soundboardEnabled: state.soundboardEnabled,
|
||||
latencySharingEnabled: state.latencySharingEnabled,
|
||||
interfaceSoundsEnabled: state.interfaceSoundsEnabled,
|
||||
videoQuality: state.videoQuality,
|
||||
screenSharePreset: state.screenSharePreset,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user