import type { DailyEventObjectRecordingStarted } from "@daily-co/daily-js"; import DailyIframe from "@daily-co/daily-js"; import MarkdownIt from "markdown-it"; import type { GetServerSidePropsContext } from "next"; import Head from "next/head"; import { useState, useEffect, useRef } from "react"; import z from "zod"; import dayjs from "@calcom/dayjs"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import classNames from "@calcom/lib/classNames"; import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants"; import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { ChevronRight } from "@calcom/ui/components/icon"; import PageWrapper from "@components/PageWrapper"; import { ssrInit } from "@server/lib/ssr"; const recordingStartedEventResponse = z .object({ recordingId: z.string(), }) .passthrough(); export type JoinCallPageProps = inferSSRProps; const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true }); export default function JoinCall(props: JoinCallPageProps) { const { t } = useLocale(); const { meetingUrl, meetingPassword, booking } = props; const recordingId = useRef(null); useEffect(() => { const callFrame = DailyIframe.createFrame({ theme: { colors: { accent: "#FFF", accentText: "#111111", background: "#111111", backgroundAccent: "#111111", baseText: "#FFF", border: "#292929", mainAreaBg: "#111111", mainAreaBgAccent: "#111111", mainAreaText: "#FFF", supportiveText: "#FFF", }, }, showLeaveButton: true, iframeStyle: { position: "fixed", width: "100%", height: "100%", }, url: meetingUrl, ...(typeof meetingPassword === "string" && { token: meetingPassword }), }); callFrame.join(); callFrame.on("recording-started", onRecordingStarted).on("recording-stopped", onRecordingStopped); return () => { callFrame.destroy(); }; }, []); const onRecordingStopped = () => { const data = { recordingId: recordingId.current, bookingUID: booking.uid }; fetch("/api/recorded-daily-video", { method: "POST", body: JSON.stringify(data), }).catch((err) => { console.log(err); }); recordingId.current = null; }; const onRecordingStarted = (event?: DailyEventObjectRecordingStarted | undefined) => { const response = recordingStartedEventResponse.parse(event); recordingId.current = response.recordingId; }; const title = `${APP_NAME} Video`; return ( <> {title}
Cal.com Logo
); } interface ProgressBarProps { startTime: string; endTime: string; } function ProgressBar(props: ProgressBarProps) { const { t } = useLocale(); const { startTime, endTime } = props; const currentTime = dayjs().second(0).millisecond(0); const startingTime = dayjs(startTime).second(0).millisecond(0); const isPast = currentTime.isAfter(startingTime); const currentDifference = dayjs().diff(startingTime, "minutes"); const startDuration = dayjs(endTime).diff(startingTime, "minutes"); const [duration, setDuration] = useState(() => { if (currentDifference >= 0 && isPast) { return startDuration - currentDifference; } else { return startDuration; } }); const timeoutRef = useRef(null); const intervalRef = useRef(null); useEffect(() => { const now = dayjs(); const remainingMilliseconds = (60 - now.get("seconds")) * 1000 - now.get("milliseconds"); timeoutRef.current = setTimeout(() => { const past = dayjs().isAfter(startingTime); if (past) { setDuration((prev) => prev - 1); } intervalRef.current = setInterval(() => { if (dayjs().isAfter(startingTime)) { setDuration((prev) => prev - 1); } }, 60000); }, remainingMilliseconds); return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, []); const prev = startDuration - duration; const percentage = prev * (100 / startDuration); return (

{duration} {t("minutes")}

); } interface VideoMeetingInfo { booking: JoinCallPageProps["booking"]; } export function VideoMeetingInfo(props: VideoMeetingInfo) { const [open, setOpen] = useState(false); const { booking } = props; const { t } = useLocale(); const endTime = new Date(booking.endTime); const startTime = new Date(booking.startTime); return ( <> ); } VideoMeetingInfo.PageWrapper = PageWrapper; export async function getServerSideProps(context: GetServerSidePropsContext) { const { req, res } = context; const ssr = await ssrInit(context); const booking = await prisma.booking.findUnique({ where: { uid: context.query.uid as string, }, select: { ...bookingMinimalSelect, uid: true, description: true, isRecorded: true, user: { select: { id: true, timeZone: true, name: true, email: true, }, }, references: { select: { uid: true, type: true, meetingUrl: true, meetingPassword: true, }, where: { type: "daily_video", }, }, }, }); if (!booking || booking.references.length === 0 || !booking.references[0].meetingUrl) { return { redirect: { destination: "/video/no-meeting-found", permanent: false, }, }; } //daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals const now = new Date(); const exitDate = new Date(now.getTime() - 60 * 60 * 1000); //find out if the meeting is in the past const isPast = booking?.endTime <= exitDate; if (isPast) { return { redirect: { destination: `/video/meeting-ended/${booking?.uid}`, permanent: false, }, }; } const bookingObj = Object.assign({}, booking, { startTime: booking.startTime.toString(), endTime: booking.endTime.toString(), }); const session = await getServerSession({ req, res }); // set meetingPassword to null for guests if (session?.user.id !== bookingObj.user?.id) { bookingObj.references.forEach((bookRef) => { bookRef.meetingPassword = null; }); } return { props: { meetingUrl: bookingObj.references[0].meetingUrl ?? "", ...(typeof bookingObj.references[0].meetingPassword === "string" && { meetingPassword: bookingObj.references[0].meetingPassword, }), booking: { ...bookingObj, ...(bookingObj.description && { description: md.render(bookingObj.description) }), }, trpcState: ssr.dehydrate(), }, }; }