Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/internal/messages/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ type CallTokensMessage struct {
type RejectCallMessage struct {
Type MessageType `json:"type"`
Payload struct {
CallerID string `json:"caller_id" validate:"required"`
CallerID string `json:"caller_id" validate:"required"`
RejectReason string `json:"reject_reason,omitempty" validate:"omitempty,oneof=in-call rejected"`
} `json:"payload"`
}

Expand Down
70 changes: 56 additions & 14 deletions tauri/src/components/ui/call-center.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ import { ChevronDownIcon } from "@radix-ui/react-icons";
import { HiOutlinePhoneXMark } from "react-icons/hi2";
import toast from "react-hot-toast";

const Colors = {
deactivatedIcon: "text-slate-600",
deactivatedText: "text-slate-500",
mic: { text: "text-blue-600", icon: "text-blue-600", ring: "ring-blue-600" },
camera: { text: "text-green-600", icon: "text-green-600", ring: "ring-green-600" },
screen: { text: "text-yellow-600", icon: "text-yellow-600", ring: "ring-yellow-600" },
} as const;

export function CallCenter() {
const { callTokens } = useStore();

Expand Down Expand Up @@ -336,17 +344,30 @@ function MicrophoneIcon() {
hasAudioEnabled: !hasAudioEnabled,
});
}}
icon={hasAudioEnabled ? <LuMic className="size-4" /> : <LuMicOff className="size-4" />}
icon={
hasAudioEnabled ?
<LuMic className={`size-4 ${Colors.mic.icon}`} />
: <LuMicOff className={`size-4 ${Colors.deactivatedIcon}`} />
}
state={hasAudioEnabled ? "active" : "neutral"}
size="unsized"
className="flex-1 min-w-0 text-slate-600"
className={clsx("flex-1 min-w-0", {
[Colors.deactivatedText]: !hasAudioEnabled,
[`${Colors.mic.text} ${Colors.mic.ring}`]: hasAudioEnabled,
})}
cornerIcon={
<Select
value={activeMicrophoneDeviceId}
onValueChange={handleMicrophoneChange}
onOpenChange={handleDropdownOpenChange}
>
<SelectTrigger className="hover:outline-solid hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-xs p-0 border-0 shadow-none hover:shadow-xs" />
<SelectTrigger
iconClassName={clsx({
[Colors.mic.text]: hasAudioEnabled,
[Colors.deactivatedIcon]: !hasAudioEnabled,
})}
className="hover:outline-solid hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-xs p-0 border-0 shadow-none hover:shadow-xs"
/>
<SelectPortal container={document.getElementsByClassName("container")[0]}>
<SelectContent align="center">
{microphoneDevices.map((device) => {
Expand All @@ -363,7 +384,7 @@ function MicrophoneIcon() {
</Select>
}
>
{hasAudioEnabled ? "Mute me" : "Mic"}
{hasAudioEnabled ? "Mute me" : "Open mic"}
</ToggleIconButton>
);
}
Expand Down Expand Up @@ -401,24 +422,32 @@ function ScreenShareIcon({
onClick={toggleScreenShare}
icon={
callTokens?.role === ParticipantRole.SHARER ?
<LuScreenShare className="size-4" />
: <LuScreenShareOff className="size-4" />
<LuScreenShare className={`size-4 ${Colors.screen.icon}`} />
: <LuScreenShareOff className={`size-4 ${Colors.deactivatedIcon}`} />
}
state={callTokens?.role === ParticipantRole.SHARER ? "active" : "neutral"}
size="unsized"
className="flex-1 min-w-0 text-slate-600"
className={clsx("flex-1 min-w-0", {
[Colors.deactivatedText]: !(callTokens?.role === ParticipantRole.SHARER),
[`${Colors.screen.text} ${Colors.screen.ring}`]: callTokens?.role === ParticipantRole.SHARER,
})}
cornerIcon={
callTokens?.role === ParticipantRole.SHARER && (
<button
onClick={changeScreenShare}
className="hover:outline-solid hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-xs p-0 border-0 shadow-none hover:shadow-xs"
className="hover:outline-solid hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-sm p-0 border-0 shadow-none hover:shadow-xs"
>
<ChevronDownIcon className="size-3" />
<ChevronDownIcon
className={clsx("size-4", {
[Colors.screen.icon]: callTokens?.role === ParticipantRole.SHARER,
[Colors.deactivatedIcon]: !(callTokens?.role === ParticipantRole.SHARER),
})}
/>
</button>
)
}
>
Screen
{callTokens?.role === ParticipantRole.SHARER ? "Stop sharing" : "Share screen"}
</ToggleIconButton>
);
}
Expand Down Expand Up @@ -611,7 +640,11 @@ function CameraIcon() {
return (
<ToggleIconButton
onClick={handleCameraToggle}
icon={cameraEnabled ? <LuVideo className="size-4" /> : <LuVideoOff className="size-4" />}
icon={
cameraEnabled ?
<LuVideo className={`size-4 ${Colors.camera.icon}`} />
: <LuVideoOff className={`size-4 ${Colors.deactivatedIcon}`} />
}
state={
cameraEnabled ? "active"
: isDisabled ?
Expand All @@ -620,10 +653,19 @@ function CameraIcon() {
}
size="unsized"
disabled={isDisabled}
className="flex-1 min-w-0 text-slate-600"
className={clsx("flex-1 min-w-0", {
[Colors.deactivatedText]: !cameraEnabled,
[`${Colors.camera.text} ${Colors.camera.ring}`]: cameraEnabled,
})}
cornerIcon={
<Select value={activeCameraDeviceId} onValueChange={handleCameraChange} onOpenChange={handleDropdownOpenChange}>
<SelectTrigger className="hover:outline hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-sm p-0 border-0 shadow-none hover:shadow-xs" />
<SelectTrigger
iconClassName={clsx({
[Colors.camera.text]: cameraEnabled,
[Colors.deactivatedIcon]: !cameraEnabled,
})}
className="hover:outline hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-sm p-0 border-0 shadow-none hover:shadow-xs"
/>
<SelectPortal container={document.getElementsByClassName("container")[0]}>
<SelectContent align="center">
{cameraDevices.map((device) => {
Expand All @@ -638,7 +680,7 @@ function CameraIcon() {
</Select>
}
>
Cam
{cameraEnabled ? "Stop sharing" : "Share cam"}
</ToggleIconButton>
);
}
15 changes: 11 additions & 4 deletions tauri/src/components/ui/participant-row-wo-livekit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { socketService } from "@/services/socket";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { sleep } from "@/lib/utils";
import { TCallRequestMessage, TWebSocketMessage } from "@/payloads";
import { TRejectCallMessage, TCallRequestMessage, TWebSocketMessage } from "@/payloads";
import useStore, { ParticipantRole } from "@/store/store";
import { sounds } from "@/constants/sounds";
import { usePostHog } from "posthog-js/react";
Expand Down Expand Up @@ -68,9 +68,16 @@ export const ParticipantRow = (props: { user: components["schemas"]["BaseUser"]

switch (data.type) {
case "call_reject":
toast.error(`${props.user.first_name} rejected your call`, {
duration: 2500,
});
const { payload } = data as TRejectCallMessage;
if (payload.reject_reason == "in-call") {
toast.error(`${props.user.first_name} is already in a call`, {
duration: 2500,
});
} else {
toast.error(`${props.user.first_name} rejected your call`, {
duration: 2500,
});
}
setCalling(null);
sounds.ringing.stop();
sounds.unavailable.play();
Expand Down
7 changes: 4 additions & 3 deletions tauri/src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons";
import clsx from "clsx";

const Select = SelectPrimitive.Root;

Expand All @@ -11,8 +12,8 @@ const SelectValue = SelectPrimitive.Value;

const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & { iconClassName?: string }
>(({ className, children, iconClassName = "", ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
Expand All @@ -23,7 +24,7 @@ const SelectTrigger = React.forwardRef<
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="h-4 w-4 opacity-50" />
<ChevronDownIcon className={clsx("size-4 opacity-70", iconClassName)} />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
Expand Down
2 changes: 1 addition & 1 deletion tauri/src/components/ui/toggle-icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const ToggleIconButton: React.FC<
"flex flex-col p-4 small items-center justify-center gap-1 px-4 py-1.5 rounded-md ring-1 ring-inset shadow-xs transition-colors duration-100 relative font-medium",
{
"bg-gray-300 text-gray-600": state === "deactivated",
"ring-emerald-600": state === "active",
// "ring-emerald-600": state === "active",
"bg-white text-gray-500 hover:bg-gray-100 ring-slate-200": state === "neutral",
"h-[65px] w-[110px]": size === "default",
},
Expand Down
5 changes: 4 additions & 1 deletion tauri/src/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ export const PCallTokensMessage = z.object({

export const PRejectCallMessage = z.object({
type: z.literal("call_reject"),
payload: z.object({ caller_id: z.string() }),
payload: z.object({
caller_id: z.string(),
reject_reason: z.enum(["in-call", "rejected"]).optional(),
}),
});

export const PErrorMessage = z.object({
Expand Down
33 changes: 27 additions & 6 deletions tauri/src/windows/main-window/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { HiOutlineExclamationCircle } from "react-icons/hi2";
import toast from "react-hot-toast";
import { CallBanner } from "@/components/ui/call-banner";
import { socketService } from "@/services/socket";
import { TWebSocketMessage } from "@/payloads";
import { TRejectCallMessage, TIncomingCallMessage, TWebSocketMessage } from "@/payloads";
import { Participants } from "@/components/ui/participants";
import { CallCenter } from "@/components/ui/call-center";
import { listen } from "@tauri-apps/api/event";
Expand Down Expand Up @@ -88,7 +88,7 @@ function App() {
useEffect(() => {
const sendLivekitUrlToBackend = async () => {
if (livekitUrlData?.url) {
console.log("livekitUrlData", livekitUrlData);
console.log("livekitUrlData", livekitUrlData);
try {
await tauriUtils.setLivekitUrl(livekitUrlData.url);
setLivekitUrl(livekitUrlData.url);
Expand Down Expand Up @@ -173,22 +173,43 @@ function App() {
return () => clearInterval(interval);
}, []);

const handleReject = () => {
if (!incomingCallerId) return;
sounds.incomingCall.stop();
const handleReject = (isInCall?: boolean) => {
if (!incomingCallerId && !isInCall) return;
if (!isInCall) {
sounds.incomingCall.stop();
}
socketService.send({
type: "call_reject",
payload: {
caller_id: incomingCallerId,
reject_reason: "rejected",
},
});
} as TRejectCallMessage);
};

const handleInCallRejection = (callerID: string) => {
socketService.send({
type: "call_reject",
payload: {
caller_id: callerID,
reject_reason: "in-call",
},
} as TRejectCallMessage);
};

// Generic socket event listeners
// Need to remove from here and add them to a shared file
// to be cleaner and easier to manage
useEffect(() => {
socketService.on("incoming_call", (data: TWebSocketMessage) => {
// Check that there is no on-going call
// If there is, reject the call
const { callTokens } = useStore.getState();
if (callTokens) {
handleInCallRejection((data as TIncomingCallMessage).payload.caller_id);
return;
}

if (data.type === "incoming_call") {
setIncomingCallerId(data.payload.caller_id);

Expand Down