1"use client";
2
3
25import { useCallback, useRef } from "react";
26import { useQuery, useQueryClient } from "@tanstack/react-query";
27import { toast } from "@/stores/ui-store";
28import { apiFetch, blockApi, workspaceApi } from "@/lib/api";
29import { effectiveMaxFileSize } from "./api";
30import { uploadFile, UploadError } from "./upload";
31import { useUploadStore } from "./upload-store";
32
33export interface UploadAnchor {
34 parentId: string | null;
35
36 position?: string | null;
37
38 updateBlockId?: string;
39
46 replaceFileId?: string;
47}
48
49interface RawWorkspace {
50 billingPlan?: string;
51 usageLimits?: { maxFileSize?: number } | null;
52}
53
54export function useFileUploads(workspaceId: string | null | undefined, pageId: string | null) {
55 const qc = useQueryClient();
56 const lastLimit = useRef<{ ws: string; at: number; max: number } | null>(null);
57 const maxFileSize = useQuery({
58 queryKey: ["files", "plan-limit", workspaceId],
59 queryFn: async () => {
60
61 const cached = lastLimit.current;
62 if (cached && cached.ws === workspaceId && Date.now() - cached.at < 5 * 60_000) return cached.max;
63 if (!workspaceId) return effectiveMaxFileSize("free", null);
64 const ws = (await workspaceApi.get(workspaceId)) as unknown as RawWorkspace;
65
66 let max = effectiveMaxFileSize(ws.billingPlan ?? "free", ws.usageLimits ?? null);
67 try {
68
69
70
71
72 const eff = await apiFetch<{ effective: { maxFileSizeBytes?: number } }>(
73 `/workspaces/${workspaceId}/policy/effective`
74 );
75 const orgCap = eff.effective?.maxFileSizeBytes;
76 if (typeof orgCap === "number" && orgCap > 0) max = Math.min(max, orgCap);
77 } catch {
78
79 }
80 lastLimit.current = { ws: workspaceId, at: Date.now(), max };
81 return max;
82 },
83 enabled: Boolean(workspaceId),
84 staleTime: Infinity,
85 retry: false,
86 });
87
88 const start = useCallback(
89 async (file: File, anchor: UploadAnchor) => {
90 const max = maxFileSize.data ?? effectiveMaxFileSize("free", null);
91 const ws = workspaceId ?? undefined;
92
93 const handle = uploadFile(
94 file,
95 {
96 pageId: pageId ?? null,
97 workspaceId: ws,
98 maxFileSize: max,
99 replaceFileId: anchor.replaceFileId,
100 onSession: (fileId) => {
101
102
103
104 useUploadStore.getState().registerCancel(fileId, handle.cancel);
105 const insert = async () => {
106 if (anchor.updateBlockId) {
107 await blockApi.update(anchor.updateBlockId, {
108 type: "file",
109 properties: { fileId, name: file.name },
110 });
111 } else {
112 await blockApi.insert({
113 pageId: pageId as string,
114 parentId: anchor.parentId,
115 type: "file",
116 properties: { fileId, name: file.name },
117
118
119 ...(anchor.position ? { position: anchor.position } : {}),
120 });
121 }
122
123
124
125
126
127
128 if (pageId) qc.invalidateQueries({ queryKey: ["pages", pageId, "blocks"] });
129 };
130 void insert().catch((e) =>
131 toast("error", "Could not add the file block", e instanceof Error ? e.message : undefined),
132 );
133 },
134 onProgress: (p) => {
135 const id = handle.fileId;
136 if (id) useUploadStore.getState().set(id, p);
137 },
138 },
139 );
140
141 try {
142 await handle.promise;
143
144 if (handle.fileId) useUploadStore.getState().clear(handle.fileId);
145 } catch (err) {
146 if (handle.fileId) useUploadStore.getState().clear(handle.fileId);
147 if (err instanceof UploadError && err.code === "CANCELLED") {
148
149 if (anchor.updateBlockId) {
150 void blockApi.remove(anchor.updateBlockId).catch(() => undefined);
151 }
152 toast("info", "Upload cancelled");
153 return;
154 }
155 if (err instanceof UploadError && err.code === "FILE_TOO_LARGE") {
156 toast("error", "File too large", err.message);
157 return;
158 }
159 toast("error", "Upload failed", err instanceof Error ? err.message : "Unknown error");
160 }
161 },
162 [qc, maxFileSize.data, pageId, workspaceId],
163 );
164
165 return { start, maxFileSize: maxFileSize.data ?? null, limitLoading: maxFileSize.isLoading };
166}
167