diff --git a/.env.example b/.env.example index c0ecaa4d653fc5..a2de29cf3de8be 100644 --- a/.env.example +++ b/.env.example @@ -297,3 +297,4 @@ APP_ROUTER_EVENT_TYPES_ENABLED=1 APP_ROUTER_SETTINGS_ADMIN_ENABLED=1 APP_ROUTER_APPS_SLUG_ENABLED=1 APP_ROUTER_APPS_SLUG_SETUP_ENABLED=1 +APP_ROUTER_VIDEO_ENABLED=1 diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index 596a8b3035ae0a..6ea5e8888d9883 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -8,6 +8,7 @@ const ROUTES: [URLPattern, boolean][] = [ ["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const, ["/apps/:slug", Boolean(process.env.APP_ROUTER_APPS_SLUG_ENABLED)] as const, ["/apps/:slug/setup", Boolean(process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED)] as const, + ["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const, ].map(([pathname, enabled]) => [ new URLPattern({ pathname, diff --git a/apps/web/app/future/(individual-page-wrapper)/teams/page.tsx b/apps/web/app/future/(individual-page-wrapper)/teams/page.tsx new file mode 100644 index 00000000000000..18059f5b99e0c8 --- /dev/null +++ b/apps/web/app/future/(individual-page-wrapper)/teams/page.tsx @@ -0,0 +1,64 @@ +import OldPage from "@pages/teams/index"; +import { ssrInit } from "app/_trpc/ssrInit"; +import { type Params } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import { type GetServerSidePropsContext } from "next"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; + +import { getLayout } from "@calcom/features/MainLayoutAppDir"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("teams"), + (t) => t("create_manage_teams_collaborative") + ); + +type PageProps = { + params: Params; +}; + +async function getData(context: Omit) { + const ssr = await ssrInit(); + await ssr.viewer.me.prefetch(); + + const session = await getServerSession({ + req: context.req, + }); + + if (!session) { + const token = Array.isArray(context.query.token) ? context.query.token[0] : context.query.token; + + const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null; + return redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login"); + } + + return { dehydratedState: await ssr.dehydrate() }; +} + +const Page = async ({ params }: PageProps) => { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + const legacyCtx = buildLegacyCtx(h, cookies(), params); + // @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext` + const props = await getData(legacyCtx); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/(individual-page-wrapper)/video/[uid]/page.tsx b/apps/web/app/future/(individual-page-wrapper)/video/[uid]/page.tsx new file mode 100644 index 00000000000000..e1e473ceecf9ac --- /dev/null +++ b/apps/web/app/future/(individual-page-wrapper)/video/[uid]/page.tsx @@ -0,0 +1,130 @@ +import OldPage from "@pages/video/[uid]"; +import { ssrInit } from "app/_trpc/ssrInit"; +import { type Params } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import MarkdownIt from "markdown-it"; +import { type GetServerSidePropsContext } from "next"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; + +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { APP_NAME } from "@calcom/lib/constants"; +import prisma, { bookingMinimalSelect } from "@calcom/prisma"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +export const generateMetadata = async () => + await _generateMetadata( + () => `${APP_NAME} Video`, + (t) => t("quick_video_meeting") + ); + +type PageProps = Readonly<{ + params: Params; +}>; + +const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true }); + +async function getData(context: Omit) { + const ssr = await ssrInit(); + + 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, + organization: { + select: { + calVideoLogo: 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("/video/no-meeting-found"); + } + + //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(`/video/meeting-ended/${booking?.uid}`); + } + + const bookingObj = Object.assign({}, booking, { + startTime: booking.startTime.toString(), + endTime: booking.endTime.toString(), + }); + + const session = await getServerSession({ req: context.req }); + + // set meetingPassword to null for guests + if (session?.user.id !== bookingObj.user?.id) { + bookingObj.references.forEach((bookRef: any) => { + bookRef.meetingPassword = null; + }); + } + + return { + 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) }), + }, + dehydratedState: await ssr.dehydrate(), + }; +} + +const Page = async ({ params }: PageProps) => { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + const legacyCtx = buildLegacyCtx(headers(), cookies(), params); + // @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext` + const { dehydratedState, ...restProps } = await getData(legacyCtx); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/(individual-page-wrapper)/video/meeting-ended/[uid]/page.tsx b/apps/web/app/future/(individual-page-wrapper)/video/meeting-ended/[uid]/page.tsx new file mode 100644 index 00000000000000..d0456d3047f11a --- /dev/null +++ b/apps/web/app/future/(individual-page-wrapper)/video/meeting-ended/[uid]/page.tsx @@ -0,0 +1,76 @@ +import OldPage from "@pages/video/meeting-ended/[uid]"; +import { type Params } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import { type GetServerSidePropsContext } from "next"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; + +import prisma, { bookingMinimalSelect } from "@calcom/prisma"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Meeting Unavailable", + () => "Meeting Unavailable" + ); + +type PageProps = Readonly<{ + params: Params; +}>; + +async function getData(context: Omit) { + const booking = await prisma.booking.findUnique({ + where: { + uid: typeof context?.params?.uid === "string" ? context.params.uid : "", + }, + select: { + ...bookingMinimalSelect, + uid: true, + user: { + select: { + credentials: true, + }, + }, + references: { + select: { + uid: true, + type: true, + meetingUrl: true, + }, + }, + }, + }); + + if (!booking) { + return redirect("/video/no-meeting-found"); + } + + const bookingObj = Object.assign({}, booking, { + startTime: booking.startTime.toString(), + endTime: booking.endTime.toString(), + }); + + return { + booking: bookingObj, + }; +} + +const Page = async ({ params }: PageProps) => { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + const legacyCtx = buildLegacyCtx(headers(), cookies(), params); + // @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext` + const props = await getData(legacyCtx); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/(individual-page-wrapper)/video/meeting-not-started/[uid]/page.tsx b/apps/web/app/future/(individual-page-wrapper)/video/meeting-not-started/[uid]/page.tsx new file mode 100644 index 00000000000000..b15f16452efbf7 --- /dev/null +++ b/apps/web/app/future/(individual-page-wrapper)/video/meeting-not-started/[uid]/page.tsx @@ -0,0 +1,69 @@ +import OldPage from "@pages/video/meeting-not-started/[uid]"; +import { type Params } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import { type GetServerSidePropsContext } from "next"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; + +import prisma, { bookingMinimalSelect } from "@calcom/prisma"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +type PageProps = Readonly<{ + params: Params; +}>; + +export const generateMetadata = async ({ params }: PageProps) => { + const booking = await prisma.booking.findUnique({ + where: { + uid: typeof params?.uid === "string" ? params.uid : "", + }, + select: bookingMinimalSelect, + }); + + return await _generateMetadata( + (t) => t("this_meeting_has_not_started_yet"), + () => booking?.title ?? "" + ); +}; + +async function getData(context: Omit) { + const booking = await prisma.booking.findUnique({ + where: { + uid: typeof context?.params?.uid === "string" ? context.params.uid : "", + }, + select: bookingMinimalSelect, + }); + + if (!booking) { + return redirect("/video/no-meeting-found"); + } + + const bookingObj = Object.assign({}, booking, { + startTime: booking.startTime.toString(), + endTime: booking.endTime.toString(), + }); + + return { + booking: bookingObj, + }; +} + +const Page = async ({ params }: PageProps) => { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + const legacyCtx = buildLegacyCtx(headers(), cookies(), params); + // @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext` + const props = await getData(legacyCtx); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(no-layout)/video/no-meeting-found/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(no-layout)/video/no-meeting-found/page.tsx new file mode 100644 index 00000000000000..df1af7a2157fcb --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(no-layout)/video/no-meeting-found/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/video/no-meeting-found"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "", + () => "" + ); + +export default Page; diff --git a/apps/web/lib/buildLegacyCtx.tsx b/apps/web/lib/buildLegacyCtx.tsx new file mode 100644 index 00000000000000..2dfbf3ff2f3ed2 --- /dev/null +++ b/apps/web/lib/buildLegacyCtx.tsx @@ -0,0 +1,23 @@ +import { type Params } from "app/_types"; +import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; +import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies"; + +// returns query object same as ctx.query but for app dir +export const getQuery = (url: string, params: Params) => { + if (!url.length) { + return params; + } + + const { searchParams } = new URL(url); + const searchParamsObj = Object.fromEntries(searchParams.entries()); + + return { ...searchParamsObj, ...params }; +}; + +export const buildLegacyCtx = (headers: ReadonlyHeaders, cookies: ReadonlyRequestCookies, params: Params) => { + return { + query: getQuery(headers.get("x-url") ?? "", params), + params, + req: { headers, cookies }, + }; +}; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 40f17053153197..969b46d89dc69d 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -103,12 +103,12 @@ export const config = { "/future/event-types/", "/settings/admin/:path*", "/future/settings/admin/:path*", - "/apps/:slug/", "/future/apps/:slug/", - "/apps/:slug/setup/", "/future/apps/:slug/setup/", + "/video/:path*", + "/future/video/:path*", ], }; diff --git a/apps/web/pages/teams/index.tsx b/apps/web/pages/teams/index.tsx index 909973f4f2a131..87a49c83ac72d5 100644 --- a/apps/web/pages/teams/index.tsx +++ b/apps/web/pages/teams/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { GetServerSidePropsContext } from "next"; import { getLayout } from "@calcom/features/MainLayout"; diff --git a/apps/web/pages/video/[uid].tsx b/apps/web/pages/video/[uid].tsx index 4a5bf88768cdb7..aacbb1d0286ef3 100644 --- a/apps/web/pages/video/[uid].tsx +++ b/apps/web/pages/video/[uid].tsx @@ -1,3 +1,5 @@ +"use client"; + import DailyIframe from "@daily-co/daily-js"; import MarkdownIt from "markdown-it"; import type { GetServerSidePropsContext } from "next"; @@ -19,7 +21,7 @@ import PageWrapper from "@components/PageWrapper"; import { ssrInit } from "@server/lib/ssr"; -export type JoinCallPageProps = inferSSRProps; +export type JoinCallPageProps = Omit, "trpcState">; const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true }); export default function JoinCall(props: JoinCallPageProps) { diff --git a/apps/web/pages/video/meeting-ended/[uid].tsx b/apps/web/pages/video/meeting-ended/[uid].tsx index b511de96bbb639..ce13db62335224 100644 --- a/apps/web/pages/video/meeting-ended/[uid].tsx +++ b/apps/web/pages/video/meeting-ended/[uid].tsx @@ -1,3 +1,5 @@ +"use client"; + import type { NextPageContext } from "next"; import dayjs from "@calcom/dayjs"; diff --git a/apps/web/pages/video/meeting-not-started/[uid].tsx b/apps/web/pages/video/meeting-not-started/[uid].tsx index 144312ac38d2bd..26a2c50070d22a 100644 --- a/apps/web/pages/video/meeting-not-started/[uid].tsx +++ b/apps/web/pages/video/meeting-not-started/[uid].tsx @@ -1,3 +1,5 @@ +"use client"; + import type { NextPageContext } from "next"; import dayjs from "@calcom/dayjs"; diff --git a/apps/web/pages/video/no-meeting-found.tsx b/apps/web/pages/video/no-meeting-found.tsx index 7de824c944a3e4..e3d30fe413b3a9 100644 --- a/apps/web/pages/video/no-meeting-found.tsx +++ b/apps/web/pages/video/no-meeting-found.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, EmptyScreen, HeadSeo } from "@calcom/ui"; import { X, ArrowRight } from "@calcom/ui/components/icon"; diff --git a/turbo.json b/turbo.json index 100cd41c08a091..e1a3c839fb6985 100644 --- a/turbo.json +++ b/turbo.json @@ -202,6 +202,7 @@ "APP_ROUTER_APPS_SLUG_SETUP_ENABLED", "APP_ROUTER_EVENT_TYPES_ENABLED", "APP_ROUTER_SETTINGS_ADMIN_ENABLED", + "APP_ROUTER_VIDEO_ENABLED", "APP_USER_NAME", "BASECAMP3_CLIENT_ID", "BASECAMP3_CLIENT_SECRET",