feat: implement Discord-like stream widget system with separate tiles

Streams now appear as separate tiles in the voice grid alongside the
user's camera/avatar tile, matching Discord's model. Each stream tile
has independent volume, mute, watch/unwatch controls, quality badges,
and stream attenuation that ducks audio when someone speaks.
This commit is contained in:
Jannis Braun
2026-02-20 03:59:28 +01:00
parent a8656e6a3b
commit 1aa40cca15
23 changed files with 2216 additions and 482 deletions
@@ -21,9 +21,12 @@ interface SelectedStream {
function selectPipStream(
participants: ParticipantInfo[],
focusedId: string | null,
watchingStreams: Set<string>,
): SelectedStream | null {
// Priority 1: Screen share (highest value content)
const screenSharer = participants.find(p => p.screenTrack !== null);
// Priority 1: Screen share from a user we're watching
const screenSharer = participants.find(
p => p.screenTrack !== null && watchingStreams.has(p.userId),
);
if (screenSharer?.screenTrack) {
return { participant: screenSharer, track: screenSharer.screenTrack, type: 'screen' };
}
@@ -61,6 +64,7 @@ export function PictureInPicture() {
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
const participants = useVoiceStore((s) => s.participants);
const focusedParticipantId = useVoiceStore((s) => s.focusedParticipantId);
const watchingStreams = useVoiceStore((s) => s.watchingStreams);
const currentChannelId = useChatStore((s) => s.currentChannelId);
const voiceFullscreen = useUIStore((s) => s.voiceFullscreen);
const pipCollapsed = useUIStore((s) => s.pipCollapsed);
@@ -95,8 +99,8 @@ export function PictureInPicture() {
// Stream selection
const selectedStream = useMemo(
() => selectPipStream(participants, focusedParticipantId),
[participants, focusedParticipantId],
() => selectPipStream(participants, focusedParticipantId, watchingStreams),
[participants, focusedParticipantId, watchingStreams],
);
// Fallback participant for avatar (most relevant remote, or first participant)