fix(install): LIVEKIT_URL попадает в compose приложения

Шаблон compose подставляет переменные из явного списка: LIVEKIT_PUBLIC_URL не
был в нём, поэтому приложение получало пустой адрес LiveKit и голос оставался
выключенным. Добавлены LIVEKIT_API_KEY/SECRET/PUBLIC_URL в рендер compose.
This commit is contained in:
2026-09-20 18:19:19 +03:00
parent ca34d05377
commit 2749a0e831
13 changed files with 73 additions and 60 deletions
+1 -2
View File
@@ -40,8 +40,7 @@ export function isValidEmojiName(name: string): boolean {
return CUSTOM_EMOJI_NAME_PATTERN.test(name);
}
export const guildEmojisQueryKey = (guildId: string) =>
['guilds', guildId, 'emojis'] as const;
export const guildEmojisQueryKey = (guildId: string) => ['guilds', guildId, 'emojis'] as const;
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
+1 -4
View File
@@ -2,10 +2,7 @@ import { Fragment, useMemo, type ReactNode, type RefObject } from 'react';
import { CustomEmojiImage } from '@/components/chat/CustomEmojiImage';
import { useChannelDirectory, type ChannelDirectory } from '@/components/chat/useChannelDirectory';
import {
useGuildEmojiDirectory,
type GuildEmojiDirectory,
} from '@/components/chat/useGuildEmojis';
import { useGuildEmojiDirectory, type GuildEmojiDirectory } from '@/components/chat/useGuildEmojis';
import { customEmojiShortcode, parseCustomEmoji } from '@/lib/customEmoji';
import {
hasMarkers,
+6 -3
View File
@@ -42,18 +42,21 @@ export function EmojiPicker({
const { t } = useTranslation();
const panelRef = useRef<HTMLDivElement | null>(null);
const [query, setQuery] = useState('');
const [prevOpen, setPrevOpen] = useState(open);
const serverEmojis = useGuildEmojis(guildId);
const onCloseRef = useRef(onClose);
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
// Поиск сбрасываем при каждом открытии: иначе список «залипал» бы от прошлого раза.
useEffect(() => {
// Поиск сбрасываем при открытии: подстройка состояния во время рендера —
// документированный приём React, эффект дал бы лишний кадр со старым списком.
if (prevOpen !== open) {
setPrevOpen(open);
if (open) {
setQuery('');
}
}, [open]);
}
const found = useMemo(() => {
const needle = query.trim().toLowerCase();
+1 -4
View File
@@ -3,10 +3,7 @@ import { useTranslation } from 'react-i18next';
import { CustomEmojiImage } from '@/components/chat/CustomEmojiImage';
import { useChannelDirectory, type ChannelDirectory } from '@/components/chat/useChannelDirectory';
import {
useGuildEmojiDirectory,
type GuildEmojiDirectory,
} from '@/components/chat/useGuildEmojis';
import { useGuildEmojiDirectory, type GuildEmojiDirectory } from '@/components/chat/useGuildEmojis';
import { customEmojiShortcode, parseCustomEmoji } from '@/lib/customEmoji';
import {
parseBlocks,
+2 -10
View File
@@ -153,13 +153,7 @@ function MessageBody({
* подставляем «участник». Неизвестное событие и мусорный JSON показываем как
* есть: ничего не теряем и не падаем.
*/
function SystemMessageBody({
message,
authors,
}: {
message: Message;
authors: AuthorDirectory;
}) {
function SystemMessageBody({ message, authors }: { message: Message; authors: AuthorDirectory }) {
const { t } = useTranslation();
const event = parseSystemMessage(message.content);
@@ -345,9 +339,7 @@ export function MessageItem({
} ${highlight ? 'gl-highlight rounded-[var(--radius-md)]' : ''}`}
>
<span className="sr-only">{t('chat.types.system')}</span>
{message.content === '' ? null : (
<SystemMessageBody message={message} authors={authors} />
)}
{message.content === '' ? null : <SystemMessageBody message={message} authors={authors} />}
<time dateTime={message.created_at} className="text-[10px] text-fg-muted/70">
{formatMessageTime(message.created_at, i18n.language)}
</time>
-5
View File
@@ -53,8 +53,3 @@ export function customEmojiShortcode(name: string): string {
export function customEmojiToken(name: string, fileId: string, animated = false): string {
return `<${animated ? 'a' : ''}:${name}:${fileId}>`;
}
/** Является ли строка токеном кастомного эмодзи. */
export function isCustomEmojiToken(value: string): boolean {
return parseCustomEmoji(value) !== null;
}
+1 -1
View File
@@ -103,7 +103,7 @@ export function canAdministerGuild(
* Может ли пользователь модерировать сообщения (удалять и закреплять чужие).
* Права приходят на уровне сервера, поэтому это приближение: сервер всё равно
* проверит права в конкретной комнате.
*/export function canManageMessages(
*/ export function canManageMessages(
permissions: readonly string[],
isInstanceAdmin = false,
): boolean {
@@ -183,9 +183,7 @@ export function GuildEmojisSection({ guildId }: GuildEmojisSectionProps) {
{formatBytes(emoji.size_bytes, i18n.language)}
</span>
)}
{emoji.animated ? (
<Badge>{t('settings.servers.emojis.animated')}</Badge>
) : null}
{emoji.animated ? <Badge>{t('settings.servers.emojis.animated')}</Badge> : null}
<Button
variant="ghost"
aria-label={t('settings.servers.emojis.renameLabel', {
@@ -287,7 +285,11 @@ export function GuildEmojisSection({ guildId }: GuildEmojisSectionProps) {
disabled={upload.isPending || file === null || name.trim() === '' || full}
onClick={submit}
>
{t(upload.isPending ? 'settings.servers.emojis.uploading' : 'settings.servers.emojis.upload')}
{t(
upload.isPending
? 'settings.servers.emojis.uploading'
: 'settings.servers.emojis.upload',
)}
</Button>
</div>
</div>
+5 -5
View File
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { guildEmojisQueryKey } from '@/api/emojis';
import {
connectGateway,
disconnectGateway,
@@ -366,11 +367,10 @@ async function invalidateGuildQueries(guildId: string): Promise<void> {
/** Инвалидация списка эмодзи сервера после `GUILD_EMOJIS_UPDATE`. */
async function invalidateEmojiQueries(guildId: string): Promise<void> {
const [{ getQueryClient }, emojis] = await Promise.all([
import('@/lib/queryClient'),
import('@/api/emojis'),
]);
await getQueryClient().invalidateQueries({ queryKey: emojis.guildEmojisQueryKey(guildId) });
// Ключ запроса берём из статического импорта: `api/emojis` не зависит от
// сторов, поэтому цикла «стор → query-client → стор» здесь нет.
const { getQueryClient } = await import('@/lib/queryClient');
await getQueryClient().invalidateQueries({ queryKey: guildEmojisQueryKey(guildId) });
}
/** Держит URL в согласии с выбранным сервером/комнатой после удалений. */