@ttt-productions/upload-ui 0.7.15 → 0.7.16
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.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/react/tray/index.d.ts +1 -1
- package/dist/react/tray/index.d.ts.map +1 -1
- package/dist/react/tray/index.js +1 -1
- package/dist/react/tray/index.js.map +1 -1
- package/dist/react/use-mark-upload-activity-seen.d.ts +10 -2
- package/dist/react/use-mark-upload-activity-seen.d.ts.map +1 -1
- package/dist/react/use-mark-upload-activity-seen.js +42 -6
- package/dist/react/use-mark-upload-activity-seen.js.map +1 -1
- package/dist/upload-display.d.ts +39 -0
- package/dist/upload-display.d.ts.map +1 -0
- package/dist/upload-display.js +62 -0
- package/dist/upload-display.js.map +1 -0
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"","sourcesContent":["// Server-safe entry. React code lives at `@ttt-productions/upload-ui/react/guard
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,sCAAsC;AACtC,OAAO,EAAE,cAAc,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["// Server-safe entry: no React. React code lives at `@ttt-productions/upload-ui/react/guard`,\n// `/react/upload`, and `/react/tray`.\nexport { formatFileSize, createUploadDisplayFormatters } from \"./upload-display.js\";\nexport type { UploadDisplayCopy, UploadDisplayConfig, UploadDisplayFormatters } from \"./upload-display.js\";\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, useUploadsSourceState, useUploadsSourceRetry, isTerminalUpload, type UploadsSourceState, type UploadsSourceRetry, type InFlightUpload, type InFlightUploadActive, type InFlightUploadCompleted, type InFlightUploadFailed, type InFlightUploadRejected, type InFlightUploadBase, type InFlightUploadStatus, type InFlightUploadsAdapter, type InFlightUploadsMonitoringEvent, type InFlightUploadsProviderProps, type ParsedPendingMedia, type FirestoreSubscribeFn, type FirestoreLikeSnapshot, type FirestoreLikeDocChange, } from '../in-flight-uploads-provider.js';
|
|
2
2
|
export { useClearUploadActivity, type ClearUploadActivityOptions, } from '../use-clear-upload-activity.js';
|
|
3
|
-
export { useMarkUploadActivitySeen, type MarkUploadActivitySeenOptions, } from '../use-mark-upload-activity-seen.js';
|
|
3
|
+
export { useMarkUploadActivitySeen, MarkUploadActivitySeenError, type MarkUploadActivitySeenOptions, } from '../use-mark-upload-activity-seen.js';
|
|
4
4
|
export { useUploadProcessing, type UploadProcessingState, type UseUploadProcessingOptions, } from '../use-upload-processing.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,EACjC,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,EACtB,KAAK,0BAA0B,GAChC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,EACzB,KAAK,6BAA6B,GACnC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,EACjC,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,EACtB,KAAK,0BAA0B,GAChC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,KAAK,6BAA6B,GACnC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,6BAA6B,CAAC"}
|
package/dist/react/tray/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, useUploadsSourceState, useUploadsSourceRetry, isTerminalUpload, } from '../in-flight-uploads-provider.js';
|
|
2
2
|
export { useClearUploadActivity, } from '../use-clear-upload-activity.js';
|
|
3
|
-
export { useMarkUploadActivitySeen, } from '../use-mark-upload-activity-seen.js';
|
|
3
|
+
export { useMarkUploadActivitySeen, MarkUploadActivitySeenError, } from '../use-mark-upload-activity-seen.js';
|
|
4
4
|
export { useUploadProcessing, } from '../use-upload-processing.js';
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,GAiBjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,GAEvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,GAiBjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,GAEvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,EACzB,2BAA2B,GAE5B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,mBAAmB,GAGpB,MAAM,6BAA6B,CAAC","sourcesContent":["export {\n InFlightUploadsProvider,\n useInFlightUploadsState,\n useInFlightUpload,\n useUploadActivityState,\n useUploadsSourceState,\n useUploadsSourceRetry,\n isTerminalUpload,\n type UploadsSourceState,\n type UploadsSourceRetry,\n type InFlightUpload,\n type InFlightUploadActive,\n type InFlightUploadCompleted,\n type InFlightUploadFailed,\n type InFlightUploadRejected,\n type InFlightUploadBase,\n type InFlightUploadStatus,\n type InFlightUploadsAdapter,\n type InFlightUploadsMonitoringEvent,\n type InFlightUploadsProviderProps,\n type ParsedPendingMedia,\n type FirestoreSubscribeFn,\n type FirestoreLikeSnapshot,\n type FirestoreLikeDocChange,\n} from '../in-flight-uploads-provider.js';\nexport {\n useClearUploadActivity,\n type ClearUploadActivityOptions,\n} from '../use-clear-upload-activity.js';\nexport {\n useMarkUploadActivitySeen,\n MarkUploadActivitySeenError,\n type MarkUploadActivitySeenOptions,\n} from '../use-mark-upload-activity-seen.js';\nexport {\n useUploadProcessing,\n type UploadProcessingState,\n type UseUploadProcessingOptions,\n} from '../use-upload-processing.js';\n"]}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
export interface MarkUploadActivitySeenOptions {
|
|
2
|
-
/** Network call performing the mark-seen
|
|
2
|
+
/** Network call performing the mark-seen for at most `maxIdsPerCall` pendingMediaIds. */
|
|
3
3
|
markFn: (pendingMediaIds: string[]) => Promise<void>;
|
|
4
|
-
/**
|
|
4
|
+
/** The most ids one `markFn` call accepts — the app's own per-call cap. */
|
|
5
|
+
maxIdsPerCall: number;
|
|
6
|
+
/** Optional error callback. Receives the first failure and every id that was not marked. */
|
|
5
7
|
onError?: (error: Error, pendingMediaIds: string[]) => void;
|
|
6
8
|
}
|
|
9
|
+
/** The mutation's failure when one or more calls failed: the first failure, and the ids left unmarked. */
|
|
10
|
+
export declare class MarkUploadActivitySeenError extends Error {
|
|
11
|
+
readonly failure: Error;
|
|
12
|
+
readonly unmarkedIds: string[];
|
|
13
|
+
constructor(failure: Error, unmarkedIds: string[]);
|
|
14
|
+
}
|
|
7
15
|
export declare function useMarkUploadActivitySeen(options: MarkUploadActivitySeenOptions): import("@tanstack/react-query").UseMutationResult<void, Error, string[], unknown>;
|
|
8
16
|
//# sourceMappingURL=use-mark-upload-activity-seen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-mark-upload-activity-seen.d.ts","sourceRoot":"","sources":["../../src/react/use-mark-upload-activity-seen.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-mark-upload-activity-seen.d.ts","sourceRoot":"","sources":["../../src/react/use-mark-upload-activity-seen.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,6BAA6B;IAC5C,yFAAyF;IACzF,MAAM,EAAE,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAC;IACtB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAC7D;AAED,0GAA0G;AAC1G,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;gBAEnB,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE;CAMlD;AAwBD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,6BAA6B,qFAS/E"}
|
|
@@ -12,17 +12,53 @@
|
|
|
12
12
|
* needed in this hook. Marking seen is tray UX state, not a sensitive action,
|
|
13
13
|
* so it mirrors `useClearUploadActivity` exactly (no audit event).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* The mutation takes every id to mark (typically every currently-unseen terminal
|
|
16
|
+
* item when the File-status tab opens) and sends them to `markFn` in sequential
|
|
17
|
+
* calls of at most `maxIdsPerCall` — the app's own per-call cap, so no call is
|
|
18
|
+
* refused for its size. A failed call does not stop the rest: its ids simply stay
|
|
19
|
+
* unseen. When any call failed, the mutation fails, and `onError` receives the first
|
|
20
|
+
* failure and every id left unmarked. An empty array makes no call.
|
|
18
21
|
*/
|
|
19
22
|
import { useMutation } from '@tanstack/react-query';
|
|
23
|
+
/** The mutation's failure when one or more calls failed: the first failure, and the ids left unmarked. */
|
|
24
|
+
export class MarkUploadActivitySeenError extends Error {
|
|
25
|
+
failure;
|
|
26
|
+
unmarkedIds;
|
|
27
|
+
constructor(failure, unmarkedIds) {
|
|
28
|
+
super(failure.message);
|
|
29
|
+
this.name = 'MarkUploadActivitySeenError';
|
|
30
|
+
this.failure = failure;
|
|
31
|
+
this.unmarkedIds = unmarkedIds;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function markInBatches(markFn, maxIdsPerCall, pendingMediaIds) {
|
|
35
|
+
if (!Number.isInteger(maxIdsPerCall) || maxIdsPerCall < 1) {
|
|
36
|
+
throw new Error(`maxIdsPerCall must be a positive whole number, got ${maxIdsPerCall}.`);
|
|
37
|
+
}
|
|
38
|
+
let firstFailure = null;
|
|
39
|
+
const unmarkedIds = [];
|
|
40
|
+
for (let start = 0; start < pendingMediaIds.length; start += maxIdsPerCall) {
|
|
41
|
+
const batch = pendingMediaIds.slice(start, start + maxIdsPerCall);
|
|
42
|
+
try {
|
|
43
|
+
await markFn(batch);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
firstFailure ??= err instanceof Error ? err : new Error(String(err));
|
|
47
|
+
unmarkedIds.push(...batch);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (firstFailure)
|
|
51
|
+
throw new MarkUploadActivitySeenError(firstFailure, unmarkedIds);
|
|
52
|
+
}
|
|
20
53
|
export function useMarkUploadActivitySeen(options) {
|
|
21
|
-
const { markFn, onError } = options;
|
|
54
|
+
const { markFn, maxIdsPerCall, onError } = options;
|
|
22
55
|
return useMutation({
|
|
23
|
-
mutationFn: markFn,
|
|
56
|
+
mutationFn: (pendingMediaIds) => markInBatches(markFn, maxIdsPerCall, pendingMediaIds),
|
|
24
57
|
onError: (error, pendingMediaIds) => {
|
|
25
|
-
|
|
58
|
+
if (error instanceof MarkUploadActivitySeenError)
|
|
59
|
+
onError?.(error.failure, error.unmarkedIds);
|
|
60
|
+
else
|
|
61
|
+
onError?.(error, pendingMediaIds);
|
|
26
62
|
},
|
|
27
63
|
});
|
|
28
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-mark-upload-activity-seen.js","sourceRoot":"","sources":["../../src/react/use-mark-upload-activity-seen.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb
|
|
1
|
+
{"version":3,"file":"use-mark-upload-activity-seen.js","sourceRoot":"","sources":["../../src/react/use-mark-upload-activity-seen.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAWpD,0GAA0G;AAC1G,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IAC3C,OAAO,CAAQ;IACf,WAAW,CAAW;IAE/B,YAAY,OAAc,EAAE,WAAqB;QAC/C,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;QAC1C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;CACF;AAED,KAAK,UAAU,aAAa,CAC1B,MAAoD,EACpD,aAAqB,EACrB,eAAyB;IAEzB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,sDAAsD,aAAa,GAAG,CAAC,CAAC;IAC1F,CAAC;IACD,IAAI,YAAY,GAAiB,IAAI,CAAC;IACtC,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,eAAe,CAAC,MAAM,EAAE,KAAK,IAAI,aAAa,EAAE,CAAC;QAC3E,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,aAAa,CAAC,CAAC;QAClE,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACrE,WAAW,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,IAAI,YAAY;QAAE,MAAM,IAAI,2BAA2B,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,OAAsC;IAC9E,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IACnD,OAAO,WAAW,CAAwB;QACxC,UAAU,EAAE,CAAC,eAAe,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,eAAe,CAAC;QACtF,OAAO,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE;YAClC,IAAI,KAAK,YAAY,2BAA2B;gBAAE,OAAO,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;;gBACzF,OAAO,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QACzC,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\n/**\n * Generic mutation hook to mark terminal pendingMedia items as seen in the tray.\n *\n * Pure adapter: the consumer supplies `markFn` (typically a wrapped Firebase\n * callable in the consuming app that writes `uploadTraySeenAt`/`uploadTraySeenBy` on the\n * caller's own terminal pendingMedia docs). The package owns the useMutation\n * wiring and the optional `onError` hook; the consumer owns the network call.\n *\n * The provider's Firestore listener observes the `uploadTraySeenAt` write and\n * updates the in-flight upload records automatically — no optimistic update is\n * needed in this hook. Marking seen is tray UX state, not a sensitive action,\n * so it mirrors `useClearUploadActivity` exactly (no audit event).\n *\n * The mutation takes every id to mark (typically every currently-unseen terminal\n * item when the File-status tab opens) and sends them to `markFn` in sequential\n * calls of at most `maxIdsPerCall` — the app's own per-call cap, so no call is\n * refused for its size. A failed call does not stop the rest: its ids simply stay\n * unseen. When any call failed, the mutation fails, and `onError` receives the first\n * failure and every id left unmarked. An empty array makes no call.\n */\n\nimport { useMutation } from '@tanstack/react-query';\n\nexport interface MarkUploadActivitySeenOptions {\n /** Network call performing the mark-seen for at most `maxIdsPerCall` pendingMediaIds. */\n markFn: (pendingMediaIds: string[]) => Promise<void>;\n /** The most ids one `markFn` call accepts — the app's own per-call cap. */\n maxIdsPerCall: number;\n /** Optional error callback. Receives the first failure and every id that was not marked. */\n onError?: (error: Error, pendingMediaIds: string[]) => void;\n}\n\n/** The mutation's failure when one or more calls failed: the first failure, and the ids left unmarked. */\nexport class MarkUploadActivitySeenError extends Error {\n readonly failure: Error;\n readonly unmarkedIds: string[];\n\n constructor(failure: Error, unmarkedIds: string[]) {\n super(failure.message);\n this.name = 'MarkUploadActivitySeenError';\n this.failure = failure;\n this.unmarkedIds = unmarkedIds;\n }\n}\n\nasync function markInBatches(\n markFn: (pendingMediaIds: string[]) => Promise<void>,\n maxIdsPerCall: number,\n pendingMediaIds: string[],\n): Promise<void> {\n if (!Number.isInteger(maxIdsPerCall) || maxIdsPerCall < 1) {\n throw new Error(`maxIdsPerCall must be a positive whole number, got ${maxIdsPerCall}.`);\n }\n let firstFailure: Error | null = null;\n const unmarkedIds: string[] = [];\n for (let start = 0; start < pendingMediaIds.length; start += maxIdsPerCall) {\n const batch = pendingMediaIds.slice(start, start + maxIdsPerCall);\n try {\n await markFn(batch);\n } catch (err) {\n firstFailure ??= err instanceof Error ? err : new Error(String(err));\n unmarkedIds.push(...batch);\n }\n }\n if (firstFailure) throw new MarkUploadActivitySeenError(firstFailure, unmarkedIds);\n}\n\nexport function useMarkUploadActivitySeen(options: MarkUploadActivitySeenOptions) {\n const { markFn, maxIdsPerCall, onError } = options;\n return useMutation<void, Error, string[]>({\n mutationFn: (pendingMediaIds) => markInBatches(markFn, maxIdsPerCall, pendingMediaIds),\n onError: (error, pendingMediaIds) => {\n if (error instanceof MarkUploadActivitySeenError) onError?.(error.failure, error.unmarkedIds);\n else onError?.(error, pendingMediaIds);\n },\n });\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** A byte count as a short size: `850 B`, `24 KB`, `1.2 MB`, `3 GB`. `null` for no usable count. */
|
|
2
|
+
export declare function formatFileSize(bytes: number | undefined): string | null;
|
|
3
|
+
/** The app's words for an upload row's time and file kind. */
|
|
4
|
+
export interface UploadDisplayCopy {
|
|
5
|
+
/** Under a minute old. */
|
|
6
|
+
justNow: string;
|
|
7
|
+
minutesAgo: (minutes: number) => string;
|
|
8
|
+
hoursAgo: (hours: number) => string;
|
|
9
|
+
/** Accepted on the previous calendar day, given the local clock time. */
|
|
10
|
+
yesterdayAt: (time: string) => string;
|
|
11
|
+
/** Accepted before yesterday, given the short local date and clock time. */
|
|
12
|
+
dateAt: (date: string, time: string) => string;
|
|
13
|
+
/** The file's kind, by its simplified media type; `other` for anything else. */
|
|
14
|
+
fileType: {
|
|
15
|
+
image: string;
|
|
16
|
+
video: string;
|
|
17
|
+
audio: string;
|
|
18
|
+
other: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface UploadDisplayConfig {
|
|
22
|
+
copy: UploadDisplayCopy;
|
|
23
|
+
/** How long a row's time reads as relative before it reads as a clock time. */
|
|
24
|
+
relativeTimeCutoffMs: number;
|
|
25
|
+
/** Locale for the clock time and date; the runtime's own when omitted. */
|
|
26
|
+
locale?: Intl.LocalesArgument;
|
|
27
|
+
}
|
|
28
|
+
export interface UploadDisplayFormatters {
|
|
29
|
+
/**
|
|
30
|
+
* An upload's accept time. Within the cutoff of now: just now, minutes ago, or hours ago. Older:
|
|
31
|
+
* the clock time for today, yesterday and the clock time, or the short date and the clock time —
|
|
32
|
+
* by local calendar day.
|
|
33
|
+
*/
|
|
34
|
+
formatUploadTimestamp: (createdAtMs: number, nowMs: number) => string;
|
|
35
|
+
/** The file's kind from its stored content type; `null` when it has none. */
|
|
36
|
+
getFileTypeLabel: (contentType: string | undefined) => string | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function createUploadDisplayFormatters(config: UploadDisplayConfig): UploadDisplayFormatters;
|
|
39
|
+
//# sourceMappingURL=upload-display.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-display.d.ts","sourceRoot":"","sources":["../src/upload-display.ts"],"names":[],"mappings":"AAKA,oGAAoG;AACpG,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUvE;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,yEAAyE;IACzE,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,4EAA4E;IAC5E,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,gFAAgF;IAChF,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1E;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,qBAAqB,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACtE,6EAA6E;IAC7E,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC;CACtE;AAMD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,GAAG,uBAAuB,CAwClG"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// What an upload row shows besides its title: the file's size, its kind, and when it was accepted.
|
|
2
|
+
// The mechanics are shared; every word, the relative-time cutoff and the locale are the app's.
|
|
3
|
+
import { getSimplifiedMediaType } from "@ttt-productions/media-schemas";
|
|
4
|
+
/** A byte count as a short size: `850 B`, `24 KB`, `1.2 MB`, `3 GB`. `null` for no usable count. */
|
|
5
|
+
export function formatFileSize(bytes) {
|
|
6
|
+
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
7
|
+
return null;
|
|
8
|
+
if (bytes < 1024)
|
|
9
|
+
return `${bytes} B`;
|
|
10
|
+
if (bytes < 1024 * 1024)
|
|
11
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
12
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
13
|
+
const mb = bytes / (1024 * 1024);
|
|
14
|
+
return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
|
|
15
|
+
}
|
|
16
|
+
const gb = bytes / (1024 * 1024 * 1024);
|
|
17
|
+
return `${gb < 10 ? gb.toFixed(1) : Math.round(gb)} GB`;
|
|
18
|
+
}
|
|
19
|
+
function sameCalendarDay(a, b) {
|
|
20
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
21
|
+
}
|
|
22
|
+
export function createUploadDisplayFormatters(config) {
|
|
23
|
+
const { copy, relativeTimeCutoffMs, locale } = config;
|
|
24
|
+
const formatUploadTimestamp = (createdAtMs, nowMs) => {
|
|
25
|
+
const diffMs = Math.max(0, nowMs - createdAtMs);
|
|
26
|
+
if (diffMs < relativeTimeCutoffMs) {
|
|
27
|
+
if (diffMs < 60_000)
|
|
28
|
+
return copy.justNow;
|
|
29
|
+
const minutes = Math.floor(diffMs / 60_000);
|
|
30
|
+
if (minutes < 60)
|
|
31
|
+
return copy.minutesAgo(minutes);
|
|
32
|
+
return copy.hoursAgo(Math.floor(diffMs / 3_600_000));
|
|
33
|
+
}
|
|
34
|
+
const created = new Date(createdAtMs);
|
|
35
|
+
const now = new Date(nowMs);
|
|
36
|
+
const time = created.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit" });
|
|
37
|
+
if (sameCalendarDay(created, now))
|
|
38
|
+
return time;
|
|
39
|
+
// The previous calendar day, found by stepping the date rather than subtracting 24 hours: a
|
|
40
|
+
// day that crosses a daylight-saving change is 23 or 25 hours long.
|
|
41
|
+
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
42
|
+
if (sameCalendarDay(created, yesterday))
|
|
43
|
+
return copy.yesterdayAt(time);
|
|
44
|
+
return copy.dateAt(created.toLocaleDateString(locale, { month: "short", day: "numeric" }), time);
|
|
45
|
+
};
|
|
46
|
+
const getFileTypeLabel = (contentType) => {
|
|
47
|
+
if (!contentType)
|
|
48
|
+
return null;
|
|
49
|
+
switch (getSimplifiedMediaType(contentType)) {
|
|
50
|
+
case "image":
|
|
51
|
+
return copy.fileType.image;
|
|
52
|
+
case "video":
|
|
53
|
+
return copy.fileType.video;
|
|
54
|
+
case "audio":
|
|
55
|
+
return copy.fileType.audio;
|
|
56
|
+
default:
|
|
57
|
+
return copy.fileType.other;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return { formatUploadTimestamp, getFileTypeLabel };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=upload-display.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-display.js","sourceRoot":"","sources":["../src/upload-display.ts"],"names":[],"mappings":"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","sourcesContent":["// What an upload row shows besides its title: the file's size, its kind, and when it was accepted.\n// The mechanics are shared; every word, the relative-time cutoff and the locale are the app's.\n\nimport { getSimplifiedMediaType } from \"@ttt-productions/media-schemas\";\n\n/** A byte count as a short size: `850 B`, `24 KB`, `1.2 MB`, `3 GB`. `null` for no usable count. */\nexport function formatFileSize(bytes: number | undefined): string | null {\n if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) return null;\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;\n if (bytes < 1024 * 1024 * 1024) {\n const mb = bytes / (1024 * 1024);\n return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;\n }\n const gb = bytes / (1024 * 1024 * 1024);\n return `${gb < 10 ? gb.toFixed(1) : Math.round(gb)} GB`;\n}\n\n/** The app's words for an upload row's time and file kind. */\nexport interface UploadDisplayCopy {\n /** Under a minute old. */\n justNow: string;\n minutesAgo: (minutes: number) => string;\n hoursAgo: (hours: number) => string;\n /** Accepted on the previous calendar day, given the local clock time. */\n yesterdayAt: (time: string) => string;\n /** Accepted before yesterday, given the short local date and clock time. */\n dateAt: (date: string, time: string) => string;\n /** The file's kind, by its simplified media type; `other` for anything else. */\n fileType: { image: string; video: string; audio: string; other: string };\n}\n\nexport interface UploadDisplayConfig {\n copy: UploadDisplayCopy;\n /** How long a row's time reads as relative before it reads as a clock time. */\n relativeTimeCutoffMs: number;\n /** Locale for the clock time and date; the runtime's own when omitted. */\n locale?: Intl.LocalesArgument;\n}\n\nexport interface UploadDisplayFormatters {\n /**\n * An upload's accept time. Within the cutoff of now: just now, minutes ago, or hours ago. Older:\n * the clock time for today, yesterday and the clock time, or the short date and the clock time —\n * by local calendar day.\n */\n formatUploadTimestamp: (createdAtMs: number, nowMs: number) => string;\n /** The file's kind from its stored content type; `null` when it has none. */\n getFileTypeLabel: (contentType: string | undefined) => string | null;\n}\n\nfunction sameCalendarDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\nexport function createUploadDisplayFormatters(config: UploadDisplayConfig): UploadDisplayFormatters {\n const { copy, relativeTimeCutoffMs, locale } = config;\n\n const formatUploadTimestamp = (createdAtMs: number, nowMs: number): string => {\n const diffMs = Math.max(0, nowMs - createdAtMs);\n if (diffMs < relativeTimeCutoffMs) {\n if (diffMs < 60_000) return copy.justNow;\n const minutes = Math.floor(diffMs / 60_000);\n if (minutes < 60) return copy.minutesAgo(minutes);\n return copy.hoursAgo(Math.floor(diffMs / 3_600_000));\n }\n\n const created = new Date(createdAtMs);\n const now = new Date(nowMs);\n const time = created.toLocaleTimeString(locale, { hour: \"numeric\", minute: \"2-digit\" });\n if (sameCalendarDay(created, now)) return time;\n\n // The previous calendar day, found by stepping the date rather than subtracting 24 hours: a\n // day that crosses a daylight-saving change is 23 or 25 hours long.\n const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n if (sameCalendarDay(created, yesterday)) return copy.yesterdayAt(time);\n\n return copy.dateAt(created.toLocaleDateString(locale, { month: \"short\", day: \"numeric\" }), time);\n };\n\n const getFileTypeLabel = (contentType: string | undefined): string | null => {\n if (!contentType) return null;\n switch (getSimplifiedMediaType(contentType)) {\n case \"image\":\n return copy.fileType.image;\n case \"video\":\n return copy.fileType.video;\n case \"audio\":\n return copy.fileType.audio;\n default:\n return copy.fileType.other;\n }\n };\n\n return { formatUploadTimestamp, getFileTypeLabel };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/upload-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.16",
|
|
4
4
|
"description": "Upload UI primitives: local upload guard provider, useGuardedUpload helper, DeferredUploadFormShell for forms that defer Firebase Storage upload until form submit.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"prepublishOnly": "npm run clean && npm run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ttt-productions/file-input": "^0.11.
|
|
44
|
-
"@ttt-productions/media-schemas": "^0.14.
|
|
45
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
43
|
+
"@ttt-productions/file-input": "^0.11.15",
|
|
44
|
+
"@ttt-productions/media-schemas": "^0.14.10",
|
|
45
|
+
"@ttt-productions/ui-core": "^0.11.33",
|
|
46
46
|
"@ttt-productions/upload-core": "^0.7.5"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|