diff --git a/apps/web/modules/videos/views/videos-single-view.tsx b/apps/web/modules/videos/views/videos-single-view.tsx index e3ff5f4b69..fcb54c4339 100644 --- a/apps/web/modules/videos/views/videos-single-view.tsx +++ b/apps/web/modules/videos/views/videos-single-view.tsx @@ -4,7 +4,7 @@ import type { DailyCall } from "@daily-co/daily-js"; import DailyIframe from "@daily-co/daily-js"; import { DailyProvider } from "@daily-co/daily-react"; import { useDailyEvent } from "@daily-co/daily-react"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import dayjs from "@calcom/dayjs"; import { WEBSITE_URL } from "@calcom/lib/constants"; @@ -17,6 +17,7 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import classNames from "@calcom/ui/classNames"; import { Button } from "@calcom/ui/components/button"; import { Dialog, DialogContent } from "@calcom/ui/components/dialog"; +import { Input } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import type { getServerSideProps } from "@lib/video/[uid]/getServerSideProps"; @@ -26,6 +27,7 @@ import { CalVideoPremiumFeatures } from "../cal-video-premium-features"; export type PageProps = inferSSRProps; export default function JoinCall(props: PageProps) { + const { t } = useLocale(); const { meetingUrl, meetingPassword, @@ -42,88 +44,144 @@ export default function JoinCall(props: PageProps) { rediectAttendeeToOnExit, } = props; const [daily, setDaily] = useState(null); + const [userNameForCall, setUserNameForCall] = useState( + overrideName ?? loggedInUserName ?? undefined + ); + const [isUserNameConfirmed, setIsUserNameConfirmed] = useState(!displayLogInOverlay); + const [isCallFrameReady, setIsCallFrameReady] = useState(false); + + const createCallFrame = useCallback( + (userName?: string) => { + let callFrame: DailyCall | undefined; + + try { + callFrame = DailyIframe.createFrame({ + theme: { + colors: { + accent: "#FFF", + accentText: "#111111", + background: "#111111", + backgroundAccent: "#111111", + baseText: "#FFF", + border: "#292929", + mainAreaBg: "#111111", + mainAreaBgAccent: "#1A1A1A", + mainAreaText: "#FFF", + supportiveText: "#FFF", + }, + }, + showLeaveButton: true, + iframeStyle: { + position: "fixed", + width: "100%", + height: "100%", + }, + url: meetingUrl, + userName: userName, + ...(typeof meetingPassword === "string" && { token: meetingPassword }), + ...(hasTeamPlan && { + customTrayButtons: { + ...(showRecordingButton + ? { + recording: { + label: t("record"), + tooltip: t("start_or_stop_recording"), + iconPath: RECORDING_DEFAULT_ICON, + iconPathDarkMode: RECORDING_DEFAULT_ICON, + }, + } + : {}), + ...(showTranscriptionButton + ? { + transcription: { + label: t("transcribe"), + tooltip: t("transcription_powered_by_ai"), + iconPath: TRANSCRIPTION_STOPPED_ICON, + iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON, + }, + } + : {}), + }, + }), + }); + + if (userName) { + callFrame.setUserName(userName); + } + + return callFrame; + } catch (err) { + return DailyIframe.getCallInstance(); + } + }, + [meetingUrl, meetingPassword, hasTeamPlan, showRecordingButton, showTranscriptionButton, t] + ); useEffect(() => { - let callFrame: DailyCall | undefined; - try { - callFrame = DailyIframe.createFrame({ - theme: { - colors: { - accent: "#FFF", - accentText: "#111111", - background: "#111111", - backgroundAccent: "#111111", - baseText: "#FFF", - border: "#292929", - mainAreaBg: "#111111", - mainAreaBgAccent: "#1A1A1A", - mainAreaText: "#FFF", - supportiveText: "#FFF", - }, - }, - showLeaveButton: true, - iframeStyle: { - position: "fixed", - width: "100%", - height: "100%", - }, - url: meetingUrl, - userName: overrideName ?? loggedInUserName ?? undefined, - ...(typeof meetingPassword === "string" && { token: meetingPassword }), - ...(hasTeamPlan && { - customTrayButtons: { - ...(showRecordingButton - ? { - recording: { - label: "Record", - tooltip: "Start or stop recording", - iconPath: RECORDING_DEFAULT_ICON, - iconPathDarkMode: RECORDING_DEFAULT_ICON, - }, - } - : {}), - ...(showTranscriptionButton - ? { - transcription: { - label: "Transcribe", - tooltip: "Transcription powered by AI", - iconPath: TRANSCRIPTION_STOPPED_ICON, - iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON, - }, - } - : {}), - }, - }), - }); + if (displayLogInOverlay && !loggedInUserName && !overrideName && !isUserNameConfirmed) { + return; + } + + let callFrame: DailyCall | null = null; + + try { + callFrame = createCallFrame(userNameForCall) ?? null; + setDaily(callFrame); + setIsCallFrameReady(true); - if (overrideName) { - callFrame.setUserName(overrideName); - } - } catch (err) { - callFrame = DailyIframe.getCallInstance(); - } finally { - setDaily(callFrame ?? null); callFrame?.join(); + } catch (error) { + console.error("Failed to create or join call:", error); } return () => { - callFrame?.destroy(); + if (callFrame) { + try { + callFrame.destroy(); + } catch (error) { + console.error("Error destroying call frame:", error); + } + } + setDaily(null); + setIsCallFrameReady(false); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + displayLogInOverlay, + isUserNameConfirmed, + userNameForCall, + createCallFrame, + loggedInUserName, + overrideName, + ]); + + const handleJoinAsGuest = useCallback((guestName: string) => { + const trimmedName = guestName.trim(); + if (!trimmedName) { + return; + } + + setUserNameForCall(trimmedName); + setIsUserNameConfirmed(true); + }, []); + + const handleUserNameConfirmed = useCallback(() => { + setIsUserNameConfirmed(true); }, []); return ( -
- -
+ {isCallFrameReady && ( +
+ +
+ )}
{calVideoLogo ? ( )}
- {displayLogInOverlay && } + {displayLogInOverlay && !isUserNameConfirmed && ( + + )}
@@ -206,8 +273,7 @@ function ProgressBar(props: ProgressBarProps) { intervalRef.current = null; } }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [startingTime]); const prev = startDuration - duration; const percentage = prev * (100 / startDuration); @@ -227,49 +293,126 @@ function ProgressBar(props: ProgressBarProps) { interface LogInOverlayProps { isLoggedIn: boolean; bookingUid: string; + loggedInUserName?: string; + overrideName?: string; + onJoinAsGuest: (guestName: string) => void; + onUserNameConfirmed?: () => void; } export function LogInOverlay(props: LogInOverlayProps) { const { t } = useLocale(); - const { isLoggedIn, bookingUid } = props; - const [open, setOpen] = useState(!isLoggedIn); + const { bookingUid, isLoggedIn, loggedInUserName, overrideName, onJoinAsGuest, onUserNameConfirmed } = + props; + + const [isOpen, setIsOpen] = useState(!isLoggedIn); + const [userName, setUserName] = useState(overrideName ?? loggedInUserName ?? ""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleContinueAsGuest = useCallback(async () => { + const trimmedName = userName.trim(); + if (!trimmedName) { + return; + } + + setIsLoading(true); + setError(null); + + try { + onJoinAsGuest(trimmedName); + onUserNameConfirmed?.(); + setIsOpen(false); + } catch (error) { + console.error("Error joining as guest:", error); + const errorMessage = error instanceof Error ? error.message : t("failed_to_join_call"); + + setError(errorMessage); + } finally { + setIsLoading(false); + } + }, [userName, onJoinAsGuest, onUserNameConfirmed, t]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && userName.trim() && !isLoading) { + handleContinueAsGuest(); + } + }, + [userName, isLoading, handleContinueAsGuest] + ); + + const handleUserNameChange = useCallback( + (e: React.ChangeEvent) => { + setUserName(e.target.value); + if (error) { + setError(null); + } + }, + [error] + ); return ( - + -
-
- - - {/* Divider */} -
-
- -
-
- {t("or")} -
+ description={t("choose_how_you_d_like_to_appear_on_the_call")} + className="bg-white text-black dark:bg-black dark:text-white sm:max-w-[480px]"> +
+
+
+
{t("join_as_guest")}
+

{t("ideal_for_one_time_calls")}

-
-

{t("sign_in_to_cal_com")}

-

{t("track_your_meetings")}

-
+ + {error && ( +
+
+
+

{error}

+
+
+
+ )} + + {/* Divider */} +
+ +
+
+

+ {t("sign_in_to_cal_com")} +

+

+ {t("track_meetings_and_manage_schedule")} +

+
+ + +
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index acc740b6b8..ce42f61635 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -125,10 +125,14 @@ "price": "Price", "paid": "Paid", "refunded": "Refunded", + "record": "Record", + "transcribe": "Transcribe", "payment": "Payment", "missing_card_fields": "Missing card fields", "pay_now": "Pay now", "general_prompt": "General Prompt", + "transcription_powered_by_ai": "Transcription powered by AI", + "start_or_stop_recording": "Start or stop recording", "begin_message": "Begin Message", "codebase_has_to_stay_opensource": "The codebase has to stay open source, whether it was modified or not", "cannot_repackage_codebase": "You can not repackage or sell the codebase", @@ -661,6 +665,8 @@ "somewhere_else": "Somewhere Else", "custom_attendee_location": "Custom attendee location", "continue_as_guest": "Continue as Guest", + "join_as_guest": "Join as Guest", + "ideal_for_one_time_calls": "Ideal for one-time calls", "any_location": "Any location", "yes": "Yes", "no": "No", @@ -1415,10 +1421,13 @@ "after_hosts_cal_video_no_show": "After hosts didn't join cal video", "after_guests_cal_video_no_show": "After guests didn't join cal video", "join_video_call": "Join the video call", - "sign_in_to_cal_com": "Sign in to your Cal.com account", + "sign_in_to_cal_com": "Sign in to Cal.com", "log_in_to_cal_com": "Log in to Cal.com", "track_your_meetings": "Track your meetings, get notifications, and manage your schedule more effectively.", + "track_meetings_and_manage_schedule": "Track your meetings and manage your schedule more effectively", + "failed_to_join_call": "Failed to join the call. Please try again.", "choose_how_you_d_like_to_join_call": "Choose how you'd like to join this call", + "choose_how_you_d_like_to_appear_on_the_call": "Choose how you'd like to appear on the call", "new_event_trigger": "when new event is booked", "email_host_action": "send email to host", "email_attendee_action": "send email to attendees",