(feat): add basic call ui

(feat): add screensharing (incl. broken desktop picker)
(qol): add todo
This commit is contained in:
Alois 2026-04-30 22:41:49 +02:00
commit fbd8ef4fa0
21 changed files with 1167 additions and 380 deletions

View file

@ -0,0 +1,59 @@
import { useTTP } from "@tensamin/ttp";
import {
Button,
Popover,
PopoverContent,
PopoverTrigger,
} from "@tensamin/ui";
import Wrapper from "@tensamin/user/wrapper";
import { Mail } from "lucide-react";
import { sendCallInvite } from "../../store";
import { log, toast } from "@tensamin/shared/log";
export default function InviteButton({
className,
iconSize,
}: {
className?: string;
iconSize?: number;
}) {
const { contacts } = useTTP();
return (
<Popover>
<PopoverTrigger
render={
<Button className={className}>
<Mail style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }} />
</Button>
}
/>
<PopoverContent className="flex w-40 flex-col gap-0.5 p-1!">
{contacts.map((contact) => (
<Wrapper
key={contact.user_id}
userId={contact.user_id}
loading={<div>Loading...</div>}
component={(user) => (
<Button
className="w-full justify-start"
variant="ghost"
onClick={() => {
void sendCallInvite(contact.user_id).catch((err) => {
toast(
"error",
"Failed to send call invite. Check console for details.",
);
log(1, "call", "red", "Failed to send call invite", err);
});
}}
>
{user.display}
</Button>
)}
/>
))}
</PopoverContent>
</Popover>
);
}

View file

