2023-04-13 19:07:10 +00:00
|
|
|
|
import type { DailyEventObjectRecordingStarted } from "@daily-co/daily-js";
|
2021-10-07 16:12:39 +00:00
|
|
|
|
import DailyIframe from "@daily-co/daily-js";
|
2023-03-14 19:43:45 +00:00
|
|
|
|
import MarkdownIt from "markdown-it";
|
2023-02-16 22:39:57 +00:00
|
|
|
|
import type { GetServerSidePropsContext } from "next";
|
2021-10-14 23:08:14 +00:00
|
|
|
|
import Head from "next/head";
|
2023-04-09 10:20:51 +00:00
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
2023-04-13 19:07:10 +00:00
|
|
|
|
import z from "zod";
|
2021-10-07 16:12:39 +00:00
|
|
|
|
|
2023-03-14 19:43:45 +00:00
|
|
|
|
import dayjs from "@calcom/dayjs";
|
2023-03-10 23:45:24 +00:00
|
|
|
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
2023-03-14 19:43:45 +00:00
|
|
|
|
import classNames from "@calcom/lib/classNames";
|
2023-01-04 22:14:46 +00:00
|
|
|
|
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
2023-03-14 19:43:45 +00:00
|
|
|
|
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
|
2022-05-18 21:05:49 +00:00
|
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
|
|
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
2023-02-16 22:39:57 +00:00
|
|
|
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
2023-04-12 15:26:31 +00:00
|
|
|
|
import { ChevronRight } from "@calcom/ui/components/icon";
|
2022-01-07 20:23:37 +00:00
|
|
|
|
|
2023-01-25 08:51:09 +00:00
|
|
|
|
import { ssrInit } from "@server/lib/ssr";
|
|
|
|
|
|
2023-04-13 19:07:10 +00:00
|
|
|
|
const recordingStartedEventResponse = z
|
|
|
|
|
.object({
|
|
|
|
|
recordingId: z.string(),
|
|
|
|
|
})
|
|
|
|
|
.passthrough();
|
|
|
|
|
|
2022-01-07 20:23:37 +00:00
|
|
|
|
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
|
2023-03-14 19:43:45 +00:00
|
|
|
|
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
2021-10-07 16:12:39 +00:00
|
|
|
|
|
2022-01-07 20:23:37 +00:00
|
|
|
|
export default function JoinCall(props: JoinCallPageProps) {
|
2022-02-15 12:46:27 +00:00
|
|
|
|
const { t } = useLocale();
|
2023-03-14 19:43:45 +00:00
|
|
|
|
const { meetingUrl, meetingPassword, booking } = props;
|
2023-04-13 19:07:10 +00:00
|
|
|
|
const recordingId = useRef<string | null>(null);
|
2021-10-26 13:10:55 +00:00
|
|
|
|
|
2021-10-07 16:12:39 +00:00
|
|
|
|
useEffect(() => {
|
2022-08-11 00:53:05 +00:00
|
|
|
|
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",
|
2021-10-26 13:10:55 +00:00
|
|
|
|
},
|
2022-08-11 00:53:05 +00:00
|
|
|
|
},
|
|
|
|
|
showLeaveButton: true,
|
|
|
|
|
iframeStyle: {
|
|
|
|
|
position: "fixed",
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
},
|
2023-01-25 08:51:09 +00:00
|
|
|
|
url: meetingUrl,
|
|
|
|
|
...(typeof meetingPassword === "string" && { token: meetingPassword }),
|
2022-08-11 00:53:05 +00:00
|
|
|
|
});
|
2023-01-25 08:51:09 +00:00
|
|
|
|
callFrame.join();
|
2023-04-13 19:07:10 +00:00
|
|
|
|
callFrame.on("recording-started", onRecordingStarted).on("recording-stopped", onRecordingStopped);
|
2023-01-25 08:51:09 +00:00
|
|
|
|
return () => {
|
|
|
|
|
callFrame.destroy();
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2023-04-13 19:07:10 +00:00
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-25 08:51:09 +00:00
|
|
|
|
const title = `${APP_NAME} Video`;
|
2021-10-09 15:15:07 +00:00
|
|
|
|
return (
|
2021-10-14 23:08:14 +00:00
|
|
|
|
<>
|
|
|
|
|
<Head>
|
2023-01-25 08:51:09 +00:00
|
|
|
|
<title>{title}</title>
|
2022-02-15 12:46:27 +00:00
|
|
|
|
<meta name="description" content={t("quick_video_meeting")} />
|
2022-07-01 17:19:52 +00:00
|
|
|
|
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
|
2022-02-15 12:46:27 +00:00
|
|
|
|
<meta property="og:type" content="website" />
|
2022-07-01 17:19:52 +00:00
|
|
|
|
<meta property="og:url" content={`${WEBSITE_URL}/video`} />
|
2023-01-04 22:14:46 +00:00
|
|
|
|
<meta property="og:title" content={APP_NAME + " Video"} />
|
2022-02-15 12:46:27 +00:00
|
|
|
|
<meta property="og:description" content={t("quick_video_meeting")} />
|
2022-07-01 17:19:52 +00:00
|
|
|
|
<meta property="twitter:image" content={SEO_IMG_OGIMG_VIDEO} />
|
2022-02-15 12:46:27 +00:00
|
|
|
|
<meta property="twitter:card" content="summary_large_image" />
|
2022-07-01 17:19:52 +00:00
|
|
|
|
<meta property="twitter:url" content={`${WEBSITE_URL}/video`} />
|
2023-01-04 22:14:46 +00:00
|
|
|
|
<meta property="twitter:title" content={APP_NAME + " Video"} />
|
2022-02-15 12:46:27 +00:00
|
|
|
|
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
2021-10-14 23:08:14 +00:00
|
|
|
|
</Head>
|
|
|
|
|
<div style={{ zIndex: 2, position: "relative" }}>
|
2022-10-26 13:31:19 +00:00
|
|
|
|
<img
|
|
|
|
|
className="h-5·w-auto fixed z-10 hidden sm:inline-block"
|
|
|
|
|
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
|
|
|
|
alt="Cal.com Logo"
|
|
|
|
|
style={{
|
|
|
|
|
top: 46,
|
|
|
|
|
left: 24,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2021-10-14 23:08:14 +00:00
|
|
|
|
</div>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<VideoMeetingInfo booking={booking} />
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ProgressBarProps {
|
|
|
|
|
startTime: string;
|
|
|
|
|
endTime: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProgressBar(props: ProgressBarProps) {
|
2023-03-21 15:06:04 +00:00
|
|
|
|
const { t } = useLocale();
|
2023-03-14 19:43:45 +00:00
|
|
|
|
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");
|
2023-04-09 10:20:51 +00:00
|
|
|
|
const [duration, setDuration] = useState(() => {
|
2023-03-14 19:43:45 +00:00
|
|
|
|
if (currentDifference >= 0 && isPast) {
|
|
|
|
|
return startDuration - currentDifference;
|
|
|
|
|
} else {
|
|
|
|
|
return startDuration;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2023-04-09 10:20:51 +00:00
|
|
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
|
const intervalRef = useRef<NodeJS.Timeout | null>(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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2023-03-14 19:43:45 +00:00
|
|
|
|
const prev = startDuration - duration;
|
|
|
|
|
const percentage = prev * (100 / startDuration);
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
2023-03-21 15:06:04 +00:00
|
|
|
|
<p>
|
|
|
|
|
{duration} {t("minutes")}
|
|
|
|
|
</p>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<div className="relative h-2 max-w-xl overflow-hidden rounded-full">
|
|
|
|
|
<div className="absolute h-full w-full bg-gray-500/10" />
|
|
|
|
|
<div className={classNames("relative h-full bg-green-500")} style={{ width: `${percentage}%` }} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface VideoMeetingInfo {
|
|
|
|
|
booking: JoinCallPageProps["booking"];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const { booking } = props;
|
2023-03-21 15:06:04 +00:00
|
|
|
|
const { t } = useLocale();
|
2023-03-14 19:43:45 +00:00
|
|
|
|
|
|
|
|
|
const endTime = new Date(booking.endTime);
|
|
|
|
|
const startTime = new Date(booking.startTime);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<aside
|
|
|
|
|
className={classNames(
|
|
|
|
|
"no-scrollbar fixed top-0 left-0 z-30 flex h-full w-64 transform justify-between overflow-x-hidden overflow-y-scroll transition-all duration-300 ease-in-out",
|
|
|
|
|
open ? "translate-x-0" : "-translate-x-[232px]"
|
|
|
|
|
)}>
|
|
|
|
|
<main className="prose-sm prose max-w-64 prose-a:text-white prose-h3:text-white prose-h3:font-cal scroll-bar scrollbar-track-w-20 w-full overflow-scroll overflow-x-hidden border-r border-gray-300/20 bg-black/80 p-4 text-white shadow-sm backdrop-blur-lg">
|
2023-03-21 15:06:04 +00:00
|
|
|
|
<h3>{t("what")}:</h3>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<p>{booking.title}</p>
|
2023-03-21 15:06:04 +00:00
|
|
|
|
<h3>{t("invitee_timezone")}:</h3>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<p>{booking.user?.timeZone}</p>
|
2023-03-21 15:06:04 +00:00
|
|
|
|
<h3>{t("when")}:</h3>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<p>
|
|
|
|
|
{formatToLocalizedDate(startTime)} <br />
|
|
|
|
|
{formatToLocalizedTime(startTime)}
|
|
|
|
|
</p>
|
2023-03-21 15:06:04 +00:00
|
|
|
|
<h3>{t("time_left")}</h3>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<ProgressBar
|
|
|
|
|
key={String(open)}
|
|
|
|
|
endTime={endTime.toISOString()}
|
|
|
|
|
startTime={startTime.toISOString()}
|
|
|
|
|
/>
|
|
|
|
|
|
2023-03-21 15:06:04 +00:00
|
|
|
|
<h3>{t("who")}:</h3>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<p>
|
2023-03-21 15:06:04 +00:00
|
|
|
|
{booking?.user?.name} - {t("organizer")}:{" "}
|
2023-03-14 19:43:45 +00:00
|
|
|
|
<a href={`mailto:${booking?.user?.email}`}>{booking?.user?.email}</a>
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{booking.attendees.length
|
|
|
|
|
? booking.attendees.map((attendee) => (
|
|
|
|
|
<p key={attendee.id}>
|
|
|
|
|
{attendee.name} – <a href={`mailto:${attendee.email}`}>{attendee.email}</a>
|
|
|
|
|
</p>
|
|
|
|
|
))
|
|
|
|
|
: null}
|
|
|
|
|
|
2023-03-21 15:06:04 +00:00
|
|
|
|
{booking.description && (
|
|
|
|
|
<>
|
|
|
|
|
<h3>{t("description")}:</h3>
|
2023-03-14 19:43:45 +00:00
|
|
|
|
|
2023-03-21 15:06:04 +00:00
|
|
|
|
<div
|
|
|
|
|
className="prose-sm prose prose-invert"
|
2023-04-09 10:20:51 +00:00
|
|
|
|
dangerouslySetInnerHTML={{ __html: booking.description }}
|
2023-03-21 15:06:04 +00:00
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2023-03-14 19:43:45 +00:00
|
|
|
|
</main>
|
|
|
|
|
<div className="flex items-center justify-center">
|
|
|
|
|
<button
|
|
|
|
|
aria-label={`${open ? "close" : "open"} booking description sidebar`}
|
|
|
|
|
className="h-20 w-6 rounded-r-md border border-l-0 border-gray-300/20 bg-black/60 text-white shadow-sm backdrop-blur-lg"
|
|
|
|
|
onClick={() => setOpen(!open)}>
|
2023-04-12 15:26:31 +00:00
|
|
|
|
<ChevronRight
|
2023-03-14 19:43:45 +00:00
|
|
|
|
aria-hidden
|
|
|
|
|
className={classNames(open && "rotate-180", "w-5 transition-all duration-300 ease-in-out")}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
2021-10-14 23:08:14 +00:00
|
|
|
|
</>
|
2021-10-09 15:15:07 +00:00
|
|
|
|
);
|
2021-10-07 16:12:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-25 08:51:09 +00:00
|
|
|
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
2023-03-10 23:45:24 +00:00
|
|
|
|
const { req, res } = context;
|
|
|
|
|
|
2023-01-25 08:51:09 +00:00
|
|
|
|
const ssr = await ssrInit(context);
|
|
|
|
|
|
2021-10-26 13:10:55 +00:00
|
|
|
|
const booking = await prisma.booking.findUnique({
|
2021-10-07 16:12:39 +00:00
|
|
|
|
where: {
|
2022-01-07 20:23:37 +00:00
|
|
|
|
uid: context.query.uid as string,
|
2021-10-07 16:12:39 +00:00
|
|
|
|
},
|
|
|
|
|
select: {
|
2022-05-18 21:05:49 +00:00
|
|
|
|
...bookingMinimalSelect,
|
2021-10-26 13:10:55 +00:00
|
|
|
|
uid: true,
|
2023-03-14 19:43:45 +00:00
|
|
|
|
description: true,
|
2023-04-13 19:07:10 +00:00
|
|
|
|
isRecorded: true,
|
2021-10-07 16:12:39 +00:00
|
|
|
|
user: {
|
|
|
|
|
select: {
|
2022-01-07 20:23:37 +00:00
|
|
|
|
id: true,
|
2021-10-07 16:12:39 +00:00
|
|
|
|
credentials: true,
|
2023-03-14 19:43:45 +00:00
|
|
|
|
timeZone: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
2021-10-07 16:12:39 +00:00
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
references: {
|
|
|
|
|
select: {
|
|
|
|
|
uid: true,
|
|
|
|
|
type: true,
|
2022-08-11 00:53:05 +00:00
|
|
|
|
meetingUrl: true,
|
|
|
|
|
meetingPassword: true,
|
|
|
|
|
},
|
|
|
|
|
where: {
|
|
|
|
|
type: "daily_video",
|
2021-10-07 16:12:39 +00:00
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2022-08-11 00:53:05 +00:00
|
|
|
|
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) {
|
2021-10-26 13:10:55 +00:00
|
|
|
|
return {
|
2022-08-11 00:53:05 +00:00
|
|
|
|
redirect: {
|
|
|
|
|
destination: `/video/meeting-ended/${booking?.uid}`,
|
|
|
|
|
permanent: false,
|
|
|
|
|
},
|
2021-10-26 13:10:55 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bookingObj = Object.assign({}, booking, {
|
|
|
|
|
startTime: booking.startTime.toString(),
|
|
|
|
|
endTime: booking.endTime.toString(),
|
|
|
|
|
});
|
2023-03-10 23:45:24 +00:00
|
|
|
|
|
|
|
|
|
const session = await getServerSession({ req, res });
|
2021-10-07 16:12:39 +00:00
|
|
|
|
|
2022-08-11 00:53:05 +00:00
|
|
|
|
// set meetingPassword to null for guests
|
2022-11-23 18:35:08 +00:00
|
|
|
|
if (session?.user.id !== bookingObj.user?.id) {
|
2022-08-11 00:53:05 +00:00
|
|
|
|
bookingObj.references.forEach((bookRef) => {
|
|
|
|
|
bookRef.meetingPassword = null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-07 16:12:39 +00:00
|
|
|
|
return {
|
|
|
|
|
props: {
|
2023-01-25 08:51:09 +00:00
|
|
|
|
meetingUrl: bookingObj.references[0].meetingUrl ?? "",
|
|
|
|
|
...(typeof bookingObj.references[0].meetingPassword === "string" && {
|
|
|
|
|
meetingPassword: bookingObj.references[0].meetingPassword,
|
|
|
|
|
}),
|
2023-04-09 10:20:51 +00:00
|
|
|
|
booking: {
|
|
|
|
|
...bookingObj,
|
|
|
|
|
...(bookingObj.description && { description: md.render(bookingObj.description) }),
|
|
|
|
|
},
|
2023-01-25 08:51:09 +00:00
|
|
|
|
trpcState: ssr.dehydrate(),
|
2021-10-07 16:12:39 +00:00
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|