Skip to content

Commit fdb412f

Browse files
Improve uploading context (#1051)
* AI slop * format * put context into React lifecycle * formast * move context to Tanstack Store + fix auth on thumbnail request * stop parsing `userId` prop * format * nit
1 parent 2965558 commit fdb412f

File tree

11 files changed

+114
-100
lines changed

11 files changed

+114
-100
lines changed

apps/web/app/(org)/dashboard/caps/Caps.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { CapPagination } from "./components/CapPagination";
2424
import { EmptyCapState } from "./components/EmptyCapState";
2525
import type { FolderDataType } from "./components/Folder";
2626
import Folder from "./components/Folder";
27-
import { useUploadingContext } from "./UploadingContext";
27+
import { useUploadingContext, useUploadingStatus } from "./UploadingContext";
2828

2929
export type VideoData = {
3030
id: Video.VideoId;
@@ -74,7 +74,6 @@ export const Caps = ({
7474
const previousCountRef = useRef<number>(0);
7575
const [selectedCaps, setSelectedCaps] = useState<Video.VideoId[]>([]);
7676
const [isDraggingCap, setIsDraggingCap] = useState(false);
77-
const { uploadStatus } = useUploadingContext();
7877

7978
const anyCapSelected = selectedCaps.length > 0;
8079

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

261-
const isUploading = uploadStatus !== undefined;
262-
const uploadingCapId =
263-
uploadStatus && "capId" in uploadStatus ? uploadStatus.capId : undefined;
264-
260+
const [isUploading, uploadingCapId] = useUploadingStatus();
265261
const visibleVideos = useMemo(
266262
() =>
267263
isUploading && uploadingCapId

apps/web/app/(org)/dashboard/caps/UploadingContext.tsx

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
"use client";
22

3+
import { useStore } from "@tanstack/react-store";
4+
import { Store } from "@tanstack/store";
35
import type React from "react";
46
import { createContext, useContext, useEffect, useState } from "react";
57

6-
interface UploadingContextType {
7-
uploadStatus: UploadStatus | undefined;
8-
setUploadStatus: (state: UploadStatus | undefined) => void;
9-
}
10-
118
export type UploadStatus =
129
| {
1310
status: "parsing";
@@ -32,6 +29,11 @@ export type UploadStatus =
3229
thumbnailUrl: string | undefined;
3330
};
3431

32+
interface UploadingContextType {
33+
uploadingStore: Store<{ uploadStatus?: UploadStatus }>;
34+
setUploadStatus: (state: UploadStatus | undefined) => void;
35+
}
36+
3537
const UploadingContext = createContext<UploadingContextType | undefined>(
3638
undefined,
3739
);
@@ -45,13 +47,52 @@ export function useUploadingContext() {
4547
return context;
4648
}
4749

50+
export function useUploadingStatus() {
51+
const { uploadingStore } = useUploadingContext();
52+
return useStore(
53+
uploadingStore,
54+
(s) =>
55+
[
56+
s.uploadStatus !== undefined,
57+
s.uploadStatus && "capId" in s.uploadStatus
58+
? s.uploadStatus.capId
59+
: null,
60+
] as const,
61+
);
62+
}
63+
4864
export function UploadingProvider({ children }: { children: React.ReactNode }) {
49-
const [state, setState] = useState<UploadStatus>();
65+
const [uploadingStore] = useState<Store<{ uploadStatus?: UploadStatus }>>(
66+
() => new Store({}),
67+
);
68+
69+
return (
70+
<UploadingContext.Provider
71+
value={{
72+
uploadingStore,
73+
setUploadStatus: (status: UploadStatus | undefined) => {
74+
uploadingStore.setState((state) => ({
75+
...state,
76+
uploadStatus: status,
77+
}));
78+
},
79+
}}
80+
>
81+
{children}
82+
83+
<ForbidLeaveWhenUploading />
84+
</UploadingContext.Provider>
85+
);
86+
}
87+
88+
// Separated to prevent rerendering whole tree
89+
function ForbidLeaveWhenUploading() {
90+
const { uploadingStore } = useUploadingContext();
91+
const uploadStatus = useStore(uploadingStore, (state) => state.uploadStatus);
5092

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

62103
window.addEventListener("beforeunload", handleBeforeUnload);
63104
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
64-
}, [state]);
105+
}, [uploadStatus]);
65106

66-
return (
67-
<UploadingContext.Provider
68-
value={{
69-
uploadStatus: state,
70-
setUploadStatus: setState,
71-
}}
72-
>
73-
{children}
74-
</UploadingContext.Provider>
75-
);
107+
return null;
76108
}

apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,6 @@ export const CapCard = ({
452452
"transition-opacity duration-200",
453453
uploadProgress && "opacity-30",
454454
)}
455-
userId={cap.ownerId}
456455
videoId={cap.id}
457456
alt={`${cap.name} Thumbnail`}
458457
/>

apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { userIsPro } from "@cap/utils";
55
import type { Folder } from "@cap/web-domain";
66
import { faUpload } from "@fortawesome/free-solid-svg-icons";
77
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8+
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
9+
import { useStore } from "@tanstack/react-store";
810
import { useRouter } from "next/navigation";
911
import { useRef, useState } from "react";
1012
import { toast } from "sonner";
@@ -15,6 +17,7 @@ import {
1517
useUploadingContext,
1618
} from "@/app/(org)/dashboard/caps/UploadingContext";
1719
import { UpgradeModal } from "@/components/UpgradeModal";
20+
import { imageUrlQuery } from "@/components/VideoThumbnail";
1821

1922
export const UploadCapButton = ({
2023
size = "md",
@@ -26,9 +29,11 @@ export const UploadCapButton = ({
2629
}) => {
2730
const { user } = useDashboardContext();
2831
const inputRef = useRef<HTMLInputElement>(null);
29-
const { uploadStatus, setUploadStatus } = useUploadingContext();
32+
const { uploadingStore, setUploadStatus } = useUploadingContext();
33+
const isUploading = useStore(uploadingStore, (s) => !!s.uploadStatus);
3034
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
3135
const router = useRouter();
36+
const queryClient = useQueryClient();
3237

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

50-
const ok = await legacyUploadCap(file, folderId, setUploadStatus);
55+
const ok = await legacyUploadCap(
56+
file,
57+
folderId,
58+
setUploadStatus,
59+
queryClient,
60+
);
5161
if (ok) router.refresh();
5262
if (inputRef.current) inputRef.current.value = "";
5363
};
5464

55-
const isUploading = !!uploadStatus;
56-
5765
return (
5866
<>
5967
<Button
@@ -86,6 +94,7 @@ async function legacyUploadCap(
8694
file: File,
8795
folderId: Folder.FolderId | undefined,
8896
setUploadStatus: (state: UploadStatus | undefined) => void,
97+
queryClient: QueryClient,
8998
) {
9099
const parser = await import("@remotion/media-parser");
91100
const webcodecs = await import("@remotion/webcodecs");
@@ -476,6 +485,7 @@ async function legacyUploadCap(
476485
xhr.onload = () => {
477486
if (xhr.status >= 200 && xhr.status < 300) {
478487
resolve();
488+
queryClient.refetchQueries(imageUrlQuery(uploadId));
479489
} else {
480490
reject(
481491
new Error(`Screenshot upload failed with status ${xhr.status}`),

apps/web/app/(org)/dashboard/caps/components/UploadPlaceholderCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

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

78
const { circumference } = getProgressCircleConfig();
89

910
export const UploadPlaceholderCard = () => {
10-
const { uploadStatus } = useUploadingContext();
11+
const { uploadingStore } = useUploadingContext();
12+
const uploadStatus = useStore(uploadingStore, (s) => s.uploadStatus);
1113
const strokeDashoffset = calculateStrokeDashoffset(
1214
uploadStatus &&
1315
(uploadStatus.status === "converting" ||

apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { Video } from "@cap/web-domain";
44
import { useQuery } from "@tanstack/react-query";
5+
import { useStore } from "@tanstack/react-store";
56
import { Effect, Exit } from "effect";
67
import { useRouter } from "next/navigation";
78
import { useMemo, useRef, useState } from "react";
@@ -13,7 +14,10 @@ import type { VideoData } from "../../../caps/Caps";
1314
import { CapCard } from "../../../caps/components/CapCard/CapCard";
1415
import { SelectedCapsBar } from "../../../caps/components/SelectedCapsBar";
1516
import { UploadPlaceholderCard } from "../../../caps/components/UploadPlaceholderCard";
16-
import { useUploadingContext } from "../../../caps/UploadingContext";
17+
import {
18+
useUploadingContext,
19+
useUploadingStatus,
20+
} from "../../../caps/UploadingContext";
1721

1822
interface FolderVideosSectionProps {
1923
initialVideos: VideoData;
@@ -27,13 +31,8 @@ export default function FolderVideosSection({
2731
cardType = "default",
2832
}: FolderVideosSectionProps) {
2933
const router = useRouter();
30-
const { uploadStatus } = useUploadingContext();
3134
const { user } = useDashboardContext();
3235

33-
const isUploading = uploadStatus !== undefined;
34-
const uploadingCapId =
35-
uploadStatus && "capId" in uploadStatus ? uploadStatus.capId : null;
36-
3736
const [selectedCaps, setSelectedCaps] = useState<Video.VideoId[]>([]);
3837
const previousCountRef = useRef<number>(0);
3938

@@ -159,6 +158,7 @@ export default function FolderVideosSection({
159158
refetchOnMount: true,
160159
});
161160

161+
const [isUploading, uploadingCapId] = useUploadingStatus();
162162
const visibleVideos = useMemo(
163163
() =>
164164
isUploading && uploadingCapId

apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ const VideoCard: React.FC<VideoCardProps> = memo(
100100
>
101101
<VideoThumbnail
102102
imageClass="w-full h-full transition-all duration-200 group-hover:scale-105"
103-
userId={video.ownerId}
104103
videoId={video.id}
105104
alt={`${video.name} Thumbnail`}
106105
objectFit="cover"

apps/web/app/api/thumbnail/route.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ export const revalidate = 0;
1010

1111
export async function GET(request: NextRequest) {
1212
const { searchParams } = request.nextUrl;
13-
const userId = searchParams.get("userId");
1413
const videoId = searchParams.get("videoId");
1514
const origin = request.headers.get("origin") as string;
1615

17-
if (!userId || !videoId) {
16+
if (!videoId)
1817
return new Response(
1918
JSON.stringify({
2019
error: true,
@@ -25,9 +24,8 @@ export async function GET(request: NextRequest) {
2524
headers: getHeaders(origin),
2625
},
2726
);
28-
}
2927

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

39-
if (query.length === 0) {
40-
return new Response(
41-
JSON.stringify({ error: true, message: "Video does not exist" }),
42-
{
43-
status: 401,
44-
headers: getHeaders(origin),
45-
},
46-
);
47-
}
48-
49-
const result = query[0];
50-
if (!result?.video) {
37+
if (!query)
5138
return new Response(
5239
JSON.stringify({ error: true, message: "Video not found" }),
5340
{
54-
status: 401,
41+
status: 404,
5542
headers: getHeaders(origin),
5643
},
5744
);
58-
}
5945

60-
const prefix = `${userId}/${videoId}/`;
61-
const bucketProvider = await createBucketProvider(result.bucket);
46+
const prefix = `${query.video.ownerId}/${query.video.id}/`;
47+
const bucketProvider = await createBucketProvider(query.bucket);
6248

6349
try {
6450
const listResponse = await bucketProvider.listObjects({
6551
prefix: prefix,
6652
});
6753
const contents = listResponse.Contents || [];
6854

69-
const thumbnailKey = contents.find((item: any) =>
55+
const thumbnailKey = contents.find((item) =>
7056
item.Key?.endsWith("screen-capture.jpg"),
7157
)?.Key;
7258

73-
if (!thumbnailKey) {
59+
if (!thumbnailKey)
7460
return new Response(
7561
JSON.stringify({
7662
error: true,
@@ -81,7 +67,6 @@ export async function GET(request: NextRequest) {
8167
headers: getHeaders(origin),
8268
},
8369
);
84-
}
8570

8671
const thumbnailUrl = await bucketProvider.getSignedObjectUrl(thumbnailKey);
8772

0 commit comments

Comments
 (0)