Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions apps/web/app/(org)/dashboard/caps/Caps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { CapPagination } from "./components/CapPagination";
import { EmptyCapState } from "./components/EmptyCapState";
import type { FolderDataType } from "./components/Folder";
import Folder from "./components/Folder";
import { useUploadingContext } from "./UploadingContext";
import { useUploadingContext, useUploadingStatus } from "./UploadingContext";

export type VideoData = {
id: Video.VideoId;
Expand Down Expand Up @@ -74,7 +74,6 @@ export const Caps = ({
const previousCountRef = useRef<number>(0);
const [selectedCaps, setSelectedCaps] = useState<Video.VideoId[]>([]);
const [isDraggingCap, setIsDraggingCap] = useState(false);
const { uploadStatus } = useUploadingContext();

const anyCapSelected = selectedCaps.length > 0;

Expand Down Expand Up @@ -258,10 +257,7 @@ export const Caps = ({
onError: () => toast.error("Failed to delete cap"),
});

const isUploading = uploadStatus !== undefined;
const uploadingCapId =
uploadStatus && "capId" in uploadStatus ? uploadStatus.capId : undefined;

const [isUploading, uploadingCapId] = useUploadingStatus();
const visibleVideos = useMemo(
() =>
isUploading && uploadingCapId
Expand Down
70 changes: 51 additions & 19 deletions apps/web/app/(org)/dashboard/caps/UploadingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
"use client";

import { useStore } from "@tanstack/react-store";
import { Store } from "@tanstack/store";
import type React from "react";
import { createContext, useContext, useEffect, useState } from "react";

interface UploadingContextType {
uploadStatus: UploadStatus | undefined;
setUploadStatus: (state: UploadStatus | undefined) => void;
}

export type UploadStatus =
| {
status: "parsing";
Expand All @@ -32,6 +29,11 @@ export type UploadStatus =
thumbnailUrl: string | undefined;
};

interface UploadingContextType {
uploadingStore: Store<{ uploadStatus?: UploadStatus }>;
setUploadStatus: (state: UploadStatus | undefined) => void;
}

const UploadingContext = createContext<UploadingContextType | undefined>(
undefined,
);
Expand All @@ -45,13 +47,52 @@ export function useUploadingContext() {
return context;
}

export function useUploadingStatus() {
const { uploadingStore } = useUploadingContext();
return useStore(
uploadingStore,
(s) =>
[
s.uploadStatus !== undefined,
s.uploadStatus && "capId" in s.uploadStatus
? s.uploadStatus.capId
: null,
] as const,
);
}

export function UploadingProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<UploadStatus>();
const [uploadingStore] = useState<Store<{ uploadStatus?: UploadStatus }>>(
() => new Store({}),
);

return (
<UploadingContext.Provider
value={{
uploadingStore,
setUploadStatus: (status: UploadStatus | undefined) => {
uploadingStore.setState((state) => ({
...state,
uploadStatus: status,
}));
},
}}
>
{children}

<ForbidLeaveWhenUploading />
</UploadingContext.Provider>
);
}

// Separated to prevent rerendering whole tree
function ForbidLeaveWhenUploading() {
const { uploadingStore } = useUploadingContext();
const uploadStatus = useStore(uploadingStore, (state) => state.uploadStatus);

// Prevent the user closing the tab while uploading
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (state?.status) {
if (uploadStatus?.status) {
e.preventDefault();
// Chrome requires returnValue to be set
e.returnValue = "";
Expand All @@ -61,16 +102,7 @@ export function UploadingProvider({ children }: { children: React.ReactNode }) {

window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [state]);
}, [uploadStatus]);

return (
<UploadingContext.Provider
value={{
uploadStatus: state,
setUploadStatus: setState,
}}
>
{children}
</UploadingContext.Provider>
);
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,6 @@ export const CapCard = ({
"transition-opacity duration-200",
uploadProgress && "opacity-30",
)}
userId={cap.ownerId}
videoId={cap.id}
alt={`${cap.name} Thumbnail`}
/>
Expand Down
18 changes: 14 additions & 4 deletions apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { userIsPro } from "@cap/utils";
import type { Folder } from "@cap/web-domain";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
import { useStore } from "@tanstack/react-store";
import { useRouter } from "next/navigation";
import { useRef, useState } from "react";
import { toast } from "sonner";
Expand All @@ -15,6 +17,7 @@ import {
useUploadingContext,
} from "@/app/(org)/dashboard/caps/UploadingContext";
import { UpgradeModal } from "@/components/UpgradeModal";
import { imageUrlQuery } from "@/components/VideoThumbnail";

export const UploadCapButton = ({
size = "md",
Expand All @@ -26,9 +29,11 @@ export const UploadCapButton = ({
}) => {
const { user } = useDashboardContext();
const inputRef = useRef<HTMLInputElement>(null);
const { uploadStatus, setUploadStatus } = useUploadingContext();
const { uploadingStore, setUploadStatus } = useUploadingContext();
const isUploading = useStore(uploadingStore, (s) => !!s.uploadStatus);
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
const router = useRouter();
const queryClient = useQueryClient();

const handleClick = () => {
if (!user) return;
Expand All @@ -47,13 +52,16 @@ export const UploadCapButton = ({
const file = e.target.files?.[0];
if (!file || !user) return;

const ok = await legacyUploadCap(file, folderId, setUploadStatus);
const ok = await legacyUploadCap(
file,
folderId,
setUploadStatus,
queryClient,
);
if (ok) router.refresh();
if (inputRef.current) inputRef.current.value = "";
};

const isUploading = !!uploadStatus;

return (
<>
<Button
Expand Down Expand Up @@ -86,6 +94,7 @@ async function legacyUploadCap(
file: File,
folderId: Folder.FolderId | undefined,
setUploadStatus: (state: UploadStatus | undefined) => void,
queryClient: QueryClient,
) {
const parser = await import("@remotion/media-parser");
const webcodecs = await import("@remotion/webcodecs");
Expand Down Expand Up @@ -476,6 +485,7 @@ async function legacyUploadCap(
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
queryClient.refetchQueries(imageUrlQuery(uploadId));
} else {
reject(
new Error(`Screenshot upload failed with status ${xhr.status}`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import { LogoSpinner } from "@cap/ui";
import { calculateStrokeDashoffset, getProgressCircleConfig } from "@cap/utils";
import { useStore } from "@tanstack/react-store";
import { type UploadStatus, useUploadingContext } from "../UploadingContext";

const { circumference } = getProgressCircleConfig();

export const UploadPlaceholderCard = () => {
const { uploadStatus } = useUploadingContext();
const { uploadingStore } = useUploadingContext();
const uploadStatus = useStore(uploadingStore, (s) => s.uploadStatus);
const strokeDashoffset = calculateStrokeDashoffset(
uploadStatus &&
(uploadStatus.status === "converting" ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type { Video } from "@cap/web-domain";
import { useQuery } from "@tanstack/react-query";
import { useStore } from "@tanstack/react-store";
import { Effect, Exit } from "effect";
import { useRouter } from "next/navigation";
import { useMemo, useRef, useState } from "react";
Expand All @@ -13,7 +14,10 @@ import type { VideoData } from "../../../caps/Caps";
import { CapCard } from "../../../caps/components/CapCard/CapCard";
import { SelectedCapsBar } from "../../../caps/components/SelectedCapsBar";
import { UploadPlaceholderCard } from "../../../caps/components/UploadPlaceholderCard";
import { useUploadingContext } from "../../../caps/UploadingContext";
import {
useUploadingContext,
useUploadingStatus,
} from "../../../caps/UploadingContext";

interface FolderVideosSectionProps {
initialVideos: VideoData;
Expand All @@ -27,13 +31,8 @@ export default function FolderVideosSection({
cardType = "default",
}: FolderVideosSectionProps) {
const router = useRouter();
const { uploadStatus } = useUploadingContext();
const { user } = useDashboardContext();

const isUploading = uploadStatus !== undefined;
const uploadingCapId =
uploadStatus && "capId" in uploadStatus ? uploadStatus.capId : null;

const [selectedCaps, setSelectedCaps] = useState<Video.VideoId[]>([]);
const previousCountRef = useRef<number>(0);

Expand Down Expand Up @@ -159,6 +158,7 @@ export default function FolderVideosSection({
refetchOnMount: true,
});

const [isUploading, uploadingCapId] = useUploadingStatus();
const visibleVideos = useMemo(
() =>
isUploading && uploadingCapId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ const VideoCard: React.FC<VideoCardProps> = memo(
>
<VideoThumbnail
imageClass="w-full h-full transition-all duration-200 group-hover:scale-105"
userId={video.ownerId}
videoId={video.id}
alt={`${video.name} Thumbnail`}
objectFit="cover"
Expand Down
31 changes: 8 additions & 23 deletions apps/web/app/api/thumbnail/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ export const revalidate = 0;

export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const userId = searchParams.get("userId");
const videoId = searchParams.get("videoId");
const origin = request.headers.get("origin") as string;

if (!userId || !videoId) {
if (!videoId)
return new Response(
JSON.stringify({
error: true,
Expand All @@ -25,9 +24,8 @@ export async function GET(request: NextRequest) {
headers: getHeaders(origin),
},
);
}

const query = await db()
const [query] = await db()
.select({
video: videos,
bucket: s3Buckets,
Expand All @@ -36,41 +34,29 @@ export async function GET(request: NextRequest) {
.leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id))
.where(eq(videos.id, Video.VideoId.make(videoId)));

if (query.length === 0) {
return new Response(
JSON.stringify({ error: true, message: "Video does not exist" }),
{
status: 401,
headers: getHeaders(origin),
},
);
}

const result = query[0];
if (!result?.video) {
if (!query)
return new Response(
JSON.stringify({ error: true, message: "Video not found" }),
{
status: 401,
status: 404,
headers: getHeaders(origin),
},
);
}

const prefix = `${userId}/${videoId}/`;
const bucketProvider = await createBucketProvider(result.bucket);
const prefix = `${query.video.ownerId}/${query.video.id}/`;
const bucketProvider = await createBucketProvider(query.bucket);

try {
const listResponse = await bucketProvider.listObjects({
prefix: prefix,
});
const contents = listResponse.Contents || [];

const thumbnailKey = contents.find((item: any) =>
const thumbnailKey = contents.find((item) =>
item.Key?.endsWith("screen-capture.jpg"),
)?.Key;

if (!thumbnailKey) {
if (!thumbnailKey)
return new Response(
JSON.stringify({
error: true,
Expand All @@ -81,7 +67,6 @@ export async function GET(request: NextRequest) {
headers: getHeaders(origin),
},
);
}

const thumbnailUrl = await bucketProvider.getSignedObjectUrl(thumbnailKey);

Expand Down
Loading
Loading