Files
calendar/apps/web/modules/videos/views/videos-single-view.tsx
T
1a4afe06c8 chore: Remove Pages Router / Render App Router - /video (#16518)
* /video: Remove Pages Router / Render App Router

* fix

* add meeting_unavailable i18n

* remove <Head> from videos view

* add correct metadata to video/[uid]

* add safety to videos-single-view logic

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-11 18:51:30 +01:00

268 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import type { DailyCall } from "@daily-co/daily-js";
import DailyIframe from "@daily-co/daily-js";
import { DailyProvider } from "@daily-co/daily-react";
import { useState, useEffect, useRef } from "react";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { TRANSCRIPTION_STOPPED_ICON, RECORDING_DEFAULT_ICON } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Icon } from "@calcom/ui";
import type { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
import { CalAiTranscribe } from "~/videos/ai/ai-transcribe";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function JoinCall(props: PageProps) {
const { meetingUrl, meetingPassword, booking, hasTeamPlan, calVideoLogo } = props;
const [daily, setDaily] = useState<DailyCall | null>(null);
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,
...(typeof meetingPassword === "string" && { token: meetingPassword }),
...(hasTeamPlan && {
customTrayButtons: {
recording: {
label: "Record",
tooltip: "Start or stop recording",
iconPath: RECORDING_DEFAULT_ICON,
iconPathDarkMode: RECORDING_DEFAULT_ICON,
},
transcription: {
label: "Cal.ai",
tooltip: "Transcription powered by AI",
iconPath: TRANSCRIPTION_STOPPED_ICON,
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
},
},
}),
});
} catch (err) {
callFrame = DailyIframe.getCallInstance();
} finally {
setDaily(callFrame ?? null);
callFrame?.join();
}
return () => {
callFrame?.destroy();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<DailyProvider callObject={daily}>
<div
className="mx-auto hidden sm:block"
style={{ zIndex: 2, left: "30%", position: "absolute", bottom: 100, width: "auto" }}>
<CalAiTranscribe />
</div>
<div style={{ zIndex: 2, position: "relative" }}>
{calVideoLogo ? (
<img
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
src={calVideoLogo}
alt="My Org Logo"
style={{
top: 32,
left: 32,
}}
/>
) : (
<img
className="fixed z-10 hidden h-5 sm:inline-block"
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
alt="Logo"
style={{
top: 47,
left: 20,
}}
/>
)}
</div>
<VideoMeetingInfo booking={booking} />
</DailyProvider>
);
}
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<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;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const prev = startDuration - duration;
const percentage = prev * (100 / startDuration);
return (
<div>
<p>
{duration} {t("minutes")}
</p>
<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: PageProps["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 (
<>
<aside
className={classNames(
"no-scrollbar fixed left-0 top-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">
<h3>{t("what")}:</h3>
<p>{booking.title}</p>
<h3>{t("invitee_timezone")}:</h3>
<p>{booking.user?.timeZone}</p>
<h3>{t("when")}:</h3>
<p suppressHydrationWarning={true}>
{formatToLocalizedDate(startTime)} <br />
{formatToLocalizedTime(startTime)}
</p>
<h3>{t("time_left")}</h3>
<ProgressBar
key={String(open)}
endTime={endTime.toISOString()}
startTime={startTime.toISOString()}
/>
<h3>{t("who")}:</h3>
<p>
{booking?.user?.name} - {t("organizer")}:{" "}
<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}
{booking.description && (
<>
<h3>{t("description")}:</h3>
<div
className="prose-sm prose prose-invert"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: markdownToSafeHTML(booking.description) }}
/>
</>
)}
</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)}>
<Icon
name="chevron-right"
aria-hidden
className={classNames(open && "rotate-180", "w-5 transition-all duration-300 ease-in-out")}
/>
</button>
</div>
</aside>
</>
);
}