@oxyhq/services 22.5.0 → 22.6.1
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/lib/commonjs/index.js +1 -1
- package/lib/commonjs/ui/client.js +1 -1
- package/lib/commonjs/ui/components/FontLoader.js +18 -145
- package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
- package/lib/commonjs/ui/components/FontLoader.native.js +88 -0
- package/lib/commonjs/ui/components/FontLoader.native.js.map +1 -0
- package/lib/commonjs/ui/components/OxyProvider.js +1 -1
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js +34 -41
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/styles.js +1 -225
- package/lib/commonjs/ui/components/fileManagement/styles.js.map +1 -1
- package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js +0 -2
- package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +5 -2
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/useOxyAccountGraph.js +7 -0
- package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +10 -4
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/userCache.js +33 -9
- package/lib/commonjs/ui/hooks/queries/userCache.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAvatarPicker.js +23 -8
- package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js +7 -21
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js.map +1 -1
- package/lib/commonjs/ui/hooks/useResolvedFileUrls.js +163 -0
- package/lib/commonjs/ui/hooks/useResolvedFileUrls.js.map +1 -0
- package/lib/commonjs/ui/index.js +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +51 -22
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +227 -204
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +26 -9
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/photoGridLayout.js +44 -0
- package/lib/commonjs/ui/screens/fileManagement/photoGridLayout.js.map +1 -0
- package/lib/commonjs/ui/session/tokenTransport.js +21 -10
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
- package/lib/commonjs/ui/utils/fileManagement.js +3 -43
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/client.js +1 -1
- package/lib/module/ui/client.js.map +1 -1
- package/lib/module/ui/components/FontLoader.js +18 -146
- package/lib/module/ui/components/FontLoader.js.map +1 -1
- package/lib/module/ui/components/FontLoader.native.js +83 -0
- package/lib/module/ui/components/FontLoader.native.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +1 -1
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js +34 -41
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/module/ui/components/fileManagement/styles.js +0 -224
- package/lib/module/ui/components/fileManagement/styles.js.map +1 -1
- package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js +0 -2
- package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +5 -2
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/useOxyAccountGraph.js +7 -0
- package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +10 -4
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/queries/userCache.js +33 -9
- package/lib/module/ui/hooks/queries/userCache.js.map +1 -1
- package/lib/module/ui/hooks/useAvatarPicker.js +23 -8
- package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/module/ui/hooks/useFileDownloadUrl.js +7 -21
- package/lib/module/ui/hooks/useFileDownloadUrl.js.map +1 -1
- package/lib/module/ui/hooks/useResolvedFileUrls.js +157 -0
- package/lib/module/ui/hooks/useResolvedFileUrls.js.map +1 -0
- package/lib/module/ui/index.js +1 -1
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +52 -23
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +229 -206
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +26 -9
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/photoGridLayout.js +39 -0
- package/lib/module/ui/screens/fileManagement/photoGridLayout.js.map +1 -0
- package/lib/module/ui/session/tokenTransport.js +21 -10
- package/lib/module/ui/session/tokenTransport.js.map +1 -1
- package/lib/module/ui/utils/fileManagement.js +3 -42
- package/lib/module/ui/utils/fileManagement.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/FontLoader.d.ts +14 -5
- package/lib/typescript/commonjs/ui/components/FontLoader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/FontLoader.native.d.ts +13 -0
- package/lib/typescript/commonjs/ui/components/FontLoader.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/fileManagement/styles.d.ts +0 -218
- package/lib/typescript/commonjs/ui/components/fileManagement/styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +2 -2
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +9 -0
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts +3 -0
- package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts +3 -2
- package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useResolvedFileUrls.d.ts +48 -0
- package/lib/typescript/commonjs/ui/hooks/useResolvedFileUrls.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/photoGridLayout.d.ts +37 -0
- package/lib/typescript/commonjs/ui/screens/fileManagement/photoGridLayout.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +15 -8
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts +4 -11
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/client.d.ts +1 -1
- package/lib/typescript/module/ui/client.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FontLoader.d.ts +14 -5
- package/lib/typescript/module/ui/components/FontLoader.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FontLoader.native.d.ts +13 -0
- package/lib/typescript/module/ui/components/FontLoader.native.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/fileManagement/styles.d.ts +0 -218
- package/lib/typescript/module/ui/components/fileManagement/styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +2 -2
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +9 -0
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/userCache.d.ts +3 -0
- package/lib/typescript/module/ui/hooks/queries/userCache.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts +3 -2
- package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useResolvedFileUrls.d.ts +48 -0
- package/lib/typescript/module/ui/hooks/useResolvedFileUrls.d.ts.map +1 -0
- package/lib/typescript/module/ui/index.d.ts +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/photoGridLayout.d.ts +37 -0
- package/lib/typescript/module/ui/screens/fileManagement/photoGridLayout.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +15 -8
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/fileManagement.d.ts +4 -11
- package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
- package/package.json +5 -10
- package/src/ui/components/FontLoader.native.tsx +88 -0
- package/src/ui/components/FontLoader.tsx +16 -151
- package/src/ui/components/fileManagement/FileDetailsModal.tsx +29 -28
- package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
- package/src/ui/components/fileManagement/styles.ts +0 -225
- package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +0 -1
- package/src/ui/context/hooks/useAuthOperations.ts +5 -2
- package/src/ui/context/useOxyAccountGraph.ts +5 -0
- package/src/ui/hooks/queries/__tests__/userCache.test.ts +29 -0
- package/src/ui/hooks/queries/useAccountQueries.ts +10 -4
- package/src/ui/hooks/queries/userCache.ts +31 -8
- package/src/ui/hooks/useAvatarPicker.ts +27 -7
- package/src/ui/hooks/useFileDownloadUrl.ts +7 -23
- package/src/ui/hooks/useResolvedFileUrls.ts +170 -0
- package/src/ui/screens/FileManagementScreen.tsx +55 -27
- package/src/ui/screens/__tests__/photoGridLayout.test.ts +57 -0
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +229 -215
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +27 -3
- package/src/ui/screens/fileManagement/photoGridLayout.ts +45 -0
- package/src/ui/session/__tests__/tokenTransport.test.ts +25 -10
- package/src/ui/session/tokenTransport.ts +21 -10
- package/src/ui/utils/fileManagement.ts +8 -49
- package/lib/typescript/types/expo-vector-icons.d.ts +0 -22
- package/src/types/expo-vector-icons.d.ts +0 -22
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import type { FileMetadata } from '@oxyhq/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The variant-aware batch resolver contract (`@oxyhq/core`).
|
|
7
|
+
*
|
|
8
|
+
* `getFileDownloadUrls` takes a per-file `{ fileId, variant }` list and returns
|
|
9
|
+
* a map keyed by `fileId`; ids that are denied or missing are OMITTED (never an
|
|
10
|
+
* empty string), and PRIVATE assets resolve to a working scoped-media-token
|
|
11
|
+
* stream URL. Typed here rather than `Pick<OxyServices, …>` so this file states
|
|
12
|
+
* the exact contract it depends on.
|
|
13
|
+
*/
|
|
14
|
+
export interface AssetUrlBatchResolver {
|
|
15
|
+
getFileDownloadUrls(
|
|
16
|
+
requests: Array<{ fileId: string; variant?: string }>,
|
|
17
|
+
options?: { expiresIn?: number; context?: string },
|
|
18
|
+
): Promise<Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Private-safe grid/thumbnail URL resolution.
|
|
23
|
+
*
|
|
24
|
+
* The synchronous `oxyServices.getFileDownloadUrl(id, variant)` always yields
|
|
25
|
+
* the public CDN origin (`cloud.oxy.so/<id>`), which 404s for PRIVATE assets —
|
|
26
|
+
* and uploads default to private. Thumbnails and previews therefore have to be
|
|
27
|
+
* resolved through the authenticated batch endpoint, which returns a working
|
|
28
|
+
* `/assets/:id/stream?…&mt=<scoped media token>` URL for private files.
|
|
29
|
+
*
|
|
30
|
+
* This hook resolves an ENTIRE page of files in ONE (chunked) batch request
|
|
31
|
+
* rather than N per-tile calls, and caches the result in React Query keyed by
|
|
32
|
+
* the (fileId, variant) set. `staleTime` is deliberately shorter than the
|
|
33
|
+
* server media-token TTL so a tile never renders a URL whose token has already
|
|
34
|
+
* expired.
|
|
35
|
+
*
|
|
36
|
+
* Optimistic (`temp-…` / `uploading`) entries are NEVER resolved here — an
|
|
37
|
+
* asset URL must never be built from an id the server does not yet know. Their
|
|
38
|
+
* preview comes from the locally-picked uri (see {@link fileThumbSource}).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** Server media-token lifetime we request for each resolved URL (seconds). */
|
|
42
|
+
const ASSET_URL_TTL_SECONDS = 600; // 10 minutes
|
|
43
|
+
/**
|
|
44
|
+
* Refresh window. Kept below {@link ASSET_URL_TTL_SECONDS} so React Query
|
|
45
|
+
* re-resolves in the background before a rendered URL's token can die.
|
|
46
|
+
*/
|
|
47
|
+
const ASSET_URL_STALE_MS = 5 * 60 * 1000; // 5 minutes
|
|
48
|
+
const ASSET_URL_GC_MS = 30 * 60 * 1000;
|
|
49
|
+
/** Server-side batch cap for `POST /assets/batch-access` — chunk beyond this. */
|
|
50
|
+
const BATCH_CAP = 100;
|
|
51
|
+
|
|
52
|
+
/** React Query prefix — scoped media URLs must never leak across accounts. */
|
|
53
|
+
export const ASSET_DOWNLOAD_URLS_QUERY_KEY = 'assetDownloadUrls' as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* True when a file is an optimistic placeholder that has not been persisted by
|
|
57
|
+
* the server yet — either its id is still the client-minted `temp-…` id, or it
|
|
58
|
+
* carries the `uploading` flag. Mirrors the `temp-` id guard precedent in
|
|
59
|
+
* `@oxyhq/core`'s `avatarUtils.updateAvatarVisibility`.
|
|
60
|
+
*/
|
|
61
|
+
export function isOptimisticFile(file: Pick<FileMetadata, 'id' | 'metadata'>): boolean {
|
|
62
|
+
if (typeof file.id === 'string' && file.id.startsWith('temp-')) return true;
|
|
63
|
+
return file.metadata?.uploading === true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Grid thumbnail variant for a file, or `undefined` when it needs no URL. */
|
|
67
|
+
function thumbVariantFor(file: FileMetadata): string | undefined {
|
|
68
|
+
if (file.contentType.startsWith('video/')) return 'poster';
|
|
69
|
+
if (file.contentType.startsWith('image/')) return 'thumb';
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Locally-picked preview uri stashed on an optimistic entry's metadata by the
|
|
75
|
+
* upload flow, if present.
|
|
76
|
+
*/
|
|
77
|
+
function localPreviewUri(file: FileMetadata): string | undefined {
|
|
78
|
+
const uri = (file.metadata as Record<string, unknown> | undefined)?.localPreviewUri;
|
|
79
|
+
return typeof uri === 'string' && uri.length > 0 ? uri : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the image source for a grid tile.
|
|
84
|
+
*
|
|
85
|
+
* - Optimistic entry → its locally-picked preview uri (never an asset URL).
|
|
86
|
+
* - Persisted entry → the private-safe URL from {@link useResolvedFileUrls},
|
|
87
|
+
* or `undefined` while it is still resolving / was denied (tile shows a
|
|
88
|
+
* placeholder instead of a guaranteed-404 CDN URL).
|
|
89
|
+
*/
|
|
90
|
+
export function fileThumbSource(
|
|
91
|
+
file: FileMetadata,
|
|
92
|
+
resolved: ReadonlyMap<string, string>,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
if (isOptimisticFile(file)) {
|
|
95
|
+
return localPreviewUri(file);
|
|
96
|
+
}
|
|
97
|
+
return resolved.get(file.id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Batch-resolve private-safe thumbnail URLs for the given files.
|
|
102
|
+
*
|
|
103
|
+
* Only image/video, non-optimistic files are resolved (documents render an
|
|
104
|
+
* icon, optimistic entries render their local preview). Returns a stable
|
|
105
|
+
* `Map<fileId, url>`; missing entries mean "not resolved yet or access denied".
|
|
106
|
+
*/
|
|
107
|
+
export function useResolvedFileUrls(
|
|
108
|
+
oxyServices: AssetUrlBatchResolver,
|
|
109
|
+
files: FileMetadata[],
|
|
110
|
+
/** Active viewer account id — media tokens are minted per user; must be in the query key. */
|
|
111
|
+
viewerId?: string | null,
|
|
112
|
+
): ReadonlyMap<string, string> {
|
|
113
|
+
const viewerKey = viewerId ?? '';
|
|
114
|
+
const requests = useMemo(() => {
|
|
115
|
+
const list: Array<{ fileId: string; variant?: string }> = [];
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
if (isOptimisticFile(file)) continue;
|
|
118
|
+
const variant = thumbVariantFor(file);
|
|
119
|
+
if (!variant) continue;
|
|
120
|
+
list.push({ fileId: file.id, variant });
|
|
121
|
+
}
|
|
122
|
+
return list;
|
|
123
|
+
}, [files]);
|
|
124
|
+
|
|
125
|
+
// A content signature so the query key is stable across renders while the
|
|
126
|
+
// resolvable set is unchanged (the `files` array is a fresh reference every
|
|
127
|
+
// render). Sorted so ordering churn does not force a refetch.
|
|
128
|
+
const signature = useMemo(
|
|
129
|
+
() => requests.map((r) => `${r.fileId}:${r.variant ?? ''}`).sort().join('|'),
|
|
130
|
+
[requests],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const query = useQuery({
|
|
134
|
+
queryKey: [ASSET_DOWNLOAD_URLS_QUERY_KEY, viewerKey, signature],
|
|
135
|
+
enabled: requests.length > 0 && viewerKey.length > 0,
|
|
136
|
+
staleTime: ASSET_URL_STALE_MS,
|
|
137
|
+
gcTime: ASSET_URL_GC_MS,
|
|
138
|
+
// Keep previously-resolved URLs on screen while the SAME viewer's file set
|
|
139
|
+
// grows (pagination), but never carry another account's scoped media tokens
|
|
140
|
+
// across a switch — `keepPreviousData` alone would do that.
|
|
141
|
+
placeholderData: (previousData, previousQuery) => {
|
|
142
|
+
if (!previousData || !previousQuery) return undefined;
|
|
143
|
+
const prevViewer = previousQuery.queryKey[1];
|
|
144
|
+
if (prevViewer !== viewerKey) return undefined;
|
|
145
|
+
return previousData;
|
|
146
|
+
},
|
|
147
|
+
queryFn: async (): Promise<Record<string, string>> => {
|
|
148
|
+
const merged: Record<string, string> = {};
|
|
149
|
+
for (let i = 0; i < requests.length; i += BATCH_CAP) {
|
|
150
|
+
const chunk = requests.slice(i, i + BATCH_CAP);
|
|
151
|
+
const urls = await oxyServices.getFileDownloadUrls(chunk, {
|
|
152
|
+
expiresIn: ASSET_URL_TTL_SECONDS,
|
|
153
|
+
});
|
|
154
|
+
Object.assign(merged, urls);
|
|
155
|
+
}
|
|
156
|
+
return merged;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return useMemo(() => {
|
|
161
|
+
const map = new Map<string, string>();
|
|
162
|
+
const data = query.data;
|
|
163
|
+
if (data) {
|
|
164
|
+
for (const [id, url] of Object.entries(data)) {
|
|
165
|
+
if (typeof url === 'string' && url.length > 0) map.set(id, url);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return map;
|
|
169
|
+
}, [query.data]);
|
|
170
|
+
}
|
|
@@ -28,8 +28,8 @@ import { useUploadFile } from '../hooks/mutations/useAccountMutations';
|
|
|
28
28
|
import {
|
|
29
29
|
formatFileSize,
|
|
30
30
|
getFileIcon,
|
|
31
|
-
getSafeDownloadUrl,
|
|
32
31
|
} from '../utils/fileManagement';
|
|
32
|
+
import { useResolvedFileUrls, fileThumbSource } from '../hooks/useResolvedFileUrls';
|
|
33
33
|
import { FileViewer } from '../components/fileManagement/FileViewer';
|
|
34
34
|
import { FileDetailsModal } from '../components/fileManagement/FileDetailsModal';
|
|
35
35
|
import { UploadPreview } from '../components/fileManagement/UploadPreview';
|
|
@@ -356,12 +356,18 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
356
356
|
onClose?.();
|
|
357
357
|
}, [selectMode, multiSelect, selectedIds, files, onConfirmSelection, onClose, defaultVisibility, oxyServices, linkContext]);
|
|
358
358
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
359
|
+
// Private-safe thumbnail URLs for the currently-shown files, resolved in
|
|
360
|
+
// ONE batch per page (not N per-tile) and cached by React Query. Uploads
|
|
361
|
+
// default to private, so the synchronous public-CDN URL would 404 for
|
|
362
|
+
// every private thumbnail.
|
|
363
|
+
const resolvedThumbUrls = useResolvedFileUrls(oxyServices, filteredFiles, user?.id);
|
|
364
|
+
|
|
365
|
+
// Image source for a grid tile: the resolved private-safe URL for a
|
|
366
|
+
// persisted file, or the locally-picked preview for an in-flight optimistic
|
|
367
|
+
// entry. Never builds an asset URL from a `temp-…`/uploading id.
|
|
368
|
+
const thumbSourceFor = useCallback(
|
|
369
|
+
(file: FileMetadata): string | undefined => fileThumbSource(file, resolvedThumbUrls),
|
|
370
|
+
[resolvedThumbUrls],
|
|
365
371
|
);
|
|
366
372
|
|
|
367
373
|
const bloomTheme = useTheme();
|
|
@@ -480,7 +486,14 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
480
486
|
await Promise.all(
|
|
481
487
|
photosToLoad.map(async (photo) => {
|
|
482
488
|
try {
|
|
483
|
-
const downloadUrl =
|
|
489
|
+
const downloadUrl = thumbSourceFor(photo);
|
|
490
|
+
// URL not resolved yet — skip (do NOT record fallback
|
|
491
|
+
// dims) so this photo is re-measured once its private-safe
|
|
492
|
+
// URL resolves. Measuring the broken public URL is exactly
|
|
493
|
+
// the bug we are fixing.
|
|
494
|
+
if (!downloadUrl) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
484
497
|
|
|
485
498
|
// Unified approach using Image.getSize (works on all platforms)
|
|
486
499
|
await new Promise<void>((resolve) => {
|
|
@@ -515,7 +528,19 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
515
528
|
} finally {
|
|
516
529
|
setLoadingDimensions(false);
|
|
517
530
|
}
|
|
518
|
-
}, [
|
|
531
|
+
}, [thumbSourceFor, photoDimensions]);
|
|
532
|
+
|
|
533
|
+
// Re-measure photo dimensions when their private-safe URLs resolve. The
|
|
534
|
+
// justified grid's own trigger fires on the photo SET, not on URL
|
|
535
|
+
// availability, so newly-resolved private tiles would otherwise keep their
|
|
536
|
+
// skipped (unmeasured) state and render with fallback geometry.
|
|
537
|
+
useEffect(() => {
|
|
538
|
+
if (viewMode !== 'photos') return;
|
|
539
|
+
const photos = filteredFiles.filter((file) => file.contentType.startsWith('image/'));
|
|
540
|
+
if (photos.length > 0) {
|
|
541
|
+
loadPhotoDimensions(photos);
|
|
542
|
+
}
|
|
543
|
+
}, [resolvedThumbUrls, viewMode, filteredFiles, loadPhotoDimensions]);
|
|
519
544
|
|
|
520
545
|
// Create justified rows from photos with responsive algorithm
|
|
521
546
|
const createJustifiedRows = useCallback((photos: FileMetadata[], containerWidth: number) => {
|
|
@@ -646,9 +671,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
646
671
|
// Unified download function - works on all platforms
|
|
647
672
|
const handleFileDownload = async (fileId: string, filename: string) => {
|
|
648
673
|
try {
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
|
|
674
|
+
// Resolve an authenticated, private-safe URL. The synchronous
|
|
675
|
+
// `getFileDownloadUrl` yields the public CDN origin, which 404s for
|
|
676
|
+
// private assets (the default visibility).
|
|
677
|
+
const downloadUrl = await oxyServices.getFileDownloadUrlAsync(fileId);
|
|
652
678
|
|
|
653
679
|
// For web platforms, use link download
|
|
654
680
|
if (typeof window !== 'undefined' && window.document) {
|
|
@@ -715,8 +741,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
715
741
|
file.contentType.includes('pdf') ||
|
|
716
742
|
file.contentType.startsWith('video/') ||
|
|
717
743
|
file.contentType.startsWith('audio/')) {
|
|
718
|
-
// For images, PDFs, videos, and audio, we
|
|
719
|
-
|
|
744
|
+
// For images, PDFs, videos, and audio, we render the URL
|
|
745
|
+
// directly. Resolve the authenticated, private-safe URL —
|
|
746
|
+
// the synchronous public-CDN URL 404s for private assets.
|
|
747
|
+
const downloadUrl = await oxyServices.getFileDownloadUrlAsync(file.id);
|
|
720
748
|
setFileContent(downloadUrl);
|
|
721
749
|
} else {
|
|
722
750
|
// For text files, get the content using authenticated request
|
|
@@ -755,7 +783,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
755
783
|
};
|
|
756
784
|
|
|
757
785
|
const renderSimplePhotoItem = useCallback((photo: FileMetadata, index: number) => {
|
|
758
|
-
const downloadUrl =
|
|
786
|
+
const downloadUrl = thumbSourceFor(photo);
|
|
759
787
|
|
|
760
788
|
// Calculate photo item width based on actual container size from bottom sheet
|
|
761
789
|
let itemsPerRow = 3; // Default for mobile
|
|
@@ -803,10 +831,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
803
831
|
</View>
|
|
804
832
|
</TouchableOpacity>
|
|
805
833
|
);
|
|
806
|
-
}, [
|
|
834
|
+
}, [thumbSourceFor, safeContainerWidth, selectMode, selectedIds, colors.primary, colors.text]);
|
|
807
835
|
|
|
808
836
|
const renderJustifiedPhotoItem = useCallback((photo: FileMetadata, width: number, height: number, isLast: boolean) => {
|
|
809
|
-
const downloadUrl =
|
|
837
|
+
const downloadUrl = thumbSourceFor(photo);
|
|
810
838
|
|
|
811
839
|
return (
|
|
812
840
|
<TouchableOpacity
|
|
@@ -843,7 +871,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
843
871
|
</View>
|
|
844
872
|
</TouchableOpacity>
|
|
845
873
|
);
|
|
846
|
-
}, [
|
|
874
|
+
}, [thumbSourceFor, selectMode, selectedIds, multiSelect, colors.primary, colors.text]);
|
|
847
875
|
|
|
848
876
|
// Run initial load once per targetUserId change to avoid accidental loops
|
|
849
877
|
const lastLoadedFor = useRef<string | undefined>(undefined);
|
|
@@ -879,7 +907,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
879
907
|
<View style={fileManagementStyles.filePreview}>
|
|
880
908
|
{isImage && (
|
|
881
909
|
<ExpoImage
|
|
882
|
-
source={{ uri:
|
|
910
|
+
source={{ uri: thumbSourceFor(file) }}
|
|
883
911
|
style={fileManagementStyles.previewImage}
|
|
884
912
|
contentFit="cover"
|
|
885
913
|
transition={120}
|
|
@@ -899,7 +927,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
899
927
|
{isVideo && (
|
|
900
928
|
<View style={fileManagementStyles.videoPreviewWrapper}>
|
|
901
929
|
<ExpoImage
|
|
902
|
-
source={{ uri:
|
|
930
|
+
source={{ uri: thumbSourceFor(file) }}
|
|
903
931
|
style={fileManagementStyles.videoPosterImage}
|
|
904
932
|
contentFit="cover"
|
|
905
933
|
transition={120}
|
|
@@ -919,7 +947,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
919
947
|
style={[fileManagementStyles.fallbackIcon, { display: isImage ? 'none' : 'flex' }]}
|
|
920
948
|
>
|
|
921
949
|
<Ionicons
|
|
922
|
-
name={getFileIcon(file.contentType)
|
|
950
|
+
name={getFileIcon(file.contentType)}
|
|
923
951
|
size={32}
|
|
924
952
|
color={colors.primary}
|
|
925
953
|
/>
|
|
@@ -934,7 +962,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
934
962
|
) : (
|
|
935
963
|
<View style={fileManagementStyles.fileIconContainer}>
|
|
936
964
|
<Ionicons
|
|
937
|
-
name={getFileIcon(file.contentType)
|
|
965
|
+
name={getFileIcon(file.contentType)}
|
|
938
966
|
size={32}
|
|
939
967
|
color={colors.primary}
|
|
940
968
|
/>
|
|
@@ -1016,7 +1044,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1016
1044
|
fileIcon = (
|
|
1017
1045
|
<View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden' }}>
|
|
1018
1046
|
<ExpoImage
|
|
1019
|
-
source={{ uri:
|
|
1047
|
+
source={{ uri: thumbSourceFor(file) }}
|
|
1020
1048
|
style={{ width: 36, height: 36 }}
|
|
1021
1049
|
contentFit="cover"
|
|
1022
1050
|
transition={120}
|
|
@@ -1032,7 +1060,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1032
1060
|
fileIcon = (
|
|
1033
1061
|
<View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden', backgroundColor: '#000000', position: 'relative' }}>
|
|
1034
1062
|
<ExpoImage
|
|
1035
|
-
source={{ uri:
|
|
1063
|
+
source={{ uri: thumbSourceFor(file) }}
|
|
1036
1064
|
style={{ width: 36, height: 36 }}
|
|
1037
1065
|
contentFit="cover"
|
|
1038
1066
|
transition={120}
|
|
@@ -1102,7 +1130,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1102
1130
|
) : undefined,
|
|
1103
1131
|
};
|
|
1104
1132
|
});
|
|
1105
|
-
}, [filteredFiles, theme, deleting, handleFileDownload, confirmFileDelete, handleFileOpen,
|
|
1133
|
+
}, [filteredFiles, theme, deleting, handleFileDownload, confirmFileDelete, handleFileOpen, thumbSourceFor, selectMode, selectedIds]);
|
|
1106
1134
|
|
|
1107
1135
|
// Scroll to selected file after selection
|
|
1108
1136
|
useEffect(() => {
|
|
@@ -1191,7 +1219,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1191
1219
|
}, [lastSelectedFileId]);
|
|
1192
1220
|
|
|
1193
1221
|
const renderPhotoItem = (photo: FileMetadata, index: number) => {
|
|
1194
|
-
const downloadUrl =
|
|
1222
|
+
const downloadUrl = thumbSourceFor(photo);
|
|
1195
1223
|
|
|
1196
1224
|
// Calculate photo item width based on actual container size from bottom sheet
|
|
1197
1225
|
let itemsPerRow = 3; // Default for mobile
|
|
@@ -1570,7 +1598,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1570
1598
|
hasMore={paging.hasMore}
|
|
1571
1599
|
loadingMore={paging.loadingMore}
|
|
1572
1600
|
reduceMotion={reduceMotion}
|
|
1573
|
-
getThumbUrl={
|
|
1601
|
+
getThumbUrl={thumbSourceFor}
|
|
1574
1602
|
primaryColor={colors.primary}
|
|
1575
1603
|
isOwner={isOwner}
|
|
1576
1604
|
onTogglePhoto={toggleSelect}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computePhotoGridLayout,
|
|
3
|
+
PHOTO_GRID_GUTTER,
|
|
4
|
+
PHOTO_GRID_WIDE_BREAKPOINT,
|
|
5
|
+
} from '../fileManagement/photoGridLayout';
|
|
6
|
+
|
|
7
|
+
describe('computePhotoGridLayout', () => {
|
|
8
|
+
it('uses 3 columns below the wide breakpoint', () => {
|
|
9
|
+
expect(computePhotoGridLayout(390).columns).toBe(3);
|
|
10
|
+
expect(computePhotoGridLayout(500).columns).toBe(3);
|
|
11
|
+
expect(computePhotoGridLayout(PHOTO_GRID_WIDE_BREAKPOINT - 1).columns).toBe(3);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('uses 4 columns at/above the wide breakpoint', () => {
|
|
15
|
+
expect(computePhotoGridLayout(PHOTO_GRID_WIDE_BREAKPOINT).columns).toBe(4);
|
|
16
|
+
expect(computePhotoGridLayout(700).columns).toBe(4);
|
|
17
|
+
expect(computePhotoGridLayout(1024).columns).toBe(4);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('floors the tile size so a full row never exceeds the container', () => {
|
|
21
|
+
// The whole point of the bug fix: geometry derives from the passed
|
|
22
|
+
// container width, and a full row of tiles + gutters must fit inside it.
|
|
23
|
+
for (const width of [320, 375, 390, 428, 500, 599, 600, 768, 834, 1024]) {
|
|
24
|
+
const { columns, cellSize, gutter } = computePhotoGridLayout(width);
|
|
25
|
+
const rowWidth = columns * cellSize + (columns - 1) * gutter;
|
|
26
|
+
expect(rowWidth).toBeLessThanOrEqual(width);
|
|
27
|
+
// ...and the floor is tight (adding one px per tile would overflow).
|
|
28
|
+
expect((columns * (cellSize + 1)) + (columns - 1) * gutter).toBeGreaterThan(width);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('derives smaller tiles from a narrow sheet than from a wide screen', () => {
|
|
33
|
+
// Regression guard for the original bug: a centered dialog (narrow
|
|
34
|
+
// container) must NOT size tiles from a wider screen width.
|
|
35
|
+
const narrowSheet = computePhotoGridLayout(500); // 3 cols
|
|
36
|
+
const wideScreen = computePhotoGridLayout(1200); // 4 cols
|
|
37
|
+
expect(narrowSheet.cellSize).toBeLessThan(wideScreen.cellSize);
|
|
38
|
+
// Cross-check the exact arithmetic against the gutter constant.
|
|
39
|
+
expect(narrowSheet.cellSize).toBe(
|
|
40
|
+
Math.floor((500 - PHOTO_GRID_GUTTER * 2) / 3),
|
|
41
|
+
);
|
|
42
|
+
expect(wideScreen.cellSize).toBe(
|
|
43
|
+
Math.floor((1200 - PHOTO_GRID_GUTTER * 3) / 4),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns cellSize 0 for a not-yet-measured (<= 0) width', () => {
|
|
48
|
+
expect(computePhotoGridLayout(0).cellSize).toBe(0);
|
|
49
|
+
expect(computePhotoGridLayout(-10).cellSize).toBe(0);
|
|
50
|
+
// Columns still resolve to the narrow default so callers can render.
|
|
51
|
+
expect(computePhotoGridLayout(0).columns).toBe(3);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('exposes the gutter it used to derive the tile size', () => {
|
|
55
|
+
expect(computePhotoGridLayout(500).gutter).toBe(PHOTO_GRID_GUTTER);
|
|
56
|
+
});
|
|
57
|
+
});
|