From 6fda7f1c71bea4ce9c63aac197ac0c97cdb02d77 Mon Sep 17 00:00:00 2001 From: Alois Date: Mon, 1 Jun 2026 14:13:43 +0200 Subject: [PATCH] (feat): replace toast with popup for call invites (qol): update todo --- apps/web/src/index.tsx | 4 +- packages/call/src/components/invitePopup.tsx | 58 +++++++++++++++ packages/call/src/store.tsx | 77 ++++++++++++++------ packages/chat/todo.md | 1 - 4 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 packages/call/src/components/invitePopup.tsx diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 7ea7afc..6487ba1 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -163,9 +163,7 @@ function AppShell() { } function CallInit() { - useInitializeCall(); - - return null; + return useInitializeCall(); } const rootRoute = createRootRoute({ diff --git a/packages/call/src/components/invitePopup.tsx b/packages/call/src/components/invitePopup.tsx new file mode 100644 index 0000000..f531913 --- /dev/null +++ b/packages/call/src/components/invitePopup.tsx @@ -0,0 +1,58 @@ +import { + Avatar, + AvatarFallback, + AvatarImage, + Button, + Dialog, + DialogContent, +} from "@tensamin/ui"; +import Wrapper from "@tensamin/user/wrapper"; +import { PhoneIncoming, PhoneMissed } from "lucide-react"; + +export default function InvitePopup({ + open, + setOpen, + onAccept, + user, +}: { + open: boolean; + setOpen: (value: boolean) => void; + onAccept: (value: boolean) => void; + user: number; +}) { + return ( + ( + + + + + + {user.display.slice(0, 2).toUpperCase()} + + +

{user.display}

+
+ + +
+
+
+ )} + /> + ); +} diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 0d28c61..e2595b6 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -24,7 +24,6 @@ import { getLogger, } from "livekit-client"; import z from "zod"; -import { toast as sonnerToast } from "sonner"; import { createScreenShareController, type ScreenShareSession, @@ -33,6 +32,7 @@ import { getSpeakingDetector, disposeSpeakingDetector, } from "./speakingIndicator"; +import InvitePopup from "./components/invitePopup"; // logging setLogExtension( @@ -45,6 +45,11 @@ setLogExtension( type CallState = "closed" | "closing" | "connecting" | "open" | "encrypting"; type CallView = "preview" | "focused" | "grid"; +type IncomingCallInvite = { + callId: string; + callSecret: string; + senderId: number; +}; type CurrentCallData = | (z.infer & { exists: boolean }) | null; @@ -83,6 +88,7 @@ type CallStore = { view: CallView; invitedUserId: number | null; callId: string | null; + incomingCallInvite: IncomingCallInvite | null; callSecret: string | null; livekitToken: string | null; currentCallData: CurrentCallData; @@ -837,6 +843,7 @@ export async function disconnect() { state: "closing", invitedUserId: null, callId: null, + incomingCallInvite: null, callSecret: null, livekitToken: null, currentCallData: null, @@ -999,6 +1006,7 @@ export function resetCallState() { view: "preview", invitedUserId: null, callId: null, + incomingCallInvite: null, callSecret: null, livekitToken: null, currentCallData: null, @@ -1050,6 +1058,7 @@ export const useCall = create(() => ({ view: "preview", invitedUserId: null, callId: null, + incomingCallInvite: null, callSecret: null, livekitToken: null, currentCallData: null, @@ -1084,6 +1093,7 @@ export function useInitializeCall() { const callId = useCall((state) => state.callId); const view = useCall((state) => state.view); + const incomingCallInvite = useCall((state) => state.incomingCallInvite); const listenersRegistered = useRef(false); const noiseFilter = useMemo( @@ -1113,32 +1123,44 @@ export function useInitializeCall() { }, [decryptText, encryptText, get, getSharedSecret, load, navigate, send]); const showCallingScreen = useCallback( - async (callId: string, callSecret: string, senderId: number) => { - const senderName = await get(senderId).then((data) => data.display); - - sonnerToast(`Incoming call from ${senderName}`, { - action: { - label: "Accept", - onClick: () => { - joinCall(senderId, callSecret, callId, false).catch((err) => { - log(1, "call", "red", "Failed to join call", err); - }); - }, - }, - cancel: { - label: "Decline", - onClick: () => { - log(2, "call", "purple", "Declined call invite", { - callId, - senderId, - }); - }, - }, + (callId: string, callSecret: string, senderId: number) => { + useCall.setState({ + incomingCallInvite: { callId, callSecret, senderId }, }); }, - [get], + [], ); + const setInvitePopupOpen = useCallback((open: boolean) => { + if (!open) { + useCall.setState({ incomingCallInvite: null }); + } + }, []); + + const respondToInvite = useCallback((accepted: boolean) => { + const invite = useCall.getState().incomingCallInvite; + + useCall.setState({ incomingCallInvite: null }); + + if (!invite) { + return; + } + + if (accepted) { + joinCall(invite.senderId, invite.callSecret, invite.callId, false).catch( + (err) => { + log(1, "call", "red", "Failed to join call", err); + }, + ); + return; + } + + log(2, "call", "purple", "Declined call invite", { + callId: invite.callId, + senderId: invite.senderId, + }); + }, []); + // listen to call invites useEffect(() => { subscribePush(async (message) => { @@ -1482,4 +1504,13 @@ export function useInitializeCall() { }); }); }, [callId, send, view]); + + return incomingCallInvite ? ( + + ) : null; } diff --git a/packages/chat/todo.md b/packages/chat/todo.md index 4bd036a..63466f3 100644 --- a/packages/chat/todo.md +++ b/packages/chat/todo.md @@ -1,3 +1,2 @@ - Implement context menu features - Add default-emoji-hotkey in settings -- On mobile, turn ContextMenu into Drawer