@oxyhq/services 22.5.0 → 22.6.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.
- 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 +16 -27
- 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/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 +216 -203
- 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 +0 -40
- 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 +16 -27
- 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/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 +218 -205
- 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 +0 -39
- 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/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/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/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/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 +17 -18
- package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
- package/src/ui/components/fileManagement/styles.ts +0 -225
- 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 +215 -213
- 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 +3 -46
- package/lib/typescript/types/expo-vector-icons.d.ts +0 -22
- package/src/types/expo-vector-icons.d.ts +0 -22
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
RefreshControl,
|
|
8
8
|
FlatList,
|
|
9
9
|
Platform,
|
|
10
|
-
|
|
10
|
+
type LayoutChangeEvent,
|
|
11
11
|
} from 'react-native';
|
|
12
12
|
import { Image as ExpoImage } from 'expo-image';
|
|
13
13
|
import Reanimated, {
|
|
@@ -20,7 +20,7 @@ import Reanimated, {
|
|
|
20
20
|
} from 'react-native-reanimated';
|
|
21
21
|
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
|
22
22
|
import type { FileMetadata } from '@oxyhq/core';
|
|
23
|
-
import {
|
|
23
|
+
import { computePhotoGridLayout } from './photoGridLayout';
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Haptic feedback wrapper. `expo-haptics` is an optional dependency — when not
|
|
@@ -96,6 +96,22 @@ const hapticNotification = async (type: HapticNotification): Promise<void> => {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The bottom sheet renders below the status bar already (its `maxHeight` is
|
|
101
|
+
* capped by `SCREEN_HEIGHT - insets.top`), so the picker MUST NOT add an extra
|
|
102
|
+
* safe-area inset to the header. Header zone from the sheet top:
|
|
103
|
+
* • 28dp drag-handle hit area floats at the very top of the sheet
|
|
104
|
+
* • 56dp app bar sits immediately below the handle
|
|
105
|
+
* Grid content is pushed down by the full header zone + 4dp of breathing room
|
|
106
|
+
* so the first row clears the translucent header. These are fixed layout
|
|
107
|
+
* constants — the equivalent `pt-[28px]` / `min-h-[84px]` / `pt-[88px]` utility
|
|
108
|
+
* classes in the JSX MUST stay in sync with them.
|
|
109
|
+
*/
|
|
110
|
+
const HANDLE_ZONE = 28;
|
|
111
|
+
const APP_BAR_HEIGHT = 56;
|
|
112
|
+
const HEADER_HEIGHT = HANDLE_ZONE + APP_BAR_HEIGHT; // 84
|
|
113
|
+
const GRID_CONTENT_PADDING_TOP = HEADER_HEIGHT + 4; // 88
|
|
114
|
+
|
|
99
115
|
/**
|
|
100
116
|
* Props for the dedicated photo picker view. Used by FileManagementScreen
|
|
101
117
|
* only when `selectMode + image-only` is active. All callbacks are wired by
|
|
@@ -114,7 +130,7 @@ export interface PhotoPickerViewProps {
|
|
|
114
130
|
hasMore: boolean;
|
|
115
131
|
loadingMore: boolean;
|
|
116
132
|
reduceMotion: boolean;
|
|
117
|
-
getThumbUrl: (file: FileMetadata
|
|
133
|
+
getThumbUrl: (file: FileMetadata) => string | undefined;
|
|
118
134
|
primaryColor: string;
|
|
119
135
|
isOwner: boolean;
|
|
120
136
|
onTogglePhoto: (photo: FileMetadata) => void;
|
|
@@ -143,7 +159,7 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
|
|
|
143
159
|
selectionIndex: number; // 1-based for badge; 0 if not selected
|
|
144
160
|
dim: boolean; // any selection exists and this cell is not selected
|
|
145
161
|
primaryColor: string;
|
|
146
|
-
thumbUrl: string;
|
|
162
|
+
thumbUrl: string | undefined;
|
|
147
163
|
enterIndex: number;
|
|
148
164
|
reduceMotion: boolean;
|
|
149
165
|
onPress: () => void;
|
|
@@ -187,12 +203,25 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
|
|
|
187
203
|
transform: [{ scale: ringScale.value }],
|
|
188
204
|
}));
|
|
189
205
|
|
|
206
|
+
// Per-tile pixel geometry is derived from the measured grid width, so it
|
|
207
|
+
// stays an inline `style` — NativeWind cannot express a runtime pixel size.
|
|
208
|
+
const cellWrapperStyle = {
|
|
209
|
+
width: size,
|
|
210
|
+
height: size,
|
|
211
|
+
marginRight,
|
|
212
|
+
marginBottom,
|
|
213
|
+
};
|
|
214
|
+
|
|
190
215
|
const inner = (
|
|
191
216
|
<>
|
|
192
|
-
<View
|
|
217
|
+
<View
|
|
218
|
+
className={`flex-1 rounded-radius-8 overflow-hidden bg-[#111111]${dim ? ' opacity-60' : ''}`}
|
|
219
|
+
>
|
|
193
220
|
<ExpoImage
|
|
194
221
|
source={{ uri: thumbUrl }}
|
|
195
|
-
|
|
222
|
+
// expo-image is a third-party component (no NativeWind
|
|
223
|
+
// className remap), so fill via inline style.
|
|
224
|
+
style={{ width: '100%', height: '100%' }}
|
|
196
225
|
contentFit="cover"
|
|
197
226
|
transition={120}
|
|
198
227
|
cachePolicy="memory-disk"
|
|
@@ -202,19 +231,19 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
|
|
|
202
231
|
{isSelected && (
|
|
203
232
|
<Reanimated.View
|
|
204
233
|
pointerEvents="none"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
]}
|
|
234
|
+
className="absolute inset-0 rounded-radius-8 border-[3px]"
|
|
235
|
+
// borderColor is the theme primary (dynamic); the animated
|
|
236
|
+
// scale transform must ride on `style` too.
|
|
237
|
+
style={[{ borderColor: primaryColor }, ringAnimatedStyle]}
|
|
210
238
|
/>
|
|
211
239
|
)}
|
|
212
240
|
{isSelected && (
|
|
213
241
|
<View
|
|
214
242
|
pointerEvents="none"
|
|
215
|
-
|
|
243
|
+
className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
|
|
244
|
+
style={{ backgroundColor: primaryColor }}
|
|
216
245
|
>
|
|
217
|
-
<Text
|
|
246
|
+
<Text className="text-white text-[12px] font-bold leading-[14px]">
|
|
218
247
|
{selectionIndex > 0 ? String(selectionIndex) : ''}
|
|
219
248
|
</Text>
|
|
220
249
|
</View>
|
|
@@ -222,20 +251,14 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
|
|
|
222
251
|
</>
|
|
223
252
|
);
|
|
224
253
|
|
|
225
|
-
const cellWrapperStyle = {
|
|
226
|
-
width: size,
|
|
227
|
-
height: size,
|
|
228
|
-
marginRight,
|
|
229
|
-
marginBottom,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
254
|
if (reduceMotion) {
|
|
233
255
|
return (
|
|
234
256
|
<TouchableOpacity
|
|
235
257
|
activeOpacity={0.85}
|
|
236
258
|
onPress={onPress}
|
|
237
259
|
onLongPress={onLongPress}
|
|
238
|
-
|
|
260
|
+
className="relative"
|
|
261
|
+
style={cellWrapperStyle}
|
|
239
262
|
accessibilityRole="button"
|
|
240
263
|
accessibilityLabel={a11yLabel}
|
|
241
264
|
accessibilityState={{ selected: isSelected }}
|
|
@@ -248,13 +271,14 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
|
|
|
248
271
|
return (
|
|
249
272
|
<Reanimated.View
|
|
250
273
|
entering={FadeIn.delay(delay).duration(200)}
|
|
251
|
-
|
|
274
|
+
className="relative"
|
|
275
|
+
style={cellWrapperStyle}
|
|
252
276
|
>
|
|
253
277
|
<TouchableOpacity
|
|
254
278
|
activeOpacity={0.85}
|
|
255
279
|
onPress={onPress}
|
|
256
280
|
onLongPress={onLongPress}
|
|
257
|
-
|
|
281
|
+
className="flex-1"
|
|
258
282
|
accessibilityRole="button"
|
|
259
283
|
accessibilityLabel={a11yLabel}
|
|
260
284
|
accessibilityState={{ selected: isSelected }}
|
|
@@ -290,23 +314,21 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
290
314
|
onConfirm,
|
|
291
315
|
t,
|
|
292
316
|
}) => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}, [
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return Math.floor((windowWidth - GUTTER * (columns - 1)) / columns);
|
|
309
|
-
}, [windowWidth, columns]);
|
|
317
|
+
// Measure the grid CONTAINER (the bottom-sheet content width), NOT the
|
|
318
|
+
// device screen. On wide viewports the sheet is centered/narrower than the
|
|
319
|
+
// screen, so screen-width tile math overflows. The measured wrapper carries
|
|
320
|
+
// ONLY a `style` (no `className`) because RN-Web does not fire `onLayout`
|
|
321
|
+
// for `className`'d nodes.
|
|
322
|
+
const [gridWidth, setGridWidth] = useState(0);
|
|
323
|
+
const onRootLayout = useCallback((e: LayoutChangeEvent) => {
|
|
324
|
+
const w = Math.round(e.nativeEvent.layout.width);
|
|
325
|
+
setGridWidth((prev) => (prev === w ? prev : w));
|
|
326
|
+
}, []);
|
|
327
|
+
|
|
328
|
+
const { columns, cellSize, gutter } = useMemo(
|
|
329
|
+
() => computePhotoGridLayout(gridWidth),
|
|
330
|
+
[gridWidth],
|
|
331
|
+
);
|
|
310
332
|
|
|
311
333
|
// Map selectedIds → 1-based selection order for the badge. We freeze a
|
|
312
334
|
// stable order at the time of selection: the order is the insertion
|
|
@@ -322,28 +344,14 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
322
344
|
|
|
323
345
|
const hasAnySelection = selectedIds.size > 0;
|
|
324
346
|
|
|
325
|
-
// Compact icon-only upload pill on narrow
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// The bottom sheet renders below the status bar already (its `maxHeight`
|
|
329
|
-
// is capped by `SCREEN_HEIGHT - insets.top`), so the picker MUST NOT add
|
|
330
|
-
// an additional safe-area inset to the header. Header layout:
|
|
331
|
-
// • 28dp drag-handle hit area floats at the very top of the sheet
|
|
332
|
-
// • 56dp app bar sits immediately below the handle
|
|
333
|
-
// Total header zone = 28 + 56 = 84dp from sheet top.
|
|
334
|
-
const HANDLE_ZONE = 28;
|
|
335
|
-
const APP_BAR_HEIGHT = 56;
|
|
336
|
-
const headerHeight = HANDLE_ZONE + APP_BAR_HEIGHT;
|
|
337
|
-
const contentPaddingTop = headerHeight + 4;
|
|
347
|
+
// Compact icon-only upload pill on narrow sheets; full pill otherwise.
|
|
348
|
+
// Defaults to the full label until the sheet width has been measured.
|
|
349
|
+
const showUploadLabel = gridWidth === 0 || gridWidth >= 360;
|
|
338
350
|
|
|
339
351
|
const isEmpty = photos.length === 0;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (a11yColumnsAnnouncement.current !== columns) {
|
|
344
|
-
a11yColumnsAnnouncement.current = columns;
|
|
345
|
-
}
|
|
346
|
-
}, [columns]);
|
|
352
|
+
// Only mount the grid once a real container width is known, so columns /
|
|
353
|
+
// tile size derive from the sheet width instead of a placeholder.
|
|
354
|
+
const gridReady = gridWidth > 0 && cellSize > 0;
|
|
347
355
|
|
|
348
356
|
const handleCellPress = useCallback(
|
|
349
357
|
(photo: FileMetadata) => {
|
|
@@ -393,13 +401,13 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
393
401
|
<PhotoPickerCell
|
|
394
402
|
photo={item}
|
|
395
403
|
size={cellSize}
|
|
396
|
-
marginRight={isLastInRow ? 0 :
|
|
397
|
-
marginBottom={
|
|
404
|
+
marginRight={isLastInRow ? 0 : gutter}
|
|
405
|
+
marginBottom={gutter}
|
|
398
406
|
isSelected={isSelected}
|
|
399
407
|
selectionIndex={selIndex}
|
|
400
408
|
dim={multiSelect && hasAnySelection && !isSelected}
|
|
401
409
|
primaryColor={primaryColor}
|
|
402
|
-
thumbUrl={getThumbUrl(item
|
|
410
|
+
thumbUrl={getThumbUrl(item)}
|
|
403
411
|
enterIndex={index}
|
|
404
412
|
reduceMotion={reduceMotion}
|
|
405
413
|
onPress={() => handleCellPress(item)}
|
|
@@ -409,7 +417,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
409
417
|
);
|
|
410
418
|
},
|
|
411
419
|
[
|
|
412
|
-
selectedIds, selectionOrder, columns, cellSize, multiSelect, hasAnySelection,
|
|
420
|
+
selectedIds, selectionOrder, columns, cellSize, gutter, multiSelect, hasAnySelection,
|
|
413
421
|
primaryColor, getThumbUrl, reduceMotion, handleCellPress, handleCellLongPress, t,
|
|
414
422
|
],
|
|
415
423
|
);
|
|
@@ -419,7 +427,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
419
427
|
const listFooter = useMemo(() => {
|
|
420
428
|
if (!loadingMore) return null;
|
|
421
429
|
return (
|
|
422
|
-
<View
|
|
430
|
+
<View className="py-space-16 items-center">
|
|
423
431
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
424
432
|
</View>
|
|
425
433
|
);
|
|
@@ -441,168 +449,162 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
441
449
|
: 0;
|
|
442
450
|
|
|
443
451
|
return (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
452
|
+
// Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
|
|
453
|
+
<View style={{ flex: 1 }} onLayout={onRootLayout}>
|
|
454
|
+
<View className="flex-1 bg-black">
|
|
455
|
+
{/* Photo grid (renders behind translucent header) */}
|
|
456
|
+
{isEmpty ? (
|
|
457
|
+
<View className="flex-1 items-center justify-center px-space-32 pt-[88px]">
|
|
458
|
+
<View className="opacity-30 mb-space-16">
|
|
459
|
+
<MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
|
|
460
|
+
</View>
|
|
461
|
+
<Text className="text-white text-[17px] font-semibold text-center mb-1.5">
|
|
462
|
+
{t('fileManagement.photoPicker.emptyTitle')}
|
|
463
|
+
</Text>
|
|
464
|
+
<Text className="text-white text-[14px] font-normal text-center opacity-70 mb-space-24">
|
|
465
|
+
{t('fileManagement.photoPicker.emptySubtitle')}
|
|
466
|
+
</Text>
|
|
467
|
+
{isOwner && allowUpload && (
|
|
468
|
+
<TouchableOpacity
|
|
469
|
+
className="flex-row items-center gap-space-8 px-[22px] py-space-12 rounded-full"
|
|
470
|
+
style={{ backgroundColor: primaryColor }}
|
|
471
|
+
onPress={onUpload}
|
|
472
|
+
disabled={uploading || isPickingDocument}
|
|
473
|
+
accessibilityRole="button"
|
|
474
|
+
accessibilityLabel={t('fileManagement.uploadPhoto')}
|
|
475
|
+
accessibilityState={{ busy: uploading || isPickingDocument }}
|
|
476
|
+
>
|
|
477
|
+
{(uploading || isPickingDocument) ? (
|
|
478
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
479
|
+
) : (
|
|
480
|
+
<>
|
|
481
|
+
<Ionicons name="cloud-upload" size={18} color="#FFFFFF" />
|
|
482
|
+
<Text className="text-white text-[15px] font-semibold">
|
|
483
|
+
{t('fileManagement.uploadPhoto')}
|
|
484
|
+
</Text>
|
|
485
|
+
</>
|
|
486
|
+
)}
|
|
487
|
+
</TouchableOpacity>
|
|
488
|
+
)}
|
|
450
489
|
</View>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
)}
|
|
476
|
-
|
|
477
|
-
)}
|
|
478
|
-
</View>
|
|
479
|
-
) : (
|
|
480
|
-
<FlatList
|
|
481
|
-
key={`cols-${columns}`}
|
|
482
|
-
data={photos}
|
|
483
|
-
renderItem={renderItem}
|
|
484
|
-
keyExtractor={keyExtractor}
|
|
485
|
-
numColumns={columns}
|
|
486
|
-
contentContainerStyle={[
|
|
487
|
-
photoPickerStyles.gridContent,
|
|
488
|
-
{ paddingTop: contentPaddingTop },
|
|
489
|
-
]}
|
|
490
|
-
style={photoPickerStyles.grid}
|
|
491
|
-
showsVerticalScrollIndicator={false}
|
|
492
|
-
refreshControl={
|
|
493
|
-
<RefreshControl
|
|
494
|
-
refreshing={refreshing}
|
|
495
|
-
onRefresh={onRefresh}
|
|
496
|
-
tintColor="#FFFFFF"
|
|
497
|
-
colors={[primaryColor]}
|
|
498
|
-
progressViewOffset={contentPaddingTop}
|
|
490
|
+
) : (
|
|
491
|
+
gridReady && (
|
|
492
|
+
<FlatList
|
|
493
|
+
key={`cols-${columns}`}
|
|
494
|
+
data={photos}
|
|
495
|
+
renderItem={renderItem}
|
|
496
|
+
keyExtractor={keyExtractor}
|
|
497
|
+
numColumns={columns}
|
|
498
|
+
className="flex-1"
|
|
499
|
+
contentContainerClassName="pt-[88px] pb-space-24"
|
|
500
|
+
showsVerticalScrollIndicator={false}
|
|
501
|
+
refreshControl={
|
|
502
|
+
<RefreshControl
|
|
503
|
+
refreshing={refreshing}
|
|
504
|
+
onRefresh={onRefresh}
|
|
505
|
+
tintColor="#FFFFFF"
|
|
506
|
+
colors={[primaryColor]}
|
|
507
|
+
progressViewOffset={GRID_CONTENT_PADDING_TOP}
|
|
508
|
+
/>
|
|
509
|
+
}
|
|
510
|
+
onEndReached={handleEndReached}
|
|
511
|
+
onEndReachedThreshold={0.4}
|
|
512
|
+
ListFooterComponent={listFooter}
|
|
513
|
+
removeClippedSubviews
|
|
514
|
+
initialNumToRender={Math.max(12, columns * 6)}
|
|
515
|
+
windowSize={9}
|
|
499
516
|
/>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
onEndReachedThreshold={0.4}
|
|
503
|
-
ListFooterComponent={listFooter}
|
|
504
|
-
removeClippedSubviews
|
|
505
|
-
initialNumToRender={Math.max(12, columns * 6)}
|
|
506
|
-
windowSize={9}
|
|
507
|
-
/>
|
|
508
|
-
)}
|
|
517
|
+
)
|
|
518
|
+
)}
|
|
509
519
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
{ paddingTop: HANDLE_ZONE, minHeight: headerHeight },
|
|
518
|
-
]}
|
|
519
|
-
>
|
|
520
|
-
<View style={photoPickerStyles.headerRow}>
|
|
521
|
-
<View style={[photoPickerStyles.headerSide, photoPickerStyles.headerSideLeft]}>
|
|
522
|
-
<TouchableOpacity
|
|
523
|
-
onPress={onCancel}
|
|
524
|
-
style={photoPickerStyles.headerCancel}
|
|
525
|
-
accessibilityRole="button"
|
|
526
|
-
accessibilityLabel={t('fileManagement.a11y.cancelPicker')}
|
|
527
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
528
|
-
>
|
|
529
|
-
<Text style={photoPickerStyles.headerCancelText}>
|
|
530
|
-
{t('fileManagement.cancel')}
|
|
531
|
-
</Text>
|
|
532
|
-
</TouchableOpacity>
|
|
533
|
-
</View>
|
|
534
|
-
<View pointerEvents="none">
|
|
535
|
-
<Text style={photoPickerStyles.headerTitle} numberOfLines={1}>
|
|
536
|
-
{t('fileManagement.choosePhoto')}
|
|
537
|
-
</Text>
|
|
538
|
-
</View>
|
|
539
|
-
<View style={[photoPickerStyles.headerSide, photoPickerStyles.headerSideRight]}>
|
|
540
|
-
{multiSelect ? (
|
|
520
|
+
{/* Translucent black header. The bottom sheet already sits below
|
|
521
|
+
the status bar, so we do NOT add `insets.top` — that would
|
|
522
|
+
double-pad. `pt-[28px]` clears the 28dp drag handle floating
|
|
523
|
+
at the top of the sheet; `min-h-[84px]` = handle + app bar. */}
|
|
524
|
+
<View className="absolute top-0 left-0 right-0 flex-row items-center justify-between px-space-12 z-30 bg-[#000000EB] pt-[28px] min-h-[84px]">
|
|
525
|
+
<View className="flex-row items-center justify-between w-full h-14">
|
|
526
|
+
<View className="basis-0 grow flex-row items-center justify-start">
|
|
541
527
|
<TouchableOpacity
|
|
542
|
-
onPress={
|
|
543
|
-
|
|
544
|
-
style={[
|
|
545
|
-
photoPickerStyles.headerPrimaryPill,
|
|
546
|
-
{
|
|
547
|
-
backgroundColor: confirmDisabled
|
|
548
|
-
? 'rgba(255,255,255,0.18)'
|
|
549
|
-
: primaryColor,
|
|
550
|
-
opacity: confirmDisabled ? 0.6 : 1,
|
|
551
|
-
},
|
|
552
|
-
]}
|
|
528
|
+
onPress={onCancel}
|
|
529
|
+
className="px-1.5 py-space-8 min-h-9 min-w-11 justify-center"
|
|
553
530
|
accessibilityRole="button"
|
|
554
|
-
accessibilityLabel={t('fileManagement.a11y.
|
|
555
|
-
|
|
531
|
+
accessibilityLabel={t('fileManagement.a11y.cancelPicker')}
|
|
532
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
556
533
|
>
|
|
557
|
-
<Text
|
|
558
|
-
{
|
|
534
|
+
<Text className="text-white text-[17px] font-medium">
|
|
535
|
+
{t('fileManagement.cancel')}
|
|
559
536
|
</Text>
|
|
560
537
|
</TouchableOpacity>
|
|
561
|
-
|
|
562
|
-
|
|
538
|
+
</View>
|
|
539
|
+
<View pointerEvents="none">
|
|
540
|
+
<Text
|
|
541
|
+
className="text-white text-[17px] font-semibold text-center tracking-[-0.2px]"
|
|
542
|
+
numberOfLines={1}
|
|
543
|
+
>
|
|
544
|
+
{t('fileManagement.choosePhoto')}
|
|
545
|
+
</Text>
|
|
546
|
+
</View>
|
|
547
|
+
<View className="basis-0 grow flex-row items-center justify-end">
|
|
548
|
+
{multiSelect ? (
|
|
563
549
|
<TouchableOpacity
|
|
564
|
-
onPress={
|
|
565
|
-
disabled={
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
550
|
+
onPress={handleConfirm}
|
|
551
|
+
disabled={confirmDisabled}
|
|
552
|
+
className="flex-row items-center gap-1.5 px-3.5 py-space-8 rounded-full min-h-9"
|
|
553
|
+
style={{
|
|
554
|
+
backgroundColor: confirmDisabled
|
|
555
|
+
? 'rgba(255,255,255,0.18)'
|
|
556
|
+
: primaryColor,
|
|
557
|
+
opacity: confirmDisabled ? 0.6 : 1,
|
|
558
|
+
}}
|
|
571
559
|
accessibilityRole="button"
|
|
572
|
-
accessibilityLabel={t('fileManagement.a11y.
|
|
573
|
-
accessibilityState={{
|
|
560
|
+
accessibilityLabel={t('fileManagement.a11y.confirmSelection')}
|
|
561
|
+
accessibilityState={{ disabled: confirmDisabled }}
|
|
574
562
|
>
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
<>
|
|
579
|
-
<Ionicons name="cloud-upload" size={16} color="#FFFFFF" />
|
|
580
|
-
{showUploadLabel && (
|
|
581
|
-
<Text style={photoPickerStyles.headerPrimaryText}>
|
|
582
|
-
{t('fileManagement.upload')}
|
|
583
|
-
</Text>
|
|
584
|
-
)}
|
|
585
|
-
</>
|
|
586
|
-
)}
|
|
563
|
+
<Text className="text-white text-[15px] font-semibold">
|
|
564
|
+
{confirmLabel}
|
|
565
|
+
</Text>
|
|
587
566
|
</TouchableOpacity>
|
|
588
|
-
)
|
|
589
|
-
|
|
567
|
+
) : (
|
|
568
|
+
isOwner && allowUpload && (
|
|
569
|
+
<TouchableOpacity
|
|
570
|
+
onPress={onUpload}
|
|
571
|
+
disabled={uploading || isPickingDocument}
|
|
572
|
+
className={`flex-row items-center gap-1.5 py-space-8 rounded-full min-h-9${showUploadLabel ? ' px-3.5' : ' px-space-8 w-9 justify-center'}`}
|
|
573
|
+
style={{ backgroundColor: primaryColor }}
|
|
574
|
+
accessibilityRole="button"
|
|
575
|
+
accessibilityLabel={t('fileManagement.a11y.uploadFromDevice')}
|
|
576
|
+
accessibilityState={{ busy: uploading || isPickingDocument }}
|
|
577
|
+
>
|
|
578
|
+
{(uploading || isPickingDocument) ? (
|
|
579
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
580
|
+
) : (
|
|
581
|
+
<>
|
|
582
|
+
<Ionicons name="cloud-upload" size={16} color="#FFFFFF" />
|
|
583
|
+
{showUploadLabel && (
|
|
584
|
+
<Text className="text-white text-[15px] font-semibold">
|
|
585
|
+
{t('fileManagement.upload')}
|
|
586
|
+
</Text>
|
|
587
|
+
)}
|
|
588
|
+
</>
|
|
589
|
+
)}
|
|
590
|
+
</TouchableOpacity>
|
|
591
|
+
)
|
|
592
|
+
)}
|
|
593
|
+
</View>
|
|
590
594
|
</View>
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
photoPickerStyles.headerProgressBarFill,
|
|
598
|
-
{
|
|
595
|
+
{/* Subtle top progress bar during upload (non-blocking). */}
|
|
596
|
+
{uploading && (
|
|
597
|
+
<View className="absolute left-0 right-0 bottom-0 h-0.5 bg-[#FFFFFF1F]">
|
|
598
|
+
<View
|
|
599
|
+
className="h-full"
|
|
600
|
+
style={{
|
|
599
601
|
width: `${Math.round(progressFraction * 100)}%`,
|
|
600
602
|
backgroundColor: primaryColor,
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
603
|
+
}}
|
|
604
|
+
/>
|
|
605
|
+
</View>
|
|
606
|
+
)}
|
|
607
|
+
</View>
|
|
606
608
|
</View>
|
|
607
609
|
</View>
|
|
608
610
|
);
|
|
@@ -16,6 +16,17 @@ import {
|
|
|
16
16
|
loadDocumentPicker,
|
|
17
17
|
} from '../shared';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Optimistic-entry metadata. `uploading` flags the placeholder so no asset URL
|
|
21
|
+
* is ever built from its client-minted `temp-…` id; `localPreviewUri` lets the
|
|
22
|
+
* grid render the locally-picked image while the real upload is in flight (a
|
|
23
|
+
* bare guard would leave an empty tile). See `useResolvedFileUrls`.
|
|
24
|
+
*/
|
|
25
|
+
type OptimisticFileMetadata = NonNullable<FileMetadata['metadata']> & {
|
|
26
|
+
uploading?: boolean;
|
|
27
|
+
localPreviewUri?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
19
30
|
/** Dependencies threaded into the upload-state hook from the orchestrator. */
|
|
20
31
|
export interface UseFileUploadStateParams {
|
|
21
32
|
targetUserId?: string;
|
|
@@ -121,6 +132,15 @@ export const useFileUploadState = ({
|
|
|
121
132
|
continue;
|
|
122
133
|
}
|
|
123
134
|
|
|
135
|
+
// Preserve the locally-picked uri so the optimistic tile
|
|
136
|
+
// shows the real image (not an empty placeholder) while the
|
|
137
|
+
// upload is in flight. Never an asset URL — the id is a
|
|
138
|
+
// client-minted `temp-…`.
|
|
139
|
+
const previewUri = candidateUri(raw);
|
|
140
|
+
const optimisticMetadata: OptimisticFileMetadata = {
|
|
141
|
+
uploading: true,
|
|
142
|
+
...(previewUri ? { localPreviewUri: previewUri } : {}),
|
|
143
|
+
};
|
|
124
144
|
const optimisticFile: FileMetadata = {
|
|
125
145
|
id: optimisticId,
|
|
126
146
|
filename: fileName,
|
|
@@ -128,7 +148,7 @@ export const useFileUploadState = ({
|
|
|
128
148
|
length: fileSize,
|
|
129
149
|
chunkSize: 0,
|
|
130
150
|
uploadDate: new Date().toISOString(),
|
|
131
|
-
metadata:
|
|
151
|
+
metadata: optimisticMetadata,
|
|
132
152
|
variants: [],
|
|
133
153
|
};
|
|
134
154
|
useFileStore.getState().addFile(optimisticFile, { prepend: true });
|
|
@@ -158,8 +178,12 @@ export const useFileUploadState = ({
|
|
|
158
178
|
uploadedFiles.push(merged);
|
|
159
179
|
successCount++;
|
|
160
180
|
} else {
|
|
161
|
-
//
|
|
162
|
-
|
|
181
|
+
// Upload succeeded but the API returned no file payload,
|
|
182
|
+
// so we cannot build a real entry. Drop the optimistic
|
|
183
|
+
// placeholder instead of stranding a `temp-…` id (which
|
|
184
|
+
// would leak into asset URLs); the scheduled silent
|
|
185
|
+
// reconcile below re-fetches the persisted record.
|
|
186
|
+
useFileStore.getState().removeFile(optimisticId);
|
|
163
187
|
if (__DEV__) {
|
|
164
188
|
console.warn('Upload completed but no file data returned:', { fileName, result });
|
|
165
189
|
}
|