(feat): add admin indicator

This commit is contained in:
Alois 2026-05-01 00:08:58 +02:00
commit c7313da53f
3 changed files with 83 additions and 8 deletions

View file

@ -1,21 +1,24 @@
// show speaking ring
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@tensamin/ui"; } from "@tensamin/ui";
import { focusParticipant, setCallView, useCall } from "../../store"; import {
focusParticipant,
getParsedParticipantMetadata,
setCallView,
useCall,
} from "../../store";
import { Track, type Participant } from "livekit-client"; 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 } from "lucide-react"; import { Loader2, MicOff, Monitor, Shield } from "lucide-react";
function TransparentButton({ children }: { children: React.ReactNode }) { function TransparentButton({ children }: { children: React.ReactNode }) {
return ( return (
<div className="h-6 px-1.5 bg-black/80 rounded-sm flex items-center justify-center"> <div className="h-6 px-1.5 bg-black/40 backdrop-blur-xs border rounded-sm flex items-center justify-center">
{children} {children}
</div> </div>
); );
@ -30,8 +33,12 @@ function Overlay({
user: User; user: User;
participant: Participant; participant: Participant;
}) { }) {
const { isAdmin } = getParsedParticipantMetadata(user.user_id) || {
isAdmin: false,
};
return ( return (
<div className="w-full h-auto flex items-center gap-2"> <div className="w-full h-auto flex items-center gap-1">
{type === "user" && ( {type === "user" && (
<> <>
{!participant.isMicrophoneEnabled && ( {!participant.isMicrophoneEnabled && (
@ -39,6 +46,11 @@ function Overlay({
<MicOff className="size-3.5" /> <MicOff className="size-3.5" />
</TransparentButton> </TransparentButton>
)} )}
{isAdmin && (
<TransparentButton>
<Shield className="size-3.5" />
</TransparentButton>
)}
</> </>
)} )}
{type === "stream" && ( {type === "stream" && (
@ -130,9 +142,10 @@ export default function Base({
}`} }`}
> >
<div className="z-20 absolute bottom-0 left-0 w-full h-full flex justify-start items-end p-2"> <div className="z-20 absolute bottom-0 left-0 w-full h-full flex justify-start items-end p-2">
{view === "grid" && ( {view === "grid" ||
(view === "focused" && user.user_id !== focusedParticipantId) ? (
<Overlay type={type} user={user} participant={participant} /> <Overlay type={type} user={user} participant={participant} />
)} ) : null}
</div> </div>
<div <div
className={`z-10 bg-card absolute top-0 left-0 w-full h-full flex items-center justify-center ${ className={`z-10 bg-card absolute top-0 left-0 w-full h-full flex items-center justify-center ${

View file

@ -70,6 +70,10 @@ type Runtime = {
getUser: GetUserFn; getUser: GetUserFn;
}; };
type ParticipantMetadata = {
isAdmin: boolean;
};
type CallStore = { type CallStore = {
state: CallState; state: CallState;
view: CallView; view: CallView;
@ -86,6 +90,7 @@ 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;
@ -181,6 +186,51 @@ 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();
@ -231,6 +281,13 @@ export function syncParticipantState() {
}); });
syncScreenShareParticipants(); syncScreenShareParticipants();
syncParsedParticipantMetadata();
}
export function getParsedParticipantMetadata(
participantId: number,
): ParticipantMetadata | undefined {
return useCall.getState().parsedParticipantMetadataById[participantId];
} }
// set state functions // set state functions
@ -468,6 +525,7 @@ export async function disconnect() {
watchedStreamParticipantIds: [], watchedStreamParticipantIds: [],
pendingWatchedParticipantIds: [], pendingWatchedParticipantIds: [],
activeScreenShareParticipantIds: [], activeScreenShareParticipantIds: [],
parsedParticipantMetadataById: {},
}); });
room.remoteParticipants.forEach((participant) => { room.remoteParticipants.forEach((participant) => {
@ -611,6 +669,7 @@ export function resetCallState() {
watchedStreamParticipantIds: [], watchedStreamParticipantIds: [],
pendingWatchedParticipantIds: [], pendingWatchedParticipantIds: [],
activeScreenShareParticipantIds: [], activeScreenShareParticipantIds: [],
parsedParticipantMetadataById: {},
}); });
syncParticipantState(); syncParticipantState();
@ -652,6 +711,7 @@ 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,

View file

@ -8,3 +8,5 @@
- Preview image - Preview image
- Mobile - Mobile
- Deaf status is not synced - Deaf status is not synced
- Call invite popup
- Sounds