(feat): add basic call ui
(feat): add screensharing (incl. broken desktop picker) (qol): add todo
This commit is contained in:
parent
7b5cdca0ff
commit
fbd8ef4fa0
21 changed files with 1167 additions and 380 deletions
165
packages/call/src/components/modals/base.tsx
Normal file
165
packages/call/src/components/modals/base.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// show speaking ring
|
||||
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@tensamin/ui";
|
||||
import { focusParticipant, setCallView, useCall } from "../../store";
|
||||
import { type Participant } from "livekit-client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser, type User } from "@tensamin/user/context";
|
||||
import VideoViewer from "../videoViewer";
|
||||
import { Loader2, MicOff, Monitor } from "lucide-react";
|
||||
|
||||
function TransparentButton({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="h-6 px-1.5 bg-black/80 rounded-sm flex items-center justify-center">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Overlay({
|
||||
type,
|
||||
user,
|
||||
participant,
|
||||
}: {
|
||||
type: "user" | "stream";
|
||||
user: User;
|
||||
participant: Participant;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full h-auto flex items-center gap-2">
|
||||
{type === "user" && (
|
||||
<>
|
||||
{!participant.isMicrophoneEnabled && (
|
||||
<TransparentButton>
|
||||
<MicOff className="size-3.5" />
|
||||
</TransparentButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{type === "stream" && (
|
||||
<TransparentButton>
|
||||
<Monitor className="size-3.5" />
|
||||
</TransparentButton>
|
||||
)}
|
||||
<TransparentButton>
|
||||
<p className="text-sm">{user.display}</p>
|
||||
</TransparentButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Base({
|
||||
participant,
|
||||
type,
|
||||
flush = false,
|
||||
}: {
|
||||
participant: Participant | undefined;
|
||||
type: "user" | "stream";
|
||||
flush?: boolean;
|
||||
}) {
|
||||
const { get } = useUser();
|
||||
|
||||
const focusedParticipantId = useCall((state) => state.focusedParticipantId);
|
||||
const view = useCall((state) => state.view);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const participantId = Number(participant?.identity);
|
||||
|
||||
if (
|
||||
!participant ||
|
||||
!Number.isInteger(participantId) ||
|
||||
participantId <= 0
|
||||
) {
|
||||
// eslint-disable-next-line
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
|
||||
void get(participantId).then((nextUser) => {
|
||||
if (active) {
|
||||
setUser(nextUser);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [participant, get]);
|
||||
|
||||
if (!participant || !user) {
|
||||
return (
|
||||
<div
|
||||
className={`aspect-video w-full animate-pulse bg-muted ${
|
||||
flush ? "rounded-none" : "rounded-md"
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
if (view === "grid") {
|
||||
focusParticipant(user.user_id);
|
||||
} else {
|
||||
if (user.user_id === focusedParticipantId) {
|
||||
setCallView("grid");
|
||||
} else {
|
||||
focusParticipant(user.user_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger
|
||||
render={
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`relative aspect-video w-full border-2 ${
|
||||
flush ? "rounded-none border-x-0" : "rounded-md"
|
||||
}`}
|
||||
>
|
||||
<div className="z-20 absolute bottom-0 left-0 w-full h-full flex justify-start items-end p-2">
|
||||
{view === "grid" && (
|
||||
<Overlay type={type} user={user} participant={participant} />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`z-10 bg-card absolute top-0 left-0 w-full h-full flex items-center justify-center ${
|
||||
flush ? "rounded-none" : "rounded-sm"
|
||||
}`}
|
||||
>
|
||||
{/* Detect video / user and place here */}
|
||||
|
||||
{type === "stream" &&
|
||||
Array.from(participant.videoTrackPublications.values()).map(
|
||||
(publication) =>
|
||||
publication.isSubscribed && publication.track ? (
|
||||
<VideoViewer
|
||||
flush={flush}
|
||||
participantId={participant.identity}
|
||||
publication={publication}
|
||||
/>
|
||||
) : (
|
||||
<Loader2 className="animate-spin" />
|
||||
),
|
||||
)}
|
||||
|
||||
{type === "user" && <p>{user.display}</p>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem>Stop Watching</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue