@devultur/convex 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +100 -5
  2. package/dist/{chunk-JZYKPLI4.js → chunk-RDVBE3G3.js} +20 -1
  3. package/dist/component/_generated/api.d.ts +0 -1
  4. package/dist/component/_generated/api.js.map +1 -1
  5. package/dist/component/_generated/component.d.ts +0 -1
  6. package/dist/component/_generated/component.js.map +1 -1
  7. package/dist/component/_generated/dataModel.d.ts +0 -1
  8. package/dist/component/_generated/dataModel.js.map +1 -1
  9. package/dist/component/_generated/server.d.ts +0 -1
  10. package/dist/component/_generated/server.js.map +1 -1
  11. package/dist/component/convex.config.d.ts +0 -1
  12. package/dist/component/convex.config.js.map +1 -1
  13. package/dist/component/http.d.ts +0 -1
  14. package/dist/component/http.js.map +1 -1
  15. package/dist/component/library.d.ts +0 -1
  16. package/dist/component/library.js.map +1 -1
  17. package/dist/component/lock.d.ts +0 -1
  18. package/dist/component/lock.js.map +1 -1
  19. package/dist/component/lockValidators.d.ts +0 -1
  20. package/dist/component/lockValidators.js.map +1 -1
  21. package/dist/component/mediaClient.d.ts +0 -1
  22. package/dist/component/mediaClient.js.map +1 -1
  23. package/dist/component/mediaMeta.d.ts +0 -1
  24. package/dist/component/mediaMeta.js.map +1 -1
  25. package/dist/component/metadata.d.ts +0 -1
  26. package/dist/component/metadata.js.map +1 -1
  27. package/dist/component/public.d.ts +0 -1
  28. package/dist/component/public.js.map +1 -1
  29. package/dist/component/schema.d.ts +0 -1
  30. package/dist/component/schema.js.map +1 -1
  31. package/dist/component/uploadKeys.d.ts +0 -1
  32. package/dist/component/uploadKeys.js.map +1 -1
  33. package/dist/component/uploads.d.ts +0 -1
  34. package/dist/component/uploads.js.map +1 -1
  35. package/dist/component/workerErrors.d.ts +0 -1
  36. package/dist/component/workerErrors.js.map +1 -1
  37. package/dist/index.d.ts +33 -12
  38. package/dist/index.js +22 -4
  39. package/dist/locales/es-formal.d.ts +7 -3
  40. package/dist/locales/es-formal.js +6 -3
  41. package/dist/locales/es.d.ts +7 -3
  42. package/dist/locales/es.js +5 -3
  43. package/dist/{preview-labels-B-2mra1h.d.ts → preview-labels-C2nUUY_f.d.ts} +1 -1
  44. package/dist/react.d.ts +133 -92
  45. package/dist/react.js +310 -235
  46. package/dist/types-C0ruHUcu.d.ts +196 -0
  47. package/package.json +4 -3
  48. package/dist/component/_generated/api.d.ts.map +0 -1
  49. package/dist/component/_generated/component.d.ts.map +0 -1
  50. package/dist/component/_generated/dataModel.d.ts.map +0 -1
  51. package/dist/component/_generated/server.d.ts.map +0 -1
  52. package/dist/component/convex.config.d.ts.map +0 -1
  53. package/dist/component/http.d.ts.map +0 -1
  54. package/dist/component/library.d.ts.map +0 -1
  55. package/dist/component/lock.d.ts.map +0 -1
  56. package/dist/component/lockValidators.d.ts.map +0 -1
  57. package/dist/component/mediaClient.d.ts.map +0 -1
  58. package/dist/component/mediaMeta.d.ts.map +0 -1
  59. package/dist/component/metadata.d.ts.map +0 -1
  60. package/dist/component/public.d.ts.map +0 -1
  61. package/dist/component/schema.d.ts.map +0 -1
  62. package/dist/component/uploadKeys.d.ts.map +0 -1
  63. package/dist/component/uploads.d.ts.map +0 -1
  64. package/dist/component/workerErrors.d.ts.map +0 -1
  65. package/dist/types-LrusGVDf.d.ts +0 -62
package/dist/react.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  } from "./chunk-YYOZ3PDH.js";
27
27
 
28
28
  // src/react.tsx
29
- import React22 from "react";
29
+ import React24 from "react";
30
30
  import {
31
31
  buildMediaUrl as buildMediaUrl2,
32
32
  CaptionLocaleLabel,
@@ -36,12 +36,12 @@ import {
36
36
  mediaUrl as mediaUrl4,
37
37
  visibilityFromKey
38
38
  } from "@devultur/core";
39
- import {
40
- UploadHttpError,
41
- uploadFileWithProgress
42
- } from "@devultur/react";
39
+ import { UploadHttpError, uploadFileWithProgress } from "@devultur/react";
43
40
  import { useAction as useAction2, useConvex, useMutation, useQuery } from "convex/react";
44
41
 
42
+ // src/devultur-context.ts
43
+ import React from "react";
44
+
45
45
  // src/capability-error.ts
46
46
  var DevulturCapabilityError = class extends Error {
47
47
  code = "capability_unavailable";
@@ -55,6 +55,24 @@ var DevulturCapabilityError = class extends Error {
55
55
  }
56
56
  };
57
57
 
58
+ // src/devultur-context.ts
59
+ var DevulturContext = /* @__PURE__ */ React.createContext(null);
60
+ var DevulturApiContext = /* @__PURE__ */ React.createContext(null);
61
+ function useDevultur() {
62
+ const context = React.use(DevulturContext);
63
+ if (!context) throw new Error("useDevultur must be used within <DevulturProvider> or <DevulturViewerProvider>");
64
+ return context;
65
+ }
66
+ function useDevulturOptional() {
67
+ return React.use(DevulturContext);
68
+ }
69
+ function useDevulturApi() {
70
+ const api = React.use(DevulturApiContext);
71
+ if (api) return api;
72
+ if (React.use(DevulturContext)) throw new DevulturCapabilityError("api");
73
+ throw new Error("useDevulturApi must be used within <DevulturProvider>");
74
+ }
75
+
58
76
  // src/media-fetch.ts
59
77
  import { SESSION_HEADER } from "@devultur/core";
60
78
 
@@ -157,17 +175,16 @@ async function readCapped(body, maxBytes) {
157
175
  }
158
176
 
159
177
  // src/media-session.ts
160
- import React2 from "react";
178
+ import React3 from "react";
161
179
  import {
162
180
  buildMediaUrl,
163
181
  mediaCaptionPath,
164
182
  mediaUrl,
165
183
  SESSION_HEADER as SESSION_HEADER2
166
184
  } from "@devultur/core";
167
- import { useAction } from "convex/react";
168
185
 
169
186
  // src/viewer-token.ts
170
- import React from "react";
187
+ import React2 from "react";
171
188
  import { DEFAULT_TOKEN_EXPIRY_SECONDS } from "@devultur/core";
172
189
  import { convexToJson } from "convex/values";
173
190
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
@@ -193,30 +210,34 @@ function sortKeys(json) {
193
210
  function tokenArgsKey(tokenArgs) {
194
211
  return JSON.stringify(sortKeys(convexToJson(tokenArgs ?? {})));
195
212
  }
196
- function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
213
+ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs, isLazy = false }) {
197
214
  const argsKey = tokenArgsKey(tokenArgs);
198
- const stableArgs = React.useMemo(() => tokenArgs ?? {}, [argsKey]);
199
- const [issued, setIssued] = React.useState(null);
200
- const [attempt, setAttempt] = React.useState(null);
215
+ const stableArgs = React2.useMemo(() => tokenArgs ?? {}, [argsKey]);
216
+ const [issued, setIssued] = React2.useState(null);
217
+ const [attempt, setAttempt] = React2.useState(null);
218
+ const [isRequested, setIsRequested] = React2.useState(!isLazy);
201
219
  const token = issued?.argsKey === argsKey ? issued.token : null;
202
220
  const currentAttempt = attempt?.argsKey === argsKey ? attempt : null;
203
- const currentArgsKeyRef = React.useRef(argsKey);
204
- const refreshTimerRef = React.useRef(null);
205
- const inFlightRef = React.useRef(null);
206
- React.useEffect(() => {
221
+ const currentArgsKeyRef = React2.useRef(argsKey);
222
+ const refreshTimerRef = React2.useRef(null);
223
+ const inFlightRef = React2.useRef(null);
224
+ const isMountedRef = React2.useRef(true);
225
+ React2.useEffect(() => {
207
226
  currentArgsKeyRef.current = argsKey;
208
227
  }, [argsKey]);
209
- React.useEffect(() => {
228
+ React2.useEffect(() => {
229
+ isMountedRef.current = true;
210
230
  return () => {
231
+ isMountedRef.current = false;
211
232
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
212
233
  };
213
234
  }, []);
214
- const request = React.useCallback(async () => {
235
+ const request = React2.useCallback(async () => {
215
236
  const renew = () => {
216
237
  request().catch((error) => console.warn("Devultur: could not renew the viewer token", error));
217
238
  };
218
239
  const result = await issueViewerToken({ ...stableArgs, sessionSecret });
219
- if (currentArgsKeyRef.current !== argsKey) return null;
240
+ if (!isMountedRef.current || currentArgsKeyRef.current !== argsKey) return null;
220
241
  const { tokenValue, ttlMs } = extractTokenResult(result);
221
242
  setIssued({ token: tokenValue, argsKey });
222
243
  setAttempt(null);
@@ -224,7 +245,7 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
224
245
  refreshTimerRef.current = setTimeout(renew, Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS));
225
246
  return tokenValue;
226
247
  }, [issueViewerToken, stableArgs, argsKey, sessionSecret]);
227
- const issueOnce = React.useCallback(() => {
248
+ const issueOnce = React2.useCallback(() => {
228
249
  const inFlight = inFlightRef.current;
229
250
  if (inFlight?.argsKey === argsKey) return inFlight.promise;
230
251
  setAttempt({ argsKey, status: "loading" });
@@ -240,50 +261,62 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
240
261
  return promise;
241
262
  }, [request, argsKey]);
242
263
  const hasAttempt = currentAttempt !== null;
243
- React.useEffect(() => {
244
- if (token || hasAttempt) return;
264
+ React2.useEffect(() => {
265
+ if (!isRequested || token || hasAttempt) return;
245
266
  issueOnce().catch(() => {
246
267
  });
247
- }, [token, hasAttempt, issueOnce]);
248
- const reissueToken = React.useCallback(
268
+ }, [isRequested, token, hasAttempt, issueOnce]);
269
+ const requestToken = React2.useCallback(() => setIsRequested(true), []);
270
+ const reissueToken = React2.useCallback(
249
271
  () => issueOnce().catch(() => {
250
272
  setIssued((current) => current?.argsKey === argsKey ? null : current);
251
273
  return null;
252
274
  }),
253
275
  [issueOnce, argsKey]
254
276
  );
255
- const retryToken = React.useCallback(() => issueOnce().catch(() => null), [issueOnce]);
277
+ const retryToken = React2.useCallback(() => {
278
+ setIsRequested(true);
279
+ return issueOnce().catch(() => null);
280
+ }, [issueOnce]);
256
281
  const tokenStatus = token ? "ready" : currentAttempt?.status ?? "idle";
257
282
  const tokenError = !token && currentAttempt?.status === "error" ? currentAttempt.error : null;
258
- return { token, tokenStatus, tokenError, reissueToken, retryToken };
283
+ return { token, tokenStatus, tokenError, reissueToken, retryToken, requestToken };
259
284
  }
260
285
 
261
286
  // src/media-session.ts
262
- function useMediaSession({ issueViewerToken, tokenArgs, baseUrl }) {
263
- const [sessionSecret] = React2.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
264
- const issueViewerTokenAction = useAction(issueViewerToken);
265
- const { token, tokenStatus, tokenError, reissueToken, retryToken } = useViewerToken({
266
- issueViewerToken: issueViewerTokenAction,
287
+ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy }) {
288
+ const [sessionSecret] = React3.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
289
+ const { token, tokenStatus, tokenError, reissueToken, retryToken, requestToken } = useViewerToken({
290
+ issueViewerToken,
267
291
  sessionSecret,
268
- tokenArgs
292
+ tokenArgs,
293
+ isLazy
269
294
  });
270
- const url = React2.useCallback(
295
+ const hasRequestedRef = React3.useRef(!isLazy);
296
+ const requestTokenAfterRender = React3.useCallback(() => {
297
+ if (hasRequestedRef.current) return;
298
+ hasRequestedRef.current = true;
299
+ queueMicrotask(requestToken);
300
+ }, [requestToken]);
301
+ const url = React3.useCallback(
271
302
  (media, options) => {
272
303
  if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
304
+ if (media.visibility === "private" && !token) requestTokenAfterRender();
273
305
  return mediaUrl(baseUrl, media, { ...options, token: token ?? void 0 });
274
306
  },
275
- [baseUrl, token]
307
+ [baseUrl, token, requestTokenAfterRender]
276
308
  );
277
- const getCaptionsText = React2.useCallback(
309
+ const getCaptionsText = React3.useCallback(
278
310
  async (media, locale) => {
279
311
  if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
280
312
  const path = mediaCaptionPath(media, locale);
281
313
  if (!path) throw new Error("getCaptionsText: the reference is not a video");
282
314
  const isPrivate = media.visibility === "private";
283
- const fetchWith = (viewerToken) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken }), {
315
+ const fetchWith = (viewerToken2) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken2 }), {
284
316
  headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
285
317
  });
286
- let response = await fetchWith(isPrivate ? token ?? void 0 : void 0);
318
+ const viewerToken = isPrivate ? token ?? await retryToken() : null;
319
+ let response = await fetchWith(viewerToken ?? void 0);
287
320
  if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
288
321
  const fresh = await reissueToken();
289
322
  if (fresh) response = await fetchWith(fresh);
@@ -291,23 +324,42 @@ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl }) {
291
324
  if (!response.ok) throw new Error(`Captions fetch failed: ${response.status}`);
292
325
  return response.text();
293
326
  },
294
- [baseUrl, token, reissueToken, sessionSecret]
327
+ [baseUrl, token, reissueToken, retryToken, sessionSecret]
295
328
  );
296
- return React2.useMemo(
329
+ return React3.useMemo(
297
330
  () => ({
298
331
  token,
299
332
  tokenStatus,
300
333
  tokenError,
301
334
  retryToken,
335
+ requestToken,
302
336
  baseUrl: baseUrl ?? null,
303
337
  sessionSecret,
304
338
  url,
305
339
  getCaptionsText,
306
340
  reissueToken
307
341
  }),
308
- [token, tokenStatus, tokenError, retryToken, baseUrl, sessionSecret, url, getCaptionsText, reissueToken]
342
+ [
343
+ token,
344
+ tokenStatus,
345
+ tokenError,
346
+ retryToken,
347
+ requestToken,
348
+ baseUrl,
349
+ sessionSecret,
350
+ url,
351
+ getCaptionsText,
352
+ reissueToken
353
+ ]
309
354
  );
310
355
  }
356
+ function useRequestTokenWhen(session, isNeeded) {
357
+ const { requestToken, token } = session;
358
+ const hasToken = token !== null;
359
+ React3.useEffect(() => {
360
+ if (isNeeded && !hasToken) requestToken();
361
+ }, [isNeeded, hasToken, requestToken]);
362
+ }
311
363
 
312
364
  // src/media-upload-derive.ts
313
365
  function deriveUploadState(local, videoState, retry) {
@@ -352,7 +404,7 @@ function deriveUploadState(local, videoState, retry) {
352
404
  }
353
405
 
354
406
  // src/crop-upload.tsx
355
- import React3 from "react";
407
+ import React4 from "react";
356
408
  import { ImageCropper } from "@devultur/react";
357
409
  import { jsx, jsxs } from "react/jsx-runtime";
358
410
  var DEFAULT_LABELS = {
@@ -376,18 +428,18 @@ function CropUpload({
376
428
  children
377
429
  }) {
378
430
  const text = { ...DEFAULT_LABELS, ...labels };
379
- const [pendingFile, setPendingFile] = React3.useState(null);
380
- const inputRef = React3.useRef(null);
431
+ const [pendingFile, setPendingFile] = React4.useState(null);
432
+ const inputRef = React4.useRef(null);
381
433
  const { upload, state, cancel, reset } = useMediaUpload({ visibility, replaces, onComplete });
382
- const pickFile = React3.useCallback(() => inputRef.current?.click(), []);
383
- const handleCrop = React3.useCallback(
434
+ const pickFile = React4.useCallback(() => inputRef.current?.click(), []);
435
+ const handleCrop = React4.useCallback(
384
436
  (baked) => {
385
437
  setPendingFile(null);
386
438
  upload(baked);
387
439
  },
388
440
  [upload]
389
441
  );
390
- const resetAll = React3.useCallback(() => {
442
+ const resetAll = React4.useCallback(() => {
391
443
  setPendingFile(null);
392
444
  reset();
393
445
  }, [reset]);
@@ -440,7 +492,7 @@ function CropUpload({
440
492
  }
441
493
 
442
494
  // src/document-renderers.tsx
443
- import React4 from "react";
495
+ import React5 from "react";
444
496
 
445
497
  // src/attachment-icons.tsx
446
498
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -495,7 +547,7 @@ function AttachmentCard({
495
547
  labels,
496
548
  ...articleProps
497
549
  }) {
498
- const headingId = React4.useId();
550
+ const headingId = React5.useId();
499
551
  if (!media) return null;
500
552
  const text = mergeAttachmentLabels(labels);
501
553
  const kind = attachmentKindOf(media, contentType);
@@ -513,7 +565,7 @@ function AttachmentCard({
513
565
  }
514
566
 
515
567
  // src/image.tsx
516
- import React5 from "react";
568
+ import React6 from "react";
517
569
  import { mediaUrl as mediaUrl2 } from "@devultur/core";
518
570
 
519
571
  // src/responsive-image.ts
@@ -607,7 +659,7 @@ function Image({
607
659
  }
608
660
  if (!src) return /* @__PURE__ */ jsx4(Fragment, { children: fallback });
609
661
  if (render) {
610
- return React5.cloneElement(render, {
662
+ return React6.cloneElement(render, {
611
663
  style: { ...render.props.style, backgroundImage: `url("${src}")` }
612
664
  });
613
665
  }
@@ -627,26 +679,28 @@ function Image({
627
679
  );
628
680
  }
629
681
  function usePrivateImage(media, transforms, onLoadError) {
630
- const { token, tokenStatus, tokenError, baseUrl, reissueToken, sessionSecret } = useDevultur();
631
- const [isFailed, setIsFailed] = React5.useState(false);
632
- const [blobSrc, setBlobSrc] = React5.useState(null);
633
- const sessionRef = React5.useRef({ token, reissueToken, onLoadError });
634
- React5.useEffect(() => {
682
+ const devultur = useDevultur();
683
+ const { token, tokenStatus, tokenError, baseUrl, reissueToken, sessionSecret } = devultur;
684
+ const [isFailed, setIsFailed] = React6.useState(false);
685
+ const [blobSrc, setBlobSrc] = React6.useState(null);
686
+ const sessionRef = React6.useRef({ token, reissueToken, onLoadError });
687
+ React6.useEffect(() => {
635
688
  sessionRef.current = { token, reissueToken, onLoadError };
636
689
  });
637
690
  const isPrivate = media?.visibility === "private";
691
+ useRequestTokenWhen(devultur, isPrivate);
638
692
  const hasToken = token !== null;
639
693
  const isTokenUnavailable = isPrivate && tokenStatus === "error";
640
694
  const { width, height, fit, format, quality } = transforms;
641
- React5.useEffect(() => {
695
+ React6.useEffect(() => {
642
696
  setIsFailed(false);
643
697
  setBlobSrc(null);
644
698
  }, [media]);
645
- React5.useEffect(() => {
699
+ React6.useEffect(() => {
646
700
  if (!isTokenUnavailable || !media) return;
647
701
  sessionRef.current.onLoadError?.(tokenUnavailableError(tokenError));
648
702
  }, [isTokenUnavailable, media, tokenError]);
649
- React5.useEffect(() => {
703
+ React6.useEffect(() => {
650
704
  if (!isPrivate || isFailed || !media || !hasToken) return;
651
705
  let cancelled = false;
652
706
  let objectUrl = null;
@@ -685,21 +739,21 @@ function usePrivateImage(media, transforms, onLoadError) {
685
739
  if (objectUrl) URL.revokeObjectURL(objectUrl);
686
740
  };
687
741
  }, [isPrivate, isFailed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
688
- const handleError = React5.useCallback(() => setIsFailed(true), []);
742
+ const handleError = React6.useCallback(() => setIsFailed(true), []);
689
743
  return { blobSrc, isFailed: isFailed || isTokenUnavailable, handleError };
690
744
  }
691
745
 
692
746
  // src/media-library.tsx
693
- import React14 from "react";
747
+ import React15 from "react";
694
748
 
695
749
  // src/media-library-item.tsx
696
- import React11 from "react";
750
+ import React12 from "react";
697
751
 
698
752
  // src/pdf-thumbnail.tsx
699
- import React10 from "react";
753
+ import React11 from "react";
700
754
 
701
755
  // src/media-resource.ts
702
- import React6 from "react";
756
+ import React7 from "react";
703
757
  import { mediaPath, mediaUrl as mediaUrl3 } from "@devultur/core";
704
758
  var IDLE = { value: null, contentType: null, isLoading: false, error: null };
705
759
  var LOADING = { value: null, contentType: null, isLoading: true, error: null };
@@ -707,19 +761,20 @@ function useMediaResource(media, options) {
707
761
  const devultur = useDevultur();
708
762
  const { isEnabled, maxBytes, contentType } = options;
709
763
  const isPrivate = media?.visibility === "private";
764
+ useRequestTokenWhen(devultur, isPrivate && isEnabled);
710
765
  const isReady = !isPrivate || devultur.token !== null;
711
766
  const isTokenUnavailable = isPrivate && devultur.tokenStatus === "error";
712
- const tokenFailure = React6.useMemo(
767
+ const tokenFailure = React7.useMemo(
713
768
  () => isTokenUnavailable ? tokenUnavailableError(devultur.tokenError) : null,
714
769
  [isTokenUnavailable, devultur.tokenError]
715
770
  );
716
771
  const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
717
- const [settled, setSettled] = React6.useState(null);
718
- const inputsRef = React6.useRef({ media, devultur, options });
719
- React6.useEffect(() => {
772
+ const [settled, setSettled] = React7.useState(null);
773
+ const inputsRef = React7.useRef({ media, devultur, options });
774
+ React7.useEffect(() => {
720
775
  inputsRef.current = { media, devultur, options };
721
776
  });
722
- React6.useEffect(() => {
777
+ React7.useEffect(() => {
723
778
  if (!requestKey || !isReady) return;
724
779
  const { media: target, devultur: context, options: current } = inputsRef.current;
725
780
  if (!target || !context.baseUrl) return;
@@ -757,7 +812,7 @@ function useMediaResource(media, options) {
757
812
  }
758
813
 
759
814
  // src/pdf-context.tsx
760
- import React7 from "react";
815
+ import React8 from "react";
761
816
 
762
817
  // src/preview-labels.ts
763
818
  var ERROR_TEXT = {
@@ -793,14 +848,14 @@ function mergePreviewLabels(...sources) {
793
848
 
794
849
  // src/pdf-context.tsx
795
850
  import { jsx as jsx5 } from "react/jsx-runtime";
796
- var PdfPreviewContext = React7.createContext({});
851
+ var PdfPreviewContext = /* @__PURE__ */ React8.createContext({});
797
852
  function PdfPreviewProvider({
798
853
  pdfjs,
799
854
  workerSrc,
800
855
  labels,
801
856
  children
802
857
  }) {
803
- const config = React7.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
858
+ const config = React8.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
804
859
  return /* @__PURE__ */ jsx5(PdfPreviewContext.Provider, { value: config, children });
805
860
  }
806
861
  var loadedModules = /* @__PURE__ */ new WeakMap();
@@ -812,9 +867,9 @@ function loadOnce(loader) {
812
867
  return loading;
813
868
  }
814
869
  function usePdfjs(workerSrcOverride) {
815
- const { pdfjs: loader, workerSrc } = React7.useContext(PdfPreviewContext);
816
- const [loaded, setLoaded] = React7.useState(null);
817
- React7.useEffect(() => {
870
+ const { pdfjs: loader, workerSrc } = React8.useContext(PdfPreviewContext);
871
+ const [loaded, setLoaded] = React8.useState(null);
872
+ React8.useEffect(() => {
818
873
  if (!loader) return;
819
874
  let isCurrent = true;
820
875
  loadOnce(loader).then((pdfjs) => {
@@ -830,8 +885,8 @@ function usePdfjs(workerSrcOverride) {
830
885
  return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
831
886
  }
832
887
  function usePdfPageCounter() {
833
- const { pdfjs: loader, workerSrc } = React7.useContext(PdfPreviewContext);
834
- return React7.useMemo(() => {
888
+ const { pdfjs: loader, workerSrc } = React8.useContext(PdfPreviewContext);
889
+ return React8.useMemo(() => {
835
890
  if (!loader) return null;
836
891
  return async (file) => {
837
892
  const pdfjs = await loadOnce(loader);
@@ -841,15 +896,15 @@ function usePdfPageCounter() {
841
896
  }, [loader, workerSrc]);
842
897
  }
843
898
  function usePreviewLabels(labels) {
844
- const providerLabels = React7.useContext(PdfPreviewContext).labels;
899
+ const providerLabels = React8.useContext(PdfPreviewContext).labels;
845
900
  return mergePreviewLabels(providerLabels, labels);
846
901
  }
847
902
 
848
903
  // src/pdf-hooks.ts
849
- import React8 from "react";
904
+ import React9 from "react";
850
905
  function useIsVisible(ref, rootMargin = "200px") {
851
- const [isVisible, setIsVisible] = React8.useState(false);
852
- React8.useEffect(() => {
906
+ const [isVisible, setIsVisible] = React9.useState(false);
907
+ React9.useEffect(() => {
853
908
  const element = ref.current;
854
909
  if (isVisible || !element) return;
855
910
  if (typeof IntersectionObserver === "undefined") {
@@ -870,12 +925,12 @@ function useIsVisible(ref, rootMargin = "200px") {
870
925
  return isVisible;
871
926
  }
872
927
  function usePdfDocument({ pdfjs, bytes, workerSrc }) {
873
- const [state, setState] = React8.useState({
928
+ const [state, setState] = React9.useState({
874
929
  bytes: null,
875
930
  document: null,
876
931
  error: null
877
932
  });
878
- React8.useEffect(() => {
933
+ React9.useEffect(() => {
879
934
  let isCurrent = true;
880
935
  const loadingTask = openPdf(pdfjs, bytes, workerSrc);
881
936
  loadingTask.promise.then((document2) => {
@@ -894,8 +949,8 @@ function usePdfDocument({ pdfjs, bytes, workerSrc }) {
894
949
  }
895
950
  function usePageRender(options) {
896
951
  const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
897
- const [state, setState] = React8.useState({ isRendered: false, text: "" });
898
- React8.useEffect(() => {
952
+ const [state, setState] = React9.useState({ isRendered: false, text: "" });
953
+ React9.useEffect(() => {
899
954
  const canvas = canvasRef.current;
900
955
  if (!document2 || !isEnabled || !canvas) return;
901
956
  let isCurrent = true;
@@ -925,7 +980,7 @@ function usePageRender(options) {
925
980
  }
926
981
 
927
982
  // src/pdf-preview.tsx
928
- import React9 from "react";
983
+ import React10 from "react";
929
984
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
930
985
  var VISUALLY_HIDDEN = {
931
986
  position: "absolute",
@@ -971,9 +1026,9 @@ function PdfPreview(props) {
971
1026
  function PdfCanvasDocument(props) {
972
1027
  const { pdfjs, workerSrc, bytes, label, className, renderFallback, loading, labels, maxPages } = props;
973
1028
  const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
974
- const containerRef = React9.useRef(null);
975
- const [width, setWidth] = React9.useState(null);
976
- React9.useLayoutEffect(() => {
1029
+ const containerRef = React10.useRef(null);
1030
+ const [width, setWidth] = React10.useState(null);
1031
+ React10.useLayoutEffect(() => {
977
1032
  if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
978
1033
  }, [document2]);
979
1034
  if (error) return /* @__PURE__ */ jsx6(Fragment2, { children: renderFallback(renderFailedError(error)) });
@@ -1009,8 +1064,8 @@ function PdfCanvasDocument(props) {
1009
1064
  );
1010
1065
  }
1011
1066
  function PdfPageView({ document: document2, pageNumber, width, label }) {
1012
- const pageRef = React9.useRef(null);
1013
- const canvasRef = React9.useRef(null);
1067
+ const pageRef = React10.useRef(null);
1068
+ const canvasRef = React10.useRef(null);
1014
1069
  const isVisible = useIsVisible(pageRef);
1015
1070
  const { isRendered, text } = usePageRender({
1016
1071
  document: document2,
@@ -1027,8 +1082,8 @@ function PdfPageView({ document: document2, pageNumber, width, label }) {
1027
1082
  ] });
1028
1083
  }
1029
1084
  function PdfFrame({ bytes, label, className }) {
1030
- const [objectUrl, setObjectUrl] = React9.useState(null);
1031
- React9.useEffect(() => {
1085
+ const [objectUrl, setObjectUrl] = React10.useState(null);
1086
+ React10.useEffect(() => {
1032
1087
  const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
1033
1088
  setObjectUrl(url);
1034
1089
  return () => URL.revokeObjectURL(url);
@@ -1043,7 +1098,7 @@ var DEFAULT_THUMBNAIL_WIDTH = 96;
1043
1098
  var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
1044
1099
  function PdfThumbnail(props) {
1045
1100
  const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
1046
- const containerRef = React10.useRef(null);
1101
+ const containerRef = React11.useRef(null);
1047
1102
  const isVisible = useIsVisible(containerRef);
1048
1103
  const pdfjsState = usePdfjs(props.pdfWorkerSrc);
1049
1104
  const labels = usePreviewLabels(props.labels);
@@ -1076,7 +1131,7 @@ function PdfThumbnail(props) {
1076
1131
  return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
1077
1132
  }
1078
1133
  function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
1079
- const canvasRef = React10.useRef(null);
1134
+ const canvasRef = React11.useRef(null);
1080
1135
  const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
1081
1136
  const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
1082
1137
  if (error) return /* @__PURE__ */ jsx7(Fragment3, { children: fallback });
@@ -1144,7 +1199,7 @@ var ITEM_STYLE = {
1144
1199
  };
1145
1200
  function MediaLibraryItem(props) {
1146
1201
  const { ref, record, isSelected, isActive, tabIndex, classNames, renderItem, onChoose, onFocus } = props;
1147
- const media = React11.useMemo(() => mediaFromRecord(record), [record]);
1202
+ const media = React12.useMemo(() => mediaFromRecord(record), [record]);
1148
1203
  const handleKeyDown = (event) => {
1149
1204
  if (event.key !== "Enter" && event.key !== " ") return;
1150
1205
  event.preventDefault();
@@ -1207,13 +1262,13 @@ function mergeMediaLibraryLabels(labels) {
1207
1262
  }
1208
1263
 
1209
1264
  // src/use-media-library.ts
1210
- import React12 from "react";
1265
+ import React13 from "react";
1211
1266
  import { usePaginatedQuery } from "convex/react";
1212
1267
  var SEARCH_DEBOUNCE_MS = 250;
1213
1268
  var DEFAULT_PAGE_SIZE = 24;
1214
1269
  function useDebouncedSearch(value, delayMs) {
1215
- const [debounced, setDebounced] = React12.useState(value);
1216
- React12.useEffect(() => {
1270
+ const [debounced, setDebounced] = React13.useState(value);
1271
+ React13.useEffect(() => {
1217
1272
  if (value === "") {
1218
1273
  setDebounced("");
1219
1274
  return;
@@ -1238,7 +1293,7 @@ function useMediaLibrary(options = {}) {
1238
1293
  const args = isSearching ? { search, ...typeArg } : typeArg;
1239
1294
  const result = usePaginatedQuery(reference, args, { initialNumItems: pageSize });
1240
1295
  const resultLoadMore = result.loadMore;
1241
- const loadMore = React12.useCallback(
1296
+ const loadMore = React13.useCallback(
1242
1297
  (numItems) => resultLoadMore(numItems ?? pageSize),
1243
1298
  [resultLoadMore, pageSize]
1244
1299
  );
@@ -1253,7 +1308,7 @@ function useMediaLibrary(options = {}) {
1253
1308
  }
1254
1309
 
1255
1310
  // src/use-roving-focus.ts
1256
- import React13 from "react";
1311
+ import React14 from "react";
1257
1312
  function nextIndex(key, current, count, columns) {
1258
1313
  const last = count - 1;
1259
1314
  const clamp = (index) => Math.min(Math.max(index, 0), last);
@@ -1281,16 +1336,16 @@ function columnCount(items) {
1281
1336
  return Math.max(inFirstRow.length, 1);
1282
1337
  }
1283
1338
  function useRovingFocus(count) {
1284
- const [activeIndex, setActiveIndex] = React13.useState(0);
1285
- const itemsRef = React13.useRef([]);
1339
+ const [activeIndex, setActiveIndex] = React14.useState(0);
1340
+ const itemsRef = React14.useRef([]);
1286
1341
  const currentIndex = Math.min(activeIndex, Math.max(count - 1, 0));
1287
- const itemRef = React13.useCallback(
1342
+ const itemRef = React14.useCallback(
1288
1343
  (index) => (element) => {
1289
1344
  itemsRef.current[index] = element;
1290
1345
  },
1291
1346
  []
1292
1347
  );
1293
- const handleKeyDown = React13.useCallback(
1348
+ const handleKeyDown = React14.useCallback(
1294
1349
  (event) => {
1295
1350
  if (count === 0) return;
1296
1351
  const items = itemsRef.current.slice(0, count);
@@ -1322,16 +1377,16 @@ var LIST_STYLE = {
1322
1377
  padding: 0
1323
1378
  };
1324
1379
  function MediaLibrary(props) {
1325
- const labels = React14.useMemo(() => mergeMediaLibraryLabels(props.labels), [props.labels]);
1380
+ const labels = React15.useMemo(() => mergeMediaLibraryLabels(props.labels), [props.labels]);
1326
1381
  const classNames = { ...props.classNames, root: props.className ?? props.classNames?.root };
1327
- const [search, setSearch] = React14.useState("");
1328
- const [chosenType, setChosenType] = React14.useState("");
1329
- const [selectedKey, setSelectedKey] = React14.useState(null);
1382
+ const [search, setSearch] = React15.useState("");
1383
+ const [chosenType, setChosenType] = React15.useState("");
1384
+ const [selectedKey, setSelectedKey] = React15.useState(null);
1330
1385
  const type = props.type ?? (chosenType === "" ? void 0 : chosenType);
1331
1386
  const { onSelect } = props;
1332
1387
  const isSelectable = props.selectable ?? props.selectedKeys !== void 0;
1333
1388
  const selectedKeys = props.selectedKeys ?? (selectedKey === null ? [] : [selectedKey]);
1334
- const handleChoose = React14.useCallback(
1389
+ const handleChoose = React15.useCallback(
1335
1390
  (record) => {
1336
1391
  if (isSelectable) setSelectedKey(record.key);
1337
1392
  onSelect(record, mediaFromRecord(record));
@@ -1443,7 +1498,7 @@ function LibraryResults(props) {
1443
1498
  )
1444
1499
  ] });
1445
1500
  }
1446
- var LibraryErrorBoundary = class extends React14.Component {
1501
+ var LibraryErrorBoundary = class extends React15.Component {
1447
1502
  state = { hasError: false, error: null };
1448
1503
  static getDerivedStateFromError(error) {
1449
1504
  return { hasError: true, error };
@@ -1485,7 +1540,7 @@ function useMediaObjectUrl(media, options) {
1485
1540
  }
1486
1541
 
1487
1542
  // src/media-picker.tsx
1488
- import React21 from "react";
1543
+ import React22 from "react";
1489
1544
 
1490
1545
  // src/media-picker-accept.ts
1491
1546
  function matchesAccept(candidate, accept) {
@@ -1525,10 +1580,10 @@ function recordMatchesAccept(record, accept) {
1525
1580
  }
1526
1581
 
1527
1582
  // src/media-picker-context.ts
1528
- import React15 from "react";
1529
- var MediaPickerContext = React15.createContext(null);
1583
+ import React16 from "react";
1584
+ var MediaPickerContext = /* @__PURE__ */ React16.createContext(null);
1530
1585
  function useMediaPicker() {
1531
- const picker = React15.use(MediaPickerContext);
1586
+ const picker = React16.use(MediaPickerContext);
1532
1587
  if (!picker) throw new Error("useMediaPicker must be used within <MediaPicker>");
1533
1588
  return picker;
1534
1589
  }
@@ -1569,13 +1624,13 @@ function mergeMediaPickerLabels(labels) {
1569
1624
  }
1570
1625
 
1571
1626
  // src/media-picker-library.tsx
1572
- import React16 from "react";
1627
+ import React17 from "react";
1573
1628
  import { jsx as jsx10 } from "react/jsx-runtime";
1574
1629
  function MediaPickerLibrary(props) {
1575
1630
  const picker = useMediaPicker();
1576
1631
  const { accept, toggle } = picker;
1577
1632
  const selectedKeys = picker.selected.map((media) => media.id);
1578
- const filter = React16.useMemo(
1633
+ const filter = React17.useMemo(
1579
1634
  () => accept ? (record) => recordMatchesAccept(record, accept) : void 0,
1580
1635
  [accept]
1581
1636
  );
@@ -1596,13 +1651,13 @@ function MediaPickerLibrary(props) {
1596
1651
  }
1597
1652
 
1598
1653
  // src/media-picker-selection.tsx
1599
- import React18 from "react";
1654
+ import React19 from "react";
1600
1655
 
1601
1656
  // src/preview.tsx
1602
1657
  import { mediaPath as mediaPath2 } from "@devultur/core";
1603
1658
 
1604
1659
  // src/video-preview.tsx
1605
- import React17 from "react";
1660
+ import React18 from "react";
1606
1661
  import { VideoPlayer } from "@devultur/react";
1607
1662
  import { Fragment as Fragment6, jsx as jsx11 } from "react/jsx-runtime";
1608
1663
  var UNAUTHORIZED = 401;
@@ -1616,13 +1671,13 @@ function fetchErrorOf(error) {
1616
1671
  if (HLS_NETWORK_FAILURE.test(error.details)) return networkError(error);
1617
1672
  return renderFailedError(error);
1618
1673
  }
1619
- function VideoPreview({ media, renderFallback, className }) {
1620
- const { tokenStatus, tokenError, reissueToken } = useDevultur();
1674
+ function VideoPreview({ media, renderFallback, loading, metrics, className }) {
1675
+ const { token, tokenStatus, tokenError, reissueToken } = useDevultur();
1621
1676
  const videoProps = useVideoProps(media);
1622
- const [failure, setFailure] = React17.useState(null);
1623
- const hasReissuedRef = React17.useRef(false);
1677
+ const [failure, setFailure] = React18.useState(null);
1678
+ const hasReissuedRef = React18.useRef(false);
1624
1679
  const playerToken = videoProps?.token;
1625
- const handleError = React17.useCallback(
1680
+ const handleError = React18.useCallback(
1626
1681
  (error) => {
1627
1682
  const isFirstMismatch = error.code === "session_mismatch" && !hasReissuedRef.current;
1628
1683
  if (isFirstMismatch) {
@@ -1635,18 +1690,18 @@ function VideoPreview({ media, renderFallback, className }) {
1635
1690
  [reissueToken, playerToken]
1636
1691
  );
1637
1692
  if (!videoProps) return null;
1638
- if (media.visibility === "private" && tokenStatus === "error") {
1639
- return /* @__PURE__ */ jsx11(Fragment6, { children: renderFallback(tokenUnavailableError(tokenError)) });
1640
- }
1693
+ const isPrivate = media.visibility === "private";
1694
+ if (isPrivate && tokenStatus === "error") return /* @__PURE__ */ jsx11(Fragment6, { children: renderFallback(tokenUnavailableError(tokenError)) });
1695
+ if (isPrivate && !token) return /* @__PURE__ */ jsx11(Fragment6, { children: loading });
1641
1696
  if (failure && failure.token === playerToken) return /* @__PURE__ */ jsx11(Fragment6, { children: renderFallback(failure.error) });
1642
- return /* @__PURE__ */ jsx11(VideoPlayer, { ...videoProps, className, onError: handleError });
1697
+ return /* @__PURE__ */ jsx11(VideoPlayer, { ...videoProps, className, metrics, onError: handleError });
1643
1698
  }
1644
1699
 
1645
1700
  // src/preview.tsx
1646
1701
  import { Fragment as Fragment7, jsx as jsx12 } from "react/jsx-runtime";
1647
1702
  var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
1648
1703
  var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
1649
- function Preview({ media, contentType, fallback, loading, labels, ...viewProps }) {
1704
+ function Preview({ media, contentType, fallback, loading, labels, metrics, ...viewProps }) {
1650
1705
  const text = usePreviewLabels(labels);
1651
1706
  if (!media) return null;
1652
1707
  const renderFallback = (error) => {
@@ -1655,12 +1710,15 @@ function Preview({ media, contentType, fallback, loading, labels, ...viewProps }
1655
1710
  if (fallback !== void 0) return fallback;
1656
1711
  return /* @__PURE__ */ jsx12(DefaultFallback, { media, error: fetchError, labels: text });
1657
1712
  };
1713
+ const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx12(LoadingStatus, { text: text.loading }) : loading;
1658
1714
  if (media.kind === "video") {
1659
1715
  return /* @__PURE__ */ jsx12(
1660
1716
  VideoPreview,
1661
1717
  {
1662
1718
  media,
1663
1719
  renderFallback,
1720
+ loading: loadingNode,
1721
+ metrics,
1664
1722
  className: viewProps.className
1665
1723
  },
1666
1724
  mediaPath2(media)
@@ -1669,7 +1727,6 @@ function Preview({ media, contentType, fallback, loading, labels, ...viewProps }
1669
1727
  const expectedType = expectedContentType(media, contentType);
1670
1728
  const kind = previewKindOf(expectedType);
1671
1729
  if (!expectedType || !kind) return /* @__PURE__ */ jsx12(Fragment7, { children: renderFallback(unsupportedType(expectedType)) });
1672
- const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx12(LoadingStatus, { text: text.loading }) : loading;
1673
1730
  const props = { ...viewProps, media, expectedType, renderFallback, loading: loadingNode, labels: text };
1674
1731
  if (kind === "text") return /* @__PURE__ */ jsx12(TextPreview, { ...props });
1675
1732
  if (kind === "pdf") return /* @__PURE__ */ jsx12(PdfPreview, { ...props, maxBytes: props.maxBytes ?? DEFAULT_MAX_BYTES });
@@ -1732,8 +1789,8 @@ var THUMBNAIL_WIDTH2 = 96;
1732
1789
  function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
1733
1790
  const picker = useMediaPicker();
1734
1791
  const { labels, selected } = picker;
1735
- const headingId = React18.useId();
1736
- const [previewedId, setPreviewedId] = React18.useState(null);
1792
+ const headingId = React19.useId();
1793
+ const [previewedId, setPreviewedId] = React19.useState(null);
1737
1794
  const Heading = `h${headingLevel}`;
1738
1795
  const cardHeadingLevel = headingLevel + 1;
1739
1796
  const regionClass = className ? `devultur-media-picker__selection ${className}` : "devultur-media-picker__selection";
@@ -1799,12 +1856,12 @@ function SelectionActions() {
1799
1856
  }
1800
1857
 
1801
1858
  // src/media-picker-upload.tsx
1802
- import React19 from "react";
1859
+ import React20 from "react";
1803
1860
  import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
1804
1861
  function MediaPickerUpload({ className }) {
1805
1862
  const picker = useMediaPicker();
1806
1863
  const { labels, multiple } = picker;
1807
- const [isDragging, setIsDragging] = React19.useState(false);
1864
+ const [isDragging, setIsDragging] = React20.useState(false);
1808
1865
  const handleDragOver = (event) => {
1809
1866
  event.preventDefault();
1810
1867
  setIsDragging(true);
@@ -1861,7 +1918,7 @@ function UploadRow({ upload }) {
1861
1918
  }
1862
1919
 
1863
1920
  // src/use-media-picker-uploads.ts
1864
- import React20 from "react";
1921
+ import React21 from "react";
1865
1922
  var isAbortError = (error) => typeof error === "object" && error !== null && error.name === "AbortError";
1866
1923
  var isPdf = (file) => file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
1867
1924
  var nextUploadId = 0;
@@ -1872,18 +1929,18 @@ async function pageCountOf(file, countPdfPages) {
1872
1929
  }
1873
1930
  function useMediaPickerUploads(options) {
1874
1931
  const { upload } = useDevultur();
1875
- const [uploads, setUploads] = React20.useState([]);
1876
- const controllers = React20.useRef(/* @__PURE__ */ new Map());
1877
- const optionsRef = React20.useRef(options);
1932
+ const [uploads, setUploads] = React21.useState([]);
1933
+ const controllers = React21.useRef(/* @__PURE__ */ new Map());
1934
+ const optionsRef = React21.useRef(options);
1878
1935
  optionsRef.current = options;
1879
- const patch = React20.useCallback((id, changes) => {
1936
+ const patch = React21.useCallback((id, changes) => {
1880
1937
  setUploads((current) => current.map((entry) => entry.id === id ? { ...entry, ...changes } : entry));
1881
1938
  }, []);
1882
- const drop = React20.useCallback((id) => {
1939
+ const drop = React21.useCallback((id) => {
1883
1940
  controllers.current.delete(id);
1884
1941
  setUploads((current) => current.filter((entry) => entry.id !== id));
1885
1942
  }, []);
1886
- const run = React20.useCallback(
1943
+ const run = React21.useCallback(
1887
1944
  async (id, file) => {
1888
1945
  const { labels, announce } = optionsRef.current;
1889
1946
  const controller = new AbortController();
@@ -1913,7 +1970,7 @@ function useMediaPickerUploads(options) {
1913
1970
  },
1914
1971
  [upload, patch, drop]
1915
1972
  );
1916
- const start = React20.useCallback(
1973
+ const start = React21.useCallback(
1917
1974
  (files) => {
1918
1975
  const { accept, multiple, labels } = optionsRef.current;
1919
1976
  const chosen = multiple ? [...files] : [...files].slice(0, 1);
@@ -1935,8 +1992,8 @@ function useMediaPickerUploads(options) {
1935
1992
  },
1936
1993
  [run]
1937
1994
  );
1938
- const cancel = React20.useCallback((id) => controllers.current.get(id)?.abort(), []);
1939
- const retry = React20.useCallback(
1995
+ const cancel = React21.useCallback((id) => controllers.current.get(id)?.abort(), []);
1996
+ const retry = React21.useCallback(
1940
1997
  (id) => {
1941
1998
  const entry = uploads.find((candidate) => candidate.id === id);
1942
1999
  if (!entry?.canRetry) return;
@@ -1944,7 +2001,7 @@ function useMediaPickerUploads(options) {
1944
2001
  },
1945
2002
  [uploads, run]
1946
2003
  );
1947
- React20.useEffect(() => {
2004
+ React21.useEffect(() => {
1948
2005
  const active = controllers.current;
1949
2006
  return () => {
1950
2007
  for (const controller of active.values()) controller.abort();
@@ -1969,24 +2026,24 @@ function emit(props, next) {
1969
2026
  props.onChange(next[0] ?? null);
1970
2027
  }
1971
2028
  var isSame = (a, b) => a.id === b.id;
1972
- function MediaPicker(props) {
2029
+ function MediaPickerRoot(props) {
1973
2030
  const isMultiple = props.multiple ?? false;
1974
- const labels = React21.useMemo(() => mergeMediaPickerLabels(props.labels), [props.labels]);
2031
+ const labels = React22.useMemo(() => mergeMediaPickerLabels(props.labels), [props.labels]);
1975
2032
  const accept = uploadAcceptOf(props.accept, props.type);
1976
2033
  const libraryType = props.type ?? mediaTypeOfAccept(props.accept);
1977
2034
  const countPdfPages = usePdfPageCounter();
1978
- const [announcement, setAnnouncement] = React21.useState("");
1979
- const inputRef = React21.useRef(null);
1980
- const selectionRef = React21.useRef(null);
1981
- const [ownSelection, setOwnSelection] = React21.useState(EMPTY);
2035
+ const [announcement, setAnnouncement] = React22.useState("");
2036
+ const inputRef = React22.useRef(null);
2037
+ const selectionRef = React22.useRef(null);
2038
+ const [ownSelection, setOwnSelection] = React22.useState(EMPTY);
1982
2039
  const controlledSelection = selectionOf(props);
1983
2040
  const selected = controlledSelection ?? ownSelection;
1984
- const selectedRef = React21.useRef(selected);
2041
+ const selectedRef = React22.useRef(selected);
1985
2042
  selectedRef.current = selected;
1986
- const propsRef = React21.useRef(props);
2043
+ const propsRef = React22.useRef(props);
1987
2044
  propsRef.current = props;
1988
2045
  const isControlled = controlledSelection !== void 0;
1989
- const commit = React21.useCallback(
2046
+ const commit = React22.useCallback(
1990
2047
  (next) => {
1991
2048
  selectedRef.current = next;
1992
2049
  if (!isControlled) setOwnSelection(next);
@@ -1994,15 +2051,15 @@ function MediaPicker(props) {
1994
2051
  },
1995
2052
  [isControlled]
1996
2053
  );
1997
- const announce = React21.useCallback((message) => setAnnouncement(message), []);
1998
- const select = React21.useCallback(
2054
+ const announce = React22.useCallback((message) => setAnnouncement(message), []);
2055
+ const select = React22.useCallback(
1999
2056
  (media) => {
2000
2057
  const current = selectedRef.current.filter((item) => !isSame(item, media));
2001
2058
  commit(isMultiple ? [...current, media] : [media]);
2002
2059
  },
2003
2060
  [commit, isMultiple]
2004
2061
  );
2005
- const remove = React21.useCallback(
2062
+ const remove = React22.useCallback(
2006
2063
  (media) => {
2007
2064
  commit(selectedRef.current.filter((item) => !isSame(item, media)));
2008
2065
  announce(labels.removed(mediaName(media)));
@@ -2010,7 +2067,7 @@ function MediaPicker(props) {
2010
2067
  },
2011
2068
  [commit, announce, labels]
2012
2069
  );
2013
- const toggle = React21.useCallback(
2070
+ const toggle = React22.useCallback(
2014
2071
  (media) => {
2015
2072
  const isSelected = selectedRef.current.some((item) => isSame(item, media));
2016
2073
  if (isMultiple && isSelected) {
@@ -2022,12 +2079,12 @@ function MediaPicker(props) {
2022
2079
  },
2023
2080
  [isMultiple, remove, select, announce, labels]
2024
2081
  );
2025
- const clear = React21.useCallback(() => {
2082
+ const clear = React22.useCallback(() => {
2026
2083
  commit([]);
2027
2084
  announce(labels.cleared);
2028
2085
  selectionRef.current?.focus();
2029
2086
  }, [commit, announce, labels]);
2030
- const handleUploaded = React21.useCallback(
2087
+ const handleUploaded = React22.useCallback(
2031
2088
  (media) => {
2032
2089
  select(media);
2033
2090
  selectionRef.current?.focus();
@@ -2089,29 +2146,66 @@ function MediaPicker(props) {
2089
2146
  /* @__PURE__ */ jsx15("div", { role: "status", "aria-live": "polite", style: VISUALLY_HIDDEN, children: announcement })
2090
2147
  ] }) });
2091
2148
  }
2092
- MediaPicker.Upload = MediaPickerUpload;
2093
- MediaPicker.Library = MediaPickerLibrary;
2094
- MediaPicker.Selection = MediaPickerSelection;
2149
+ var MediaPicker = /* @__PURE__ */ Object.assign(MediaPickerRoot, {
2150
+ displayName: "MediaPicker",
2151
+ Upload: MediaPickerUpload,
2152
+ Library: MediaPickerLibrary,
2153
+ Selection: MediaPickerSelection
2154
+ });
2095
2155
 
2096
- // src/react.tsx
2156
+ // src/viewer-provider.tsx
2157
+ import React23 from "react";
2158
+ import { useAction } from "convex/react";
2097
2159
  import { jsx as jsx16 } from "react/jsx-runtime";
2098
- var DevulturContext = React22.createContext(null);
2099
- var DevulturApiContext = React22.createContext(null);
2100
- var ProgressContext = React22.createContext(null);
2160
+ var VIEWER_ACTIONS = {
2161
+ uploadUrl: () => Promise.reject(new DevulturCapabilityError("uploadUrl")),
2162
+ upload: () => Promise.reject(new DevulturCapabilityError("upload")),
2163
+ deleteMedia: () => {
2164
+ throw new DevulturCapabilityError("deleteMedia");
2165
+ }
2166
+ };
2167
+ var NO_TOKEN_ACTION = () => Promise.reject(new Error("<DevulturViewerProvider> has no `issueViewerToken`: private media can't be shown"));
2168
+ function DevulturViewerProvider({ issueViewerToken, ...props }) {
2169
+ if (!issueViewerToken) return /* @__PURE__ */ jsx16(ViewerSession, { ...props, issueViewerToken: NO_TOKEN_ACTION, lazyToken: true });
2170
+ return /* @__PURE__ */ jsx16(ConvexViewerSession, { ...props, issueViewerToken });
2171
+ }
2172
+ function ConvexViewerSession({
2173
+ issueViewerToken,
2174
+ ...props
2175
+ }) {
2176
+ const issueViewerTokenAction = useAction(issueViewerToken);
2177
+ return /* @__PURE__ */ jsx16(ViewerSession, { issueViewerToken: issueViewerTokenAction, ...props });
2178
+ }
2179
+ function ViewerSession({
2180
+ baseUrl,
2181
+ issueViewerToken,
2182
+ tokenArgs,
2183
+ lazyToken = false,
2184
+ children
2185
+ }) {
2186
+ const session = useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy: lazyToken });
2187
+ const devulturValue = React23.useMemo(() => ({ ...session, ...VIEWER_ACTIONS }), [session]);
2188
+ return /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children });
2189
+ }
2190
+
2191
+ // src/react.tsx
2192
+ import { jsx as jsx17 } from "react/jsx-runtime";
2193
+ var ProgressContext = /* @__PURE__ */ React24.createContext(null);
2101
2194
  function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }) {
2102
- const session = useMediaSession({ issueViewerToken: issueViewerToken ?? api.issueViewerToken, tokenArgs, baseUrl });
2195
+ const issueViewerTokenAction = useAction2(issueViewerToken ?? api.issueViewerToken);
2196
+ const session = useMediaSession({ issueViewerToken: issueViewerTokenAction, tokenArgs, baseUrl });
2103
2197
  const createUploadUrlAction = useAction2(api.createUploadUrl);
2104
2198
  const deleteMediaAction = useAction2(api.deleteMedia);
2105
2199
  const confirmUploadAction = useAction2(api.confirmUpload ?? api.createUploadUrl);
2106
2200
  const deleteVideoAction = useAction2(api.deleteVideo ?? api.deleteMedia);
2107
- const uploadUrl = React22.useCallback(
2201
+ const uploadUrl = React24.useCallback(
2108
2202
  async (file) => {
2109
2203
  const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
2110
2204
  return { url: result.url, key: result.key };
2111
2205
  },
2112
2206
  [createUploadUrlAction]
2113
2207
  );
2114
- const upload = React22.useCallback(
2208
+ const upload = React24.useCallback(
2115
2209
  async (file, options) => {
2116
2210
  const dimensions = await readImageDimensions(file);
2117
2211
  options?.signal?.throwIfAborted();
@@ -2137,7 +2231,7 @@ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children
2137
2231
  },
2138
2232
  [createUploadUrlAction, confirmUploadAction, api.confirmUpload]
2139
2233
  );
2140
- const deleteMedia = React22.useCallback(
2234
+ const deleteMedia = React24.useCallback(
2141
2235
  (media) => {
2142
2236
  if (media.kind === "video" && api.deleteVideo) {
2143
2237
  deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
@@ -2147,13 +2241,13 @@ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children
2147
2241
  },
2148
2242
  [deleteMediaAction, deleteVideoAction, api.deleteVideo]
2149
2243
  );
2150
- const devulturValue = React22.useMemo(
2244
+ const devulturValue = React24.useMemo(
2151
2245
  () => ({ ...session, uploadUrl, upload, deleteMedia }),
2152
2246
  [session, uploadUrl, upload, deleteMedia]
2153
2247
  );
2154
- let content = /* @__PURE__ */ jsx16(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children }) });
2248
+ let content = /* @__PURE__ */ jsx17(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx17(DevulturContext, { value: devulturValue, children }) });
2155
2249
  if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
2156
- content = /* @__PURE__ */ jsx16(
2250
+ content = /* @__PURE__ */ jsx17(
2157
2251
  ProgressBridge,
2158
2252
  {
2159
2253
  save: api.saveProgress,
@@ -2177,13 +2271,13 @@ function ProgressBridge({
2177
2271
  const saveMutation = useMutation(save);
2178
2272
  const markCompleteMutation = useMutation(markComplete);
2179
2273
  const resetMutation = useMutation(reset);
2180
- const onSave = React22.useCallback(
2274
+ const onSave = React24.useCallback(
2181
2275
  async (videoId, position) => {
2182
2276
  await saveMutation({ videoId, position });
2183
2277
  },
2184
2278
  [saveMutation]
2185
2279
  );
2186
- const onLoad = React22.useCallback(
2280
+ const onLoad = React24.useCallback(
2187
2281
  async (videoId) => {
2188
2282
  const result = await convex.query(load, { videoId });
2189
2283
  if (!result) return null;
@@ -2194,60 +2288,34 @@ function ProgressBridge({
2194
2288
  },
2195
2289
  [convex, load]
2196
2290
  );
2197
- const onMarkComplete = React22.useCallback(
2291
+ const onMarkComplete = React24.useCallback(
2198
2292
  async (videoId) => {
2199
2293
  await markCompleteMutation({ videoId });
2200
2294
  },
2201
2295
  [markCompleteMutation]
2202
2296
  );
2203
- const onReset = React22.useCallback(
2297
+ const onReset = React24.useCallback(
2204
2298
  async (videoId) => {
2205
2299
  await resetMutation({ videoId });
2206
2300
  },
2207
2301
  [resetMutation]
2208
2302
  );
2209
- const value = React22.useMemo(
2303
+ const value = React24.useMemo(
2210
2304
  () => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
2211
2305
  [onSave, onLoad, onMarkComplete, onReset]
2212
2306
  );
2213
- return /* @__PURE__ */ jsx16(ProgressContext, { value, children });
2214
- }
2215
- function useDevultur() {
2216
- const context = React22.use(DevulturContext);
2217
- if (!context) throw new Error("useDevultur must be used within <DevulturProvider> or <DevulturViewerProvider>");
2218
- return context;
2219
- }
2220
- function useDevulturApi() {
2221
- const api = React22.use(DevulturApiContext);
2222
- if (api) return api;
2223
- if (React22.use(DevulturContext)) throw new DevulturCapabilityError("api");
2224
- throw new Error("useDevulturApi must be used within <DevulturProvider>");
2225
- }
2226
- var VIEWER_ACTIONS = {
2227
- uploadUrl: () => Promise.reject(new DevulturCapabilityError("uploadUrl")),
2228
- upload: () => Promise.reject(new DevulturCapabilityError("upload")),
2229
- deleteMedia: () => {
2230
- throw new DevulturCapabilityError("deleteMedia");
2231
- }
2232
- };
2233
- function DevulturViewerProvider({
2234
- baseUrl,
2235
- issueViewerToken,
2236
- tokenArgs,
2237
- children
2238
- }) {
2239
- const session = useMediaSession({ issueViewerToken, tokenArgs, baseUrl });
2240
- const devulturValue = React22.useMemo(() => ({ ...session, ...VIEWER_ACTIONS }), [session]);
2241
- return /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children });
2307
+ return /* @__PURE__ */ jsx17(ProgressContext, { value, children });
2242
2308
  }
2243
2309
  function useDevulturProgress() {
2244
- const context = React22.use(ProgressContext);
2310
+ const context = React24.use(ProgressContext);
2245
2311
  if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
2246
2312
  return context;
2247
2313
  }
2248
2314
  function useMediaUrl(key, options) {
2249
- const { token, baseUrl } = useDevultur();
2250
- return React22.useMemo(() => {
2315
+ const devultur = useDevultur();
2316
+ const { token, baseUrl } = devultur;
2317
+ useRequestTokenWhen(devultur, Boolean(key) && visibilityFromKey(key ?? "") !== "public");
2318
+ return React24.useMemo(() => {
2251
2319
  if (!key) return null;
2252
2320
  if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
2253
2321
  const isPublic = visibilityFromKey(key) === "public";
@@ -2273,9 +2341,11 @@ async function readImageDimensions(file) {
2273
2341
  }
2274
2342
  }
2275
2343
  function useVideoProps(media, options) {
2276
- const { token, baseUrl, sessionSecret } = useDevultur();
2344
+ const devultur = useDevultur();
2345
+ const { token, baseUrl, sessionSecret } = devultur;
2346
+ useRequestTokenWhen(devultur, media?.kind === "video" && media.visibility === "private");
2277
2347
  const locales = options?.locales;
2278
- return React22.useMemo(() => {
2348
+ return React24.useMemo(() => {
2279
2349
  if (media?.kind !== "video") return null;
2280
2350
  if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
2281
2351
  const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
@@ -2294,8 +2364,10 @@ function useVideoProps(media, options) {
2294
2364
  }, [media, baseUrl, token, locales, sessionSecret]);
2295
2365
  }
2296
2366
  function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }) {
2297
- const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = useDevultur();
2298
- const handleClick = React22.useCallback(
2367
+ const devultur = useDevultur();
2368
+ const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = devultur;
2369
+ useRequestTokenWhen(devultur, media?.visibility === "private");
2370
+ const handleClick = React24.useCallback(
2299
2371
  async (event) => {
2300
2372
  if (media?.visibility !== "private" || !baseUrl) return;
2301
2373
  event.preventDefault();
@@ -2324,10 +2396,10 @@ function DownloadLink({ media, filename, onDownloadError, children, ...anchorPro
2324
2396
  [media, baseUrl, token, tokenStatus, tokenError, filename, sessionSecret, reissueToken, onDownloadError]
2325
2397
  );
2326
2398
  if (!media) return null;
2327
- return /* @__PURE__ */ jsx16("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
2399
+ return /* @__PURE__ */ jsx17("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
2328
2400
  }
2329
2401
  function useDevulturMedia(key) {
2330
- const apiCtx = React22.use(DevulturApiContext);
2402
+ const apiCtx = React24.use(DevulturApiContext);
2331
2403
  if (!apiCtx?.getMedia) {
2332
2404
  throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
2333
2405
  }
@@ -2381,10 +2453,12 @@ function useDevulturMedia(key) {
2381
2453
  };
2382
2454
  }
2383
2455
  function useVideoState(media, options) {
2384
- const { token, baseUrl, sessionSecret } = useDevultur();
2456
+ const devultur = useDevultur();
2457
+ const { token, baseUrl, sessionSecret } = devultur;
2458
+ useRequestTokenWhen(devultur, media?.kind === "video" && media.visibility === "private");
2385
2459
  const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
2386
2460
  const locales = options?.locales;
2387
- return React22.useMemo(() => {
2461
+ return React24.useMemo(() => {
2388
2462
  if (media?.kind !== "video") return { phase: "idle" };
2389
2463
  if (status.isFailed) {
2390
2464
  return {
@@ -2433,17 +2507,17 @@ function useMediaUpload(options) {
2433
2507
  const deleteMediaAction = useAction2(api.deleteMedia);
2434
2508
  const confirmUploadAction = useAction2(api.confirmUpload ?? api.createUploadUrl);
2435
2509
  const processVideoAction = useAction2(api.processVideo ?? api.createUploadUrl);
2436
- const [phase, setPhase] = React22.useState("idle");
2437
- const [percent, setPercent] = React22.useState(0);
2438
- const [currentFile, setCurrentFile] = React22.useState(null);
2439
- const [media, setMedia] = React22.useState(null);
2440
- const [uploadFailure, setUploadFailure] = React22.useState(null);
2441
- const [processRequested, setProcessRequested] = React22.useState(false);
2442
- const abortRef = React22.useRef(null);
2443
- const lastFileRef = React22.useRef(null);
2444
- const lastKeyRef = React22.useRef(null);
2510
+ const [phase, setPhase] = React24.useState("idle");
2511
+ const [percent, setPercent] = React24.useState(0);
2512
+ const [currentFile, setCurrentFile] = React24.useState(null);
2513
+ const [media, setMedia] = React24.useState(null);
2514
+ const [uploadFailure, setUploadFailure] = React24.useState(null);
2515
+ const [processRequested, setProcessRequested] = React24.useState(false);
2516
+ const abortRef = React24.useRef(null);
2517
+ const lastFileRef = React24.useRef(null);
2518
+ const lastKeyRef = React24.useRef(null);
2445
2519
  const videoState = useVideoState(media, { locales });
2446
- const triggerProcess = React22.useCallback(
2520
+ const triggerProcess = React24.useCallback(
2447
2521
  (key) => {
2448
2522
  if (!api.processVideo) {
2449
2523
  throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
@@ -2456,7 +2530,7 @@ function useMediaUpload(options) {
2456
2530
  },
2457
2531
  [api.processVideo, processVideoAction]
2458
2532
  );
2459
- const runUpload = React22.useCallback(
2533
+ const runUpload = React24.useCallback(
2460
2534
  async (file) => {
2461
2535
  const controller = new AbortController();
2462
2536
  abortRef.current = controller;
@@ -2527,10 +2601,10 @@ function useMediaUpload(options) {
2527
2601
  onError
2528
2602
  ]
2529
2603
  );
2530
- const cancel = React22.useCallback(() => {
2604
+ const cancel = React24.useCallback(() => {
2531
2605
  abortRef.current?.abort();
2532
2606
  }, []);
2533
- const reset = React22.useCallback(() => {
2607
+ const reset = React24.useCallback(() => {
2534
2608
  abortRef.current?.abort();
2535
2609
  setPhase("idle");
2536
2610
  setPercent(0);
@@ -2539,15 +2613,15 @@ function useMediaUpload(options) {
2539
2613
  setUploadFailure(null);
2540
2614
  setProcessRequested(false);
2541
2615
  }, []);
2542
- const retry = React22.useCallback(() => {
2616
+ const retry = React24.useCallback(() => {
2543
2617
  if (phase === "upload-failed" && lastFileRef.current) {
2544
2618
  runUpload(lastFileRef.current);
2545
2619
  return;
2546
2620
  }
2547
2621
  if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
2548
2622
  }, [phase, runUpload, triggerProcess]);
2549
- React22.useEffect(() => () => abortRef.current?.abort(), []);
2550
- const state = React22.useMemo(
2623
+ React24.useEffect(() => () => abortRef.current?.abort(), []);
2624
+ const state = React24.useMemo(
2551
2625
  () => deriveUploadState(
2552
2626
  { phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
2553
2627
  videoState,
@@ -2555,7 +2629,7 @@ function useMediaUpload(options) {
2555
2629
  ),
2556
2630
  [phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
2557
2631
  );
2558
- const upload = React22.useCallback((file) => runUpload(file), [runUpload]);
2632
+ const upload = React24.useCallback((file) => runUpload(file), [runUpload]);
2559
2633
  return { upload, state, cancel, reset };
2560
2634
  }
2561
2635
  export {
@@ -2583,6 +2657,7 @@ export {
2583
2657
  useDevultur,
2584
2658
  useDevulturApi,
2585
2659
  useDevulturMedia,
2660
+ useDevulturOptional,
2586
2661
  useDevulturProgress,
2587
2662
  useMediaLibrary,
2588
2663
  useMediaObjectUrl,