(feat): add admin indicator
This commit is contained in:
parent
4dbf48b3f9
commit
c7313da53f
3 changed files with 83 additions and 8 deletions
|
|
@ -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 ${
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,5 @@
|
||||||
- Preview image
|
- Preview image
|
||||||
- Mobile
|
- Mobile
|
||||||
- Deaf status is not synced
|
- Deaf status is not synced
|
||||||
|
- Call invite popup
|
||||||
|
- Sounds
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue