fix(install): публичный адрес LiveKit вычисляется после TLS_ENABLED
Установщик падал на 'TLS_ENABLED: unbound variable': расчёт LIVEKIT_PUBLIC_URL стоял выше определения TLS_ENABLED. Значение перенесено после блока TLS, dry-run проходит.
This commit is contained in:
@@ -9,19 +9,25 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { fileContentUrl } from '@/api/files';
|
||||
import { MESSAGE_MAX_LENGTH, messageTypeOf, type Message, type MessageType } from '@/api/messages';
|
||||
import { CustomEmojiImage } from '@/components/chat/CustomEmojiImage';
|
||||
import { EmojiPicker } from '@/components/chat/EmojiPicker';
|
||||
import { MessageContent } from '@/components/chat/MessageContent';
|
||||
import type { AuthorDirectory } from '@/components/chat/useAuthorDirectory';
|
||||
import { useGuildEmojiDirectory } from '@/components/chat/useGuildEmojis';
|
||||
import { ErrorNotice } from '@/components/ui/ErrorNotice';
|
||||
import { Menu, MenuItem, MenuSeparator } from '@/components/ui/Menu';
|
||||
import { QUICK_REACTIONS, copyText, formatMessageDateTime, formatMessageTime } from '@/lib/chat';
|
||||
import { customEmojiShortcode } from '@/lib/customEmoji';
|
||||
import { formatBytes } from '@/lib/format';
|
||||
import { parseSystemMessage } from '@/lib/systemMessage';
|
||||
import { useMessagesStore } from '@/stores/messages';
|
||||
|
||||
interface MessageItemProps {
|
||||
message: Message;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
/** Сервер комнаты: из него пикер берёт кастомные эмодзи. */
|
||||
guildId?: string | null;
|
||||
authors: AuthorDirectory;
|
||||
currentUserId: string | null;
|
||||
canManageMessages: boolean;
|
||||
@@ -140,6 +146,62 @@ function MessageBody({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Системное сообщение: сервер присылает JSON о событии, лента показывает его
|
||||
* человекочитаемой строкой через i18n. Имя участника резолвит тот же
|
||||
* справочник авторов; если пользователь неизвестен (вышел, удалён) —
|
||||
* подставляем «участник». Неизвестное событие и мусорный JSON показываем как
|
||||
* есть: ничего не теряем и не падаем.
|
||||
*/
|
||||
function SystemMessageBody({
|
||||
message,
|
||||
authors,
|
||||
}: {
|
||||
message: Message;
|
||||
authors: AuthorDirectory;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const event = parseSystemMessage(message.content);
|
||||
|
||||
if (event === null) {
|
||||
return (
|
||||
<MessageContent
|
||||
content={message.content}
|
||||
resolveMention={authors.resolveMention}
|
||||
className="text-xs text-fg-muted"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const nameOf = (userId: string): string =>
|
||||
authors.resolveMemberName?.(userId) ?? t('chat.system.member');
|
||||
|
||||
if (event.kind === 'member_join') {
|
||||
return (
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t('chat.system.memberJoin', { name: nameOf(event.userId) })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (event.kind === 'member_kick') {
|
||||
return (
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t('chat.system.memberKick', { name: nameOf(event.userId) })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t(
|
||||
event.channelType === 'voice'
|
||||
? 'chat.system.voiceChannelCreated'
|
||||
: 'chat.system.channelCreated',
|
||||
{ name: event.name },
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Сообщение ленты: текст, вложения, реакции, ответы и действия.
|
||||
*
|
||||
@@ -150,6 +212,7 @@ export function MessageItem({
|
||||
message,
|
||||
channelId,
|
||||
channelName,
|
||||
guildId = null,
|
||||
authors,
|
||||
currentUserId,
|
||||
canManageMessages,
|
||||
@@ -162,6 +225,8 @@ export function MessageItem({
|
||||
onJumpToReply,
|
||||
}: MessageItemProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
// Кастомные эмодзи: реакции и пикер показывают их картинкой.
|
||||
const emojis = useGuildEmojiDirectory();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
@@ -281,11 +346,7 @@ export function MessageItem({
|
||||
>
|
||||
<span className="sr-only">{t('chat.types.system')}</span>
|
||||
{message.content === '' ? null : (
|
||||
<MessageContent
|
||||
content={message.content}
|
||||
resolveMention={authors.resolveMention}
|
||||
className="text-xs text-fg-muted"
|
||||
/>
|
||||
<SystemMessageBody message={message} authors={authors} />
|
||||
)}
|
||||
<time dateTime={message.created_at} className="text-[10px] text-fg-muted/70">
|
||||
{formatMessageTime(message.created_at, i18n.language)}
|
||||
@@ -408,27 +469,36 @@ export function MessageItem({
|
||||
|
||||
{message.reactions.length === 0 ? null : (
|
||||
<ul className="flex flex-wrap gap-1" aria-label={t('chat.reactions.title')}>
|
||||
{message.reactions.map((reaction) => (
|
||||
<li key={reaction.emoji}>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={reaction.me}
|
||||
aria-label={t('chat.reactions.toggle', {
|
||||
emoji: reaction.emoji,
|
||||
count: reaction.count,
|
||||
})}
|
||||
onClick={() => react(reaction.emoji)}
|
||||
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs transition-colors duration-[var(--duration-fast)] ${
|
||||
reaction.me
|
||||
? 'border-accent/60 bg-accent/15 text-fg'
|
||||
: 'border-border/60 bg-surface-2 text-fg-muted hover:bg-surface-3'
|
||||
}`}
|
||||
>
|
||||
<span aria-hidden="true">{reaction.emoji}</span>
|
||||
<span>{reaction.count}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{message.reactions.map((reaction) => {
|
||||
// Кастомный эмодзи едет токеном `<:имя:file_id>`: показываем
|
||||
// картинку, а в подписи — читаемое `:имя:`.
|
||||
const custom = emojis.resolve(reaction.emoji);
|
||||
return (
|
||||
<li key={reaction.emoji}>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={reaction.me}
|
||||
aria-label={t('chat.reactions.toggle', {
|
||||
emoji: custom === null ? reaction.emoji : customEmojiShortcode(custom.name),
|
||||
count: reaction.count,
|
||||
})}
|
||||
onClick={() => react(reaction.emoji)}
|
||||
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs transition-colors duration-[var(--duration-fast)] ${
|
||||
reaction.me
|
||||
? 'border-accent/60 bg-accent/15 text-fg'
|
||||
: 'border-border/60 bg-surface-2 text-fg-muted hover:bg-surface-3'
|
||||
}`}
|
||||
>
|
||||
{custom === null ? (
|
||||
<span aria-hidden="true">{reaction.emoji}</span>
|
||||
) : (
|
||||
<CustomEmojiImage emoji={custom} className="h-4 w-4" />
|
||||
)}
|
||||
<span>{reaction.count}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
@@ -506,6 +576,7 @@ export function MessageItem({
|
||||
onClose={() => setPickerOpen(false)}
|
||||
onPick={react}
|
||||
placement={dropUp ? 'top' : 'bottom'}
|
||||
guildId={guildId}
|
||||
/>
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
|
||||
Reference in New Issue
Block a user