fix(install): LiveKit перезапускается при изменении конфига
LiveKit читает livekit.yaml только при старте, а compose не замечает правок в примонтированном файле — из-за этого вебхуки не включались после обновления. Установщик сравнивает sha256 конфига с сохранённым и перезапускает сервис.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useCallback, useRef, useState, type DragEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { VoiceFlags, VoiceQualityDot, speakingRingClass } from '@/components/voice/VoiceIndicators';
|
||||
import { VoiceOccupantMenu } from '@/components/voice/VoiceOccupantMenu';
|
||||
import {
|
||||
useVoiceChannels,
|
||||
useVoiceModeration,
|
||||
type VoiceOccupant,
|
||||
} from '@/components/voice/useVoiceOccupants';
|
||||
import { VOICE_DRAG_MIME } from '@/components/voice/voiceDrag';
|
||||
import { useVoiceConnectionStore } from '@/stores/voiceConnection';
|
||||
|
||||
/**
|
||||
* Строка участника голосовой комнаты в сайдбаре: аватар с кольцом речи,
|
||||
* имя, флаги и точка качества связи. Клик открывает меню с громкостью и
|
||||
* действиями модератора; при праве MOVE_MEMBERS строку можно перетащить
|
||||
* на другую голосовую комнату.
|
||||
*/
|
||||
export function VoiceOccupantRow({
|
||||
occupant,
|
||||
guildId,
|
||||
channelId,
|
||||
}: {
|
||||
occupant: VoiceOccupant;
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const permissions = useVoiceModeration(guildId);
|
||||
const moveTargets = useVoiceChannels(guildId).filter((channel) => channel.id !== channelId);
|
||||
const setParticipantMuted = useVoiceConnectionStore((state) => state.setParticipantMuted);
|
||||
const setParticipantDeafened = useVoiceConnectionStore((state) => state.setParticipantDeafened);
|
||||
const moveParticipant = useVoiceConnectionStore((state) => state.moveParticipant);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
||||
const close = useCallback(() => setOpen(false), []);
|
||||
const label = t('voice.menu.label', { name: occupant.name });
|
||||
const canDrag = permissions.move && !occupant.isLocal;
|
||||
|
||||
const onDragStart = (event: DragEvent<HTMLButtonElement>): void => {
|
||||
event.dataTransfer.setData(VOICE_DRAG_MIME, occupant.userId);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="flex items-center">
|
||||
<button
|
||||
ref={anchorRef}
|
||||
type="button"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
draggable={canDrag}
|
||||
onDragStart={canDrag ? onDragStart : undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setOpen((value) => !value);
|
||||
}}
|
||||
className="flex min-w-0 flex-1 items-center gap-1.5 rounded-[var(--radius-sm)] px-2 py-0.5 text-left text-xs text-fg-muted transition-colors duration-[var(--duration-fast)] hover:bg-surface-3 hover:text-fg"
|
||||
>
|
||||
<span className={`shrink-0 rounded-full ${speakingRingClass(occupant.isSpeaking)}`}>
|
||||
<Avatar
|
||||
name={occupant.name}
|
||||
seed={occupant.userId}
|
||||
fileId={occupant.avatarFileId}
|
||||
size="sm"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{occupant.name}</span>
|
||||
{occupant.isSpeaking ? <span className="sr-only">{t('voice.speaking')}</span> : null}
|
||||
<VoiceFlags
|
||||
className="shrink-0"
|
||||
selfMute={occupant.selfMute}
|
||||
selfDeaf={occupant.selfDeaf}
|
||||
serverMute={occupant.serverMute}
|
||||
serverDeaf={occupant.serverDeaf}
|
||||
camera={occupant.camera}
|
||||
screen={occupant.screen}
|
||||
/>
|
||||
<VoiceQualityDot quality={occupant.quality} />
|
||||
</button>
|
||||
|
||||
<VoiceOccupantMenu
|
||||
open={open}
|
||||
occupant={occupant}
|
||||
anchorRef={anchorRef}
|
||||
onClose={close}
|
||||
permissions={permissions}
|
||||
moveTargets={moveTargets}
|
||||
onMute={(userId, muted) => void setParticipantMuted(userId, muted)}
|
||||
onDeafen={(userId, deafened) => void setParticipantDeafened(userId, deafened)}
|
||||
onMove={(userId, targetId) => void moveParticipant(userId, targetId)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user