(feat): save deafen state in participant metadata

(feat): move admin check to room metadata
This commit is contained in:
Alois 2026-05-01 00:27:21 +02:00
commit c39185c200
2 changed files with 23 additions and 68 deletions

View file

@ -6,7 +6,7 @@ import {
} from "@tensamin/ui"; } from "@tensamin/ui";
import { import {
focusParticipant, focusParticipant,
getParsedParticipantMetadata, getRoomMetadata,
setCallView, setCallView,
useCall, useCall,
} from "../../store"; } from "../../store";
@ -14,7 +14,7 @@ import { Track, type Participant } from "livekit-client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useUser, type User } from "@tensamin/user/context"; import { useUser, type User } from "@tensamin/user/context";
import VideoViewer from "../videoViewer"; import VideoViewer from "../videoViewer";
import { Loader2, MicOff, Monitor, Shield } from "lucide-react"; import { HeadphoneOff, Loader2, MicOff, Monitor, Shield } from "lucide-react";
function TransparentButton({ children }: { children: React.ReactNode }) { function TransparentButton({ children }: { children: React.ReactNode }) {
return ( return (
@ -33,9 +33,8 @@ function Overlay({
user: User; user: User;
participant: Participant; participant: Participant;
}) { }) {
const { isAdmin } = getParsedParticipantMetadata(user.user_id) || { const isAdmin = getRoomMetadata()?.admin === user.user_id;
isAdmin: false, const isDeafened = participant.attributes["deafened"] === "true";
};
return ( return (
<div className="w-full h-auto flex items-center gap-1"> <div className="w-full h-auto flex items-center gap-1">
@ -46,6 +45,11 @@ function Overlay({
<MicOff className="size-3.5" /> <MicOff className="size-3.5" />
</TransparentButton> </TransparentButton>
)} )}
{isDeafened && (
<TransparentButton>
<HeadphoneOff className="size-3.5" />
</TransparentButton>
)}
{isAdmin && ( {isAdmin && (
<TransparentButton> <TransparentButton>
<Shield className="size-3.5" /> <Shield className="size-3.5" />

View file

@ -70,10 +70,6 @@ type Runtime = {
getUser: GetUserFn; getUser: GetUserFn;
}; };
type ParticipantMetadata = {
isAdmin: boolean;
};
type CallStore = { type CallStore = {
state: CallState; state: CallState;
view: CallView; view: CallView;
@ -90,7 +86,6 @@ type CallStore = {
watchedStreamParticipantIds: number[]; watchedStreamParticipantIds: number[];
pendingWatchedParticipantIds: number[]; pendingWatchedParticipantIds: number[];
activeScreenShareParticipantIds: number[]; activeScreenShareParticipantIds: number[];
parsedParticipantMetadataById: Record<number, ParticipantMetadata>;
isEncrypted: boolean; isEncrypted: boolean;
room: Room; room: Room;
keyProvider: ExternalE2EEKeyProvider; keyProvider: ExternalE2EEKeyProvider;
@ -186,51 +181,6 @@ function hasParticipant(participantId: number) {
); );
} }
function parseParticipantMetadata(
metadata: string | undefined,
): ParticipantMetadata | undefined {
if (!metadata) {
return undefined;
}
try {
return JSON.parse(metadata);
} catch {
return undefined;
}
}
function getParsedParticipantMetadataById(): Record<
number,
ParticipantMetadata
> {
return Object.fromEntries(
getAllParticipants()
.map((participant) => {
const participantId = getParticipantId(participant.identity);
if (participantId == null) {
return null;
}
return [
participantId,
parseParticipantMetadata(participant.metadata),
] as const;
})
.filter(
(entry): entry is readonly [number, ParticipantMetadata] =>
entry != null,
),
);
}
function syncParsedParticipantMetadata() {
useCall.setState({
parsedParticipantMetadataById: getParsedParticipantMetadataById(),
});
}
function syncScreenShareParticipants() { function syncScreenShareParticipants() {
const activeScreenShareParticipantIds = getActiveScreenShareParticipantIds(); const activeScreenShareParticipantIds = getActiveScreenShareParticipantIds();
const state = useCall.getState(); const state = useCall.getState();
@ -268,6 +218,13 @@ function requireRuntime(runtime: Runtime | null): Runtime {
return runtime; return runtime;
} }
export function getRoomMetadata() {
const roomMetadata = room.metadata;
return JSON.parse(roomMetadata || '{"admin": 0}').catch(() => ({
admin: 0,
})) as { admin: number };
}
// Sync local participant flags and screen-share derived state for the active call UI. // Sync local participant flags and screen-share derived state for the active call UI.
export function syncParticipantState() { export function syncParticipantState() {
const { room, screenShareSession } = useCall.getState(); const { room, screenShareSession } = useCall.getState();
@ -281,13 +238,6 @@ export function syncParticipantState() {
}); });
syncScreenShareParticipants(); syncScreenShareParticipants();
syncParsedParticipantMetadata();
}
export function getParsedParticipantMetadata(
participantId: number,
): ParticipantMetadata | undefined {
return useCall.getState().parsedParticipantMetadataById[participantId];
} }
// set state functions // set state functions
@ -525,7 +475,6 @@ export async function disconnect() {
watchedStreamParticipantIds: [], watchedStreamParticipantIds: [],
pendingWatchedParticipantIds: [], pendingWatchedParticipantIds: [],
activeScreenShareParticipantIds: [], activeScreenShareParticipantIds: [],
parsedParticipantMetadataById: {},
}); });
room.remoteParticipants.forEach((participant) => { room.remoteParticipants.forEach((participant) => {
@ -604,7 +553,7 @@ export async function joinCall(
} }
// Mute or restore incoming call audio for every remote participant. // Mute or restore incoming call audio for every remote participant.
export function toggleDeaf() { export async function toggleDeaf() {
const nextDeaf = !useCall.getState().deaf; const nextDeaf = !useCall.getState().deaf;
room.remoteParticipants.forEach((participant) => { room.remoteParticipants.forEach((participant) => {
@ -612,9 +561,13 @@ export function toggleDeaf() {
}); });
if (nextDeaf && room.localParticipant.isMicrophoneEnabled) { if (nextDeaf && room.localParticipant.isMicrophoneEnabled) {
toggleMute(); await toggleMute();
} }
await room.localParticipant.setAttributes({
deafened: nextDeaf ? "true" : "false",
});
useCall.setState({ deaf: nextDeaf }); useCall.setState({ deaf: nextDeaf });
} }
@ -623,7 +576,7 @@ export async function toggleMute() {
const micEnabled = useCall.getState().micEnabled; const micEnabled = useCall.getState().micEnabled;
if (!micEnabled && useCall.getState().deaf) { if (!micEnabled && useCall.getState().deaf) {
toggleDeaf(); await toggleDeaf();
} }
await room.localParticipant.setMicrophoneEnabled(!micEnabled); await room.localParticipant.setMicrophoneEnabled(!micEnabled);
@ -669,7 +622,6 @@ export function resetCallState() {
watchedStreamParticipantIds: [], watchedStreamParticipantIds: [],
pendingWatchedParticipantIds: [], pendingWatchedParticipantIds: [],
activeScreenShareParticipantIds: [], activeScreenShareParticipantIds: [],
parsedParticipantMetadataById: {},
}); });
syncParticipantState(); syncParticipantState();
@ -711,7 +663,6 @@ export const useCall = create<CallStore>(() => ({
watchedStreamParticipantIds: [], watchedStreamParticipantIds: [],
pendingWatchedParticipantIds: [], pendingWatchedParticipantIds: [],
activeScreenShareParticipantIds: [], activeScreenShareParticipantIds: [],
parsedParticipantMetadataById: {},
isEncrypted: isEncrypted:
room.localParticipant.isE2EEEnabled && room.localParticipant.isEncrypted, room.localParticipant.isE2EEEnabled && room.localParticipant.isEncrypted,
room, room,