@ -2,9 +2,9 @@ import { useState } from "react";
import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
import { MonitorDot, ScreenShare } from "lucide-react";
import { toast } from "@tensamin/shared/log";
import { useDesktopMedia } from "@tensamin/tauri/context";
import { isTauri } from "@tauri-apps/api/core";
import { setScreenShareEnabled, useCall } from "../../store";
import ScreenShareDialog from "./screenshareDialog";
import ScreenShareDialog from "../screenshareDialog";
export default function ScreenshareButton({
className,
@ -16,7 +16,6 @@ export default function ScreenshareButton({
const isScreensharing = useCall((state) => state.screenShareEnabled);
const [dialogOpen, setDialogOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const { isDesktopTauri } = useDesktopMedia();
async function startWebShare() {
try {
@ -67,13 +66,13 @@ export default function ScreenshareButton({
</Button>
}
/>
<PopoverContent className="flex w-52 flex-col gap-2">
<PopoverContent className="flex w-40 flex-col gap-2">
<Button
disabled={isScreensharing}
onClick={() => {
setMenuOpen(false);
if (isDesktopTauri) {
if (isTauri()) {
setDialogOpen(true);
return;
}
@ -81,7 +80,7 @@ export default function ScreenshareButton({
void startWebShare();
}}
>
Start sharing
Start screenshare
</Button>
<Button
variant="destructive"
@ -91,15 +90,13 @@ export default function ScreenshareButton({
void stopShare();
}}
>
Stop sharing
</Button>
<Button disabled variant="outline">
Stream quality soon
Stop screenshare
</Button>
<Button variant="outline">Change quality</Button>
</PopoverContent>
</Popover>
{isDesktopTauri && (
{isTauri() && (
<ScreenShareDialog
open={dialogOpen}
onOpenChange={setDialogOpen}

View file

@ -1,325 +0,0 @@
import { useEffect, useState } from "react";
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Switch,
} from "@tensamin/ui";
import {
type DesktopScreenShareAudioOutput,
type DesktopScreenShareCapabilities,
type DesktopScreenShareSource,
useDesktopMedia,
} from "@tensamin/tauri/context";
import { toast } from "@tensamin/shared/log";
import { AppWindow, Loader2, MonitorUp } from "lucide-react";
import type { ScreenShareCaptureOptions } from "livekit-client";
import {
setScreenShareEnabled,
startLinuxDesktopScreenShare,
} from "../../store";
const NONE_AUDIO_OUTPUT = "__none__";
function buildScreenShareOptions(
source: DesktopScreenShareSource,
capabilities: DesktopScreenShareCapabilities,
selectedAudioOutputId: string,
shareAudio: boolean,
): ScreenShareCaptureOptions {
const wantsAudio = capabilities.usesPipeWireAudioPicker
? selectedAudioOutputId !== NONE_AUDIO_OUTPUT
: capabilities.supportsReliableSystemAudio && shareAudio;
return {
audio: wantsAudio
? {
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false,
}
: false,
video: {
displaySurface: source.kind === "window" ? "window" : "monitor",
},
systemAudio: wantsAudio ? "include" : "exclude",
surfaceSwitching: "exclude",
selfBrowserSurface: "exclude",
contentHint: "detail",
};
}
export default function ScreenShareDialog({
open,
onOpenChange,
isScreensharing,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
isScreensharing: boolean;
}) {
const {
getScreenShareCapabilities,
listScreenShareAudioOutputs,
listScreenShareSources,
} = useDesktopMedia();
const [loading, setLoading] = useState(false);
const [sources, setSources] = useState<DesktopScreenShareSource[]>([]);
const [audioOutputs, setAudioOutputs] = useState<DesktopScreenShareAudioOutput[]>(
[],
);
const [capabilities, setCapabilities] =
useState<DesktopScreenShareCapabilities | null>(null);
const [selectedSourceId, setSelectedSourceId] = useState<string | null>(null);
const [selectedAudioOutputId, setSelectedAudioOutputId] =
useState<string>(NONE_AUDIO_OUTPUT);
const [shareAudio, setShareAudio] = useState(false);
useEffect(() => {
if (!open) {
return;
}
let active = true;
setLoading(true);
setSelectedSourceId(null);
setSelectedAudioOutputId(NONE_AUDIO_OUTPUT);
setShareAudio(false);
Promise.all([listScreenShareSources(), getScreenShareCapabilities()])
.then(async ([nextSources, nextCapabilities]) => {
if (!active) {
return;
}
setSources(nextSources);
setCapabilities(nextCapabilities);
if (nextCapabilities.usesPipeWireAudioPicker) {
const nextOutputs = await listScreenShareAudioOutputs();
if (!active) {
return;
}
setAudioOutputs(nextOutputs);
} else {
setAudioOutputs([]);
}
})
.catch((error) => {
console.error("Failed to load desktop share sources", error);
toast("error", "Failed to load screen share sources.");
})
.finally(() => {
if (active) {
setLoading(false);
}
});
return () => {
active = false;
};
}, [
getScreenShareCapabilities,
listScreenShareAudioOutputs,
listScreenShareSources,
open,
]);
const selectedSource =
sources.find((source) => source.id === selectedSourceId) ?? null;
async function startSharing() {
if (!selectedSource || !capabilities) {
return;
}
setLoading(true);
try {
if (capabilities.platform === "linux") {
await startLinuxDesktopScreenShare(selectedSource.id);
} else {
await setScreenShareEnabled(
true,
buildScreenShareOptions(
selectedSource,
capabilities,
selectedAudioOutputId,
shareAudio,
),
);
}
onOpenChange(false);
} catch (error) {
console.error("Failed to start screen share", error);
toast(
"error",
error instanceof Error
? error.message
: "Failed to start screen sharing.",
);
} finally {
setLoading(false);
}
}
async function stopSharing() {
setLoading(true);
try {
await setScreenShareEnabled(false);
onOpenChange(false);
} catch (error) {
console.error("Failed to stop screen share", error);
toast("error", "Failed to stop screen sharing.");
} finally {
setLoading(false);
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl gap-0 overflow-hidden p-0">
<DialogHeader className="p-4 pb-3">
<DialogTitle>Share your screen</DialogTitle>
<DialogDescription>
Pick the window or display you want to share with the call.
</DialogDescription>
</DialogHeader>
<div className="flex max-h-[75vh] flex-col gap-4 overflow-y-auto px-4 pb-4">
<div className="grid gap-3 sm:grid-cols-2">
{sources.map((source) => {
const selected = source.id === selectedSourceId;
return (
<button
key={source.id}
type="button"
onClick={() => setSelectedSourceId(source.id)}
className={[
"flex min-h-28 flex-col justify-between rounded-xl border p-4 text-left transition-colors",
selected
? "border-primary bg-primary/5"
: "border-border bg-card hover:bg-muted/50",
].join(" ")}
>
<div className="flex items-center gap-2 text-sm font-medium">
{source.kind === "window" ? (
<AppWindow className="size-4" />
) : (
<MonitorUp className="size-4" />
)}
{source.name}
</div>
{source.subtitle && (
<p className="text-xs text-muted-foreground">
{source.subtitle}
</p>
)}
</button>
);
})}
</div>
{!loading && sources.length === 0 && (
<p className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
No windows or displays are currently available for capture.
</p>
)}
{capabilities?.usesPipeWireAudioPicker ? (
<div className="flex flex-col gap-2">
<Label>Share audio output</Label>
<Select
value={selectedAudioOutputId}
onValueChange={(value) =>
setSelectedAudioOutputId(value ?? NONE_AUDIO_OUTPUT)
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="None" />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE_AUDIO_OUTPUT}>None</SelectItem>
{audioOutputs.map((output) => (
<SelectItem key={output.id} value={output.id}>
{output.name}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Select a PipeWire output to try sharing system audio with your
screen.
</p>
</div>
) : capabilities?.supportsSystemAudioSwitch ? (
<div className="flex flex-col gap-2 rounded-xl border p-4">
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="share-system-audio">Share audio</Label>
<p className="text-xs text-muted-foreground">
Share system audio alongside your screen when the runtime can
provide it.
</p>
</div>
<Switch
id="share-system-audio"
checked={shareAudio}
disabled={!capabilities.supportsReliableSystemAudio}
onCheckedChange={setShareAudio}
/>
</div>
{!capabilities.supportsReliableSystemAudio && (
<p className="text-xs text-muted-foreground">
System audio sharing is not available on this platform/runtime
yet.
</p>
)}
</div>
) : null}
{loading && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
Loading available share targets...
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
{isScreensharing && (
<Button variant="destructive" disabled={loading} onClick={stopSharing}>
Stop sharing
</Button>
)}
<Button
disabled={loading || selectedSource == null}
onClick={startSharing}
>
{loading ? <Loader2 className="size-4 animate-spin" /> : null}
Start sharing
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}