fix(install): публичный адрес LiveKit вычисляется после TLS_ENABLED

Установщик падал на 'TLS_ENABLED: unbound variable': расчёт LIVEKIT_PUBLIC_URL
стоял выше определения TLS_ENABLED. Значение перенесено после блока TLS,
dry-run проходит.
This commit is contained in:
2026-09-20 18:13:04 +03:00
parent ecd52c11d3
commit ca34d05377
33 changed files with 2505 additions and 71 deletions
+80 -2
View File
@@ -1,7 +1,10 @@
import { useEffect, useRef, type RefObject } from 'react';
import { useEffect, useMemo, useRef, useState, type RefObject } from 'react';
import { useTranslation } from 'react-i18next';
import { fileContentUrl } from '@/api/files';
import { useGuildEmojis } from '@/components/chat/useGuildEmojis';
import { POPULAR_EMOJIS, QUICK_REACTIONS } from '@/lib/chat';
import { customEmojiShortcode } from '@/lib/customEmoji';
interface EmojiPickerProps {
open: boolean;
@@ -11,11 +14,19 @@ interface EmojiPickerProps {
anchorRef?: RefObject<HTMLElement | null>;
/** Куда раскрывается панель: под якорем или над ним. */
placement?: 'top' | 'bottom';
/** Сервер, чьи кастомные эмодзи показываем: в беседе их нет. */
guildId?: string | null;
className?: string;
}
/**
* Выпадающий пикер эмодзи: ряд быстрых реакций и сетка популярных.
* Выпадающий пикер эмодзи: ряд быстрых реакций, эмодзи сервера и сетка
* популярных. Пикер общий для ленты (реакции) и композера (вставка в текст):
* `onPick` получает либо юникод-эмодзи, либо токен `<:имя:file_id>`.
*
* Эмодзи сервера ищутся по имени; список пуст, если сервера нет (личная
* беседа) или на нём ещё не загрузили ни одного эмодзи.
*
* Закрывается по Escape и клику вне панели; фокус остаётся в пределах
* tab-порядка страницы.
*/
@@ -25,15 +36,33 @@ export function EmojiPicker({
onPick,
anchorRef,
placement = 'bottom',
guildId = null,
className = '',
}: EmojiPickerProps) {
const { t } = useTranslation();
const panelRef = useRef<HTMLDivElement | null>(null);
const [query, setQuery] = useState('');
const serverEmojis = useGuildEmojis(guildId);
const onCloseRef = useRef(onClose);
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
// Поиск сбрасываем при каждом открытии: иначе список «залипал» бы от прошлого раза.
useEffect(() => {
if (open) {
setQuery('');
}
}, [open]);
const found = useMemo(() => {
const needle = query.trim().toLowerCase();
if (needle === '') {
return serverEmojis;
}
return serverEmojis.filter((emoji) => emoji.name.toLowerCase().includes(needle));
}, [serverEmojis, query]);
useEffect(() => {
if (!open) {
return;
@@ -90,6 +119,55 @@ export function EmojiPicker({
</button>
))}
</div>
{serverEmojis.length === 0 ? null : (
<div
role="group"
aria-label={t('chat.emoji.server')}
data-testid="emoji-picker-server"
className="mt-2 border-t border-border/40 pt-2"
>
<input
type="search"
value={query}
aria-label={t('chat.emoji.search')}
placeholder={t('chat.emoji.searchPlaceholder')}
onChange={(event) => setQuery(event.target.value)}
className="w-full rounded-[var(--radius-sm)] border border-border/60 bg-surface-1 px-2 py-1 text-xs text-fg placeholder:text-fg-muted/70"
/>
{found.length === 0 ? (
<p className="mt-1 px-1 text-xs text-fg-muted">{t('chat.emoji.notFound')}</p>
) : (
<ul className="mt-1 flex max-h-40 flex-col gap-0.5 overflow-y-auto">
{found.map((emoji) => (
<li key={emoji.id}>
<button
type="button"
title={customEmojiShortcode(emoji.name)}
aria-label={t('chat.emoji.pickServer', { name: emoji.name })}
className="flex w-full items-center gap-2 rounded-[var(--radius-sm)] px-1 py-0.5 text-left text-xs text-fg transition-colors duration-[var(--duration-fast)] hover:bg-surface-3 active:scale-[0.98]"
onClick={() => onPick(emoji.token)}
>
<img
src={fileContentUrl(emoji.file_id)}
alt=""
loading="lazy"
className="h-6 w-6 shrink-0"
/>
<span className="truncate">{customEmojiShortcode(emoji.name)}</span>
{emoji.animated ? (
<span className="ml-auto shrink-0 text-[10px] uppercase text-fg-muted">
{t('chat.emoji.animated')}
</span>
) : null}
</button>
</li>
))}
</ul>
)}
</div>
)}
<div
role="group"
aria-label={t('chat.emoji.popular')}