@oxyhq/services 22.8.5 → 22.8.7
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/ui/components/ProfileButton.js +4 -10
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js +3 -3
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +56 -173
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +60 -3
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +5 -10
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/shared.js +13 -1
- package/lib/commonjs/ui/screens/fileManagement/shared.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +3 -10
- package/lib/module/ui/components/ProfileButton.js.map +1 -1
- package/lib/module/ui/components/fileManagement/UploadPreview.js +3 -3
- package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +57 -174
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +60 -3
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +6 -11
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/shared.js +12 -0
- package/lib/module/ui/screens/fileManagement/shared.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/fileManagement/UploadPreview.d.ts +3 -10
- package/lib/typescript/commonjs/ui/components/fileManagement/UploadPreview.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +7 -0
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/shared.d.ts +4 -0
- package/lib/typescript/commonjs/ui/screens/fileManagement/shared.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/fileManagement/UploadPreview.d.ts +3 -10
- package/lib/typescript/module/ui/components/fileManagement/UploadPreview.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 +7 -0
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/shared.d.ts +4 -0
- package/lib/typescript/module/ui/screens/fileManagement/shared.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ui/components/ProfileButton.tsx +2 -14
- package/src/ui/components/fileManagement/UploadPreview.tsx +8 -16
- package/src/ui/screens/FileManagementScreen.tsx +43 -174
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +69 -7
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +7 -11
- package/src/ui/screens/fileManagement/shared.ts +23 -0
|
@@ -9,15 +9,13 @@ import {
|
|
|
9
9
|
RefreshControl,
|
|
10
10
|
TextInput,
|
|
11
11
|
Image,
|
|
12
|
-
Animated,
|
|
13
|
-
Easing,
|
|
14
12
|
AccessibilityInfo,
|
|
15
|
-
Platform,
|
|
16
13
|
useWindowDimensions,
|
|
17
14
|
} from 'react-native';
|
|
18
15
|
import { Image as ExpoImage } from 'expo-image';
|
|
19
16
|
import type { FileManagementScreenProps } from '../types/fileManagement';
|
|
20
17
|
import { Dialog, toast, useDialogControl } from '@oxyhq/bloom';
|
|
18
|
+
import * as Skeleton from '@oxyhq/bloom/skeleton';
|
|
21
19
|
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
|
22
20
|
import type { FileMetadata } from '@oxyhq/core';
|
|
23
21
|
import { useFileStore, useFiles, useUploading as useUploadingStore, useUploadAggregateProgress, useDeleting as useDeletingStore } from '../stores/fileStore';
|
|
@@ -1121,193 +1119,63 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1121
1119
|
</View>
|
|
1122
1120
|
);
|
|
1123
1121
|
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
)
|
|
1141
|
-
shimmer.start();
|
|
1142
|
-
return () => shimmer.stop();
|
|
1143
|
-
}, [shimmerAnim]);
|
|
1144
|
-
|
|
1145
|
-
// Create a sweeping shimmer effect
|
|
1146
|
-
const shimmerTranslateX = shimmerAnim.interpolate({
|
|
1147
|
-
inputRange: [0, 1],
|
|
1148
|
-
outputRange: [-skeletonContainerWidth * 2, skeletonContainerWidth * 2],
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
const SkeletonBox = ({ width, height, borderRadius = 8, style, delay = 0 }: { width: number | string; height: number; borderRadius?: number; style?: Record<string, unknown>; delay?: number }) => {
|
|
1152
|
-
const delayedTranslateX = shimmerAnim.interpolate({
|
|
1153
|
-
inputRange: [0, 1],
|
|
1154
|
-
outputRange: [-skeletonContainerWidth * 2 + delay, skeletonContainerWidth * 2 + delay],
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
return (
|
|
1158
|
-
<View
|
|
1159
|
-
style={[
|
|
1160
|
-
{
|
|
1161
|
-
width,
|
|
1162
|
-
height,
|
|
1163
|
-
borderRadius,
|
|
1164
|
-
backgroundColor: colors.backgroundSecondary,
|
|
1165
|
-
overflow: 'hidden',
|
|
1166
|
-
position: 'relative',
|
|
1167
|
-
},
|
|
1168
|
-
style,
|
|
1169
|
-
]}
|
|
1170
|
-
>
|
|
1171
|
-
{/* Base background */}
|
|
1172
|
-
<View
|
|
1173
|
-
style={{
|
|
1174
|
-
position: 'absolute',
|
|
1175
|
-
top: 0,
|
|
1176
|
-
left: 0,
|
|
1177
|
-
right: 0,
|
|
1178
|
-
bottom: 0,
|
|
1179
|
-
backgroundColor: colors.backgroundSecondary,
|
|
1180
|
-
}}
|
|
1181
|
-
/>
|
|
1182
|
-
{/* Shimmer gradient effect */}
|
|
1183
|
-
<Animated.View
|
|
1184
|
-
style={{
|
|
1185
|
-
position: 'absolute',
|
|
1186
|
-
top: 0,
|
|
1187
|
-
left: 0,
|
|
1188
|
-
width: '100%',
|
|
1189
|
-
height: '100%',
|
|
1190
|
-
transform: [{ translateX: delayedTranslateX }],
|
|
1191
|
-
}}
|
|
1192
|
-
>
|
|
1193
|
-
<View
|
|
1194
|
-
style={{
|
|
1195
|
-
width: skeletonContainerWidth,
|
|
1196
|
-
height: '100%',
|
|
1197
|
-
backgroundColor: bloomTheme.isDark
|
|
1198
|
-
? 'rgba(255, 255, 255, 0.08)'
|
|
1199
|
-
: 'rgba(255, 255, 255, 0.8)',
|
|
1200
|
-
shadowColor: bloomTheme.isDark ? '#000' : '#FFF',
|
|
1201
|
-
shadowOffset: { width: 0, height: 0 },
|
|
1202
|
-
shadowOpacity: 0.3,
|
|
1203
|
-
shadowRadius: 10,
|
|
1204
|
-
}}
|
|
1205
|
-
/>
|
|
1206
|
-
</Animated.View>
|
|
1207
|
-
</View>
|
|
1208
|
-
);
|
|
1209
|
-
};
|
|
1210
|
-
|
|
1211
|
-
// Skeleton file item matching SettingsListItem structure
|
|
1212
|
-
const SkeletonFileItem = ({ index }: { index: number }) => (
|
|
1213
|
-
<View
|
|
1214
|
-
style={[
|
|
1215
|
-
{
|
|
1216
|
-
flexDirection: 'row',
|
|
1217
|
-
alignItems: 'center',
|
|
1218
|
-
paddingHorizontal: 16,
|
|
1219
|
-
paddingVertical: 12,
|
|
1220
|
-
backgroundColor: colors.background,
|
|
1221
|
-
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
1222
|
-
borderBottomColor: colors.border,
|
|
1223
|
-
},
|
|
1224
|
-
]}
|
|
1225
|
-
>
|
|
1226
|
-
{/* Icon/Image skeleton */}
|
|
1227
|
-
<SkeletonBox width={44} height={44} borderRadius={8} delay={index * 50} />
|
|
1228
|
-
|
|
1229
|
-
{/* Content skeleton */}
|
|
1230
|
-
<View style={{ flex: 1, marginLeft: 12, justifyContent: 'center' }}>
|
|
1231
|
-
<SkeletonBox
|
|
1232
|
-
width={index % 3 === 0 ? '85%' : index % 3 === 1 ? '70%' : '90%'}
|
|
1233
|
-
height={16}
|
|
1234
|
-
style={{ marginBottom: 8 }}
|
|
1235
|
-
delay={index * 50 + 20}
|
|
1236
|
-
/>
|
|
1237
|
-
<SkeletonBox
|
|
1238
|
-
width={index % 2 === 0 ? '50%' : '60%'}
|
|
1239
|
-
height={12}
|
|
1240
|
-
delay={index * 50 + 40}
|
|
1241
|
-
/>
|
|
1242
|
-
</View>
|
|
1243
|
-
</View>
|
|
1122
|
+
// Loading state — Bloom's shared `Skeleton` primitives own the shimmer, so
|
|
1123
|
+
// this is pure layout: the top chrome (header / controls / search) plus a
|
|
1124
|
+
// photo grid of shimmering tiles sized with the same 3-per-row geometry the
|
|
1125
|
+
// real photo grid uses. No hand-rolled `Animated` shimmer.
|
|
1126
|
+
//
|
|
1127
|
+
// The image-only picker owns its OWN loading skeleton (rendered by
|
|
1128
|
+
// `PhotoPickerView` when its photo list is still loading), so it inherits the
|
|
1129
|
+
// picker's placement-aware container width / columns / tile size and the
|
|
1130
|
+
// Dialog-vs-bottom-sheet sizing. Skipping the browse chrome here is what keeps
|
|
1131
|
+
// the picker from flashing the wrong (file-manager) skeleton inside the Dialog.
|
|
1132
|
+
if (loading && !isImageOnlyPicker) {
|
|
1133
|
+
const GRID_PADDING = 10;
|
|
1134
|
+
const TILE_GAP = 4;
|
|
1135
|
+
const GRID_COLUMNS = 3;
|
|
1136
|
+
const GRID_ROWS = 6;
|
|
1137
|
+
const tileSize = Math.floor(
|
|
1138
|
+
(safeContainerWidth - GRID_PADDING * 2 - TILE_GAP * (GRID_COLUMNS - 1)) / GRID_COLUMNS,
|
|
1244
1139
|
);
|
|
1245
1140
|
|
|
1246
1141
|
return (
|
|
1247
1142
|
<View className="flex-1" style={{ backgroundColor }}>
|
|
1248
|
-
{/* Header
|
|
1143
|
+
{/* Header */}
|
|
1249
1144
|
<View
|
|
1250
|
-
className="flex-row items-center justify-between px-[16px] py-[12px]
|
|
1145
|
+
className="flex-row items-center justify-between px-[16px] py-[12px]"
|
|
1251
1146
|
style={{ borderBottomColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth }}
|
|
1252
1147
|
>
|
|
1253
|
-
<
|
|
1254
|
-
<
|
|
1255
|
-
<
|
|
1256
|
-
<
|
|
1257
|
-
</
|
|
1258
|
-
<
|
|
1148
|
+
<Skeleton.Box width={44} height={44} borderRadius={12} />
|
|
1149
|
+
<Skeleton.Col style={{ alignItems: 'center', marginHorizontal: 16, gap: 6 }}>
|
|
1150
|
+
<Skeleton.Box width={140} height={20} />
|
|
1151
|
+
<Skeleton.Box width={100} height={14} />
|
|
1152
|
+
</Skeleton.Col>
|
|
1153
|
+
<Skeleton.Box width={44} height={44} borderRadius={12} />
|
|
1259
1154
|
</View>
|
|
1260
1155
|
|
|
1261
|
-
{/* Controls
|
|
1262
|
-
<
|
|
1263
|
-
<
|
|
1264
|
-
<
|
|
1265
|
-
</
|
|
1156
|
+
{/* Controls bar */}
|
|
1157
|
+
<Skeleton.Row style={{ alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, paddingVertical: 12 }}>
|
|
1158
|
+
<Skeleton.Box width={100} height={36} borderRadius={18} />
|
|
1159
|
+
<Skeleton.Box width={44} height={44} borderRadius={22} />
|
|
1160
|
+
</Skeleton.Row>
|
|
1266
1161
|
|
|
1267
|
-
{/* Search
|
|
1268
|
-
<View
|
|
1269
|
-
|
|
1270
|
-
style={{ backgroundColor: colors.card }}
|
|
1271
|
-
>
|
|
1272
|
-
<SkeletonBox width="100%" height={44} borderRadius={12} />
|
|
1162
|
+
{/* Search bar */}
|
|
1163
|
+
<View className="mx-[12px] mb-[12px]">
|
|
1164
|
+
<Skeleton.Box width="100%" height={44} borderRadius={22} />
|
|
1273
1165
|
</View>
|
|
1274
1166
|
|
|
1275
|
-
{/*
|
|
1276
|
-
<View
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
<SkeletonBox width={40} height={14} delay={i * 30 + 15} />
|
|
1284
|
-
</View>
|
|
1167
|
+
{/* Photo grid */}
|
|
1168
|
+
<View style={{ padding: GRID_PADDING, gap: TILE_GAP }}>
|
|
1169
|
+
{Array.from({ length: GRID_ROWS }, (_, row) => (
|
|
1170
|
+
<Skeleton.Row key={row} style={{ gap: TILE_GAP }}>
|
|
1171
|
+
{Array.from({ length: GRID_COLUMNS }, (_, col) => (
|
|
1172
|
+
<Skeleton.Box key={col} width={tileSize} height={tileSize} borderRadius={6} />
|
|
1173
|
+
))}
|
|
1174
|
+
</Skeleton.Row>
|
|
1285
1175
|
))}
|
|
1286
1176
|
</View>
|
|
1287
|
-
|
|
1288
|
-
{/* File List Skeleton - Matching SettingsListItem */}
|
|
1289
|
-
<ScrollView
|
|
1290
|
-
className="flex-1"
|
|
1291
|
-
contentContainerClassName="px-[12px] pt-0 pb-[12px]"
|
|
1292
|
-
showsVerticalScrollIndicator={false}
|
|
1293
|
-
>
|
|
1294
|
-
<View style={{
|
|
1295
|
-
backgroundColor: colors.card,
|
|
1296
|
-
borderRadius: 18,
|
|
1297
|
-
overflow: 'hidden',
|
|
1298
|
-
marginTop: 8,
|
|
1299
|
-
}}>
|
|
1300
|
-
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
|
1301
|
-
<SkeletonFileItem key={i} index={i} />
|
|
1302
|
-
))}
|
|
1303
|
-
</View>
|
|
1304
|
-
</ScrollView>
|
|
1305
1177
|
</View>
|
|
1306
1178
|
);
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
if (loading) {
|
|
1310
|
-
return <SkeletonLoader />;
|
|
1311
1179
|
}
|
|
1312
1180
|
|
|
1313
1181
|
// Dedicated flagship-style photo picker view used in the avatar-picker
|
|
@@ -1335,6 +1203,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1335
1203
|
uploadProgress={uploadProgress}
|
|
1336
1204
|
hasMore={paging.hasMore}
|
|
1337
1205
|
loadingMore={paging.loadingMore}
|
|
1206
|
+
loading={loading}
|
|
1338
1207
|
reduceMotion={reduceMotion}
|
|
1339
1208
|
getThumbUrl={thumbSourceFor}
|
|
1340
1209
|
primaryColor={colors.primary}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { Image as ExpoImage } from 'expo-image';
|
|
15
15
|
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
|
16
16
|
import { BREAKPOINTS } from '@oxyhq/bloom/styles';
|
|
17
|
+
import * as Skeleton from '@oxyhq/bloom/skeleton';
|
|
17
18
|
import type { FileMetadata } from '@oxyhq/core';
|
|
18
19
|
import { computePhotoGridLayout } from './photoGridLayout';
|
|
19
20
|
|
|
@@ -122,6 +123,13 @@ export interface PhotoPickerViewProps {
|
|
|
122
123
|
uploadProgress: { current: number; total: number } | null;
|
|
123
124
|
hasMore: boolean;
|
|
124
125
|
loadingMore: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Initial-load flag. While `true` AND no photos have arrived yet, the picker
|
|
128
|
+
* renders its OWN loading skeleton (placement-aware grid of shimmer tiles)
|
|
129
|
+
* instead of the empty state — so the loading UI matches the picker's shape
|
|
130
|
+
* and container width, not the browse file-manager chrome.
|
|
131
|
+
*/
|
|
132
|
+
loading: boolean;
|
|
125
133
|
reduceMotion: boolean;
|
|
126
134
|
getThumbUrl: (file: FileMetadata) => string | undefined;
|
|
127
135
|
primaryColor: string;
|
|
@@ -161,6 +169,18 @@ const RING_SPRING_TENSION = 120;
|
|
|
161
169
|
const CELL_CORNER_RADIUS = 8; // `rounded-radius-8`
|
|
162
170
|
const SELECTION_RING_WIDTH = 3; // `border-[3px]`
|
|
163
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Loading-skeleton tile fill. The picker's backdrop is ALWAYS black regardless
|
|
174
|
+
* of theme, so we cannot use Bloom `Skeleton.Box`'s theme default (`contrast50`,
|
|
175
|
+
* which is a near-black gray on a dark theme → invisible on this backdrop). A
|
|
176
|
+
* neutral dark gray reads as a photo placeholder on black; Bloom's shimmer still
|
|
177
|
+
* pulses the tile opacity on top of it. `Skeleton.Box` applies `style` after its
|
|
178
|
+
* own base, so this override wins while the animated opacity is preserved.
|
|
179
|
+
*/
|
|
180
|
+
const SKELETON_TILE_COLOR = '#26262A';
|
|
181
|
+
/** Minimum skeleton rows so the grid never looks empty on a very short panel. */
|
|
182
|
+
const SKELETON_MIN_ROWS = 4;
|
|
183
|
+
|
|
164
184
|
type PhotoPickerCellProps = {
|
|
165
185
|
photo: FileMetadata;
|
|
166
186
|
size: number;
|
|
@@ -325,6 +345,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
325
345
|
uploadProgress,
|
|
326
346
|
hasMore,
|
|
327
347
|
loadingMore,
|
|
348
|
+
loading,
|
|
328
349
|
reduceMotion,
|
|
329
350
|
getThumbUrl,
|
|
330
351
|
primaryColor,
|
|
@@ -366,14 +387,13 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
366
387
|
// better below full height); the bottom sheet may run taller. Keep this
|
|
367
388
|
// breakpoint in sync with `DIALOG_PLACEMENT` in BottomSheetRouter.
|
|
368
389
|
const isCenteredPanel = windowWidth >= BREAKPOINTS.md;
|
|
369
|
-
const
|
|
370
|
-
() => (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
: Math.round(windowHeight * 0.9),
|
|
374
|
-
}),
|
|
390
|
+
const maxPanelHeight = useMemo(
|
|
391
|
+
() => (isCenteredPanel
|
|
392
|
+
? Math.min(Math.round(windowHeight * 0.85), 720)
|
|
393
|
+
: Math.round(windowHeight * 0.9)),
|
|
375
394
|
[isCenteredPanel, windowHeight],
|
|
376
395
|
);
|
|
396
|
+
const rootStyle = useMemo(() => ({ maxHeight: maxPanelHeight }), [maxPanelHeight]);
|
|
377
397
|
|
|
378
398
|
const effectiveWidth = gridWidth > 0 ? gridWidth : windowWidth;
|
|
379
399
|
const { columns, cellSize, gutter } = useMemo(
|
|
@@ -381,6 +401,18 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
381
401
|
[effectiveWidth],
|
|
382
402
|
);
|
|
383
403
|
|
|
404
|
+
// How many shimmer tiles the loading skeleton renders: enough FULL rows to
|
|
405
|
+
// fill the panel down to (but never past) its `maxHeight` cap, so the panel
|
|
406
|
+
// hugs the skeleton content instead of overflowing or scrolling. `floor`
|
|
407
|
+
// keeps the last row whole; `SKELETON_MIN_ROWS` guards a very short panel.
|
|
408
|
+
const skeletonTileCount = useMemo(() => {
|
|
409
|
+
const rowHeight = cellSize + gutter;
|
|
410
|
+
if (rowHeight <= 0) return columns * SKELETON_MIN_ROWS;
|
|
411
|
+
const available = Math.max(0, maxPanelHeight - GRID_CONTENT_PADDING_TOP);
|
|
412
|
+
const rows = Math.max(SKELETON_MIN_ROWS, Math.floor(available / rowHeight));
|
|
413
|
+
return rows * columns;
|
|
414
|
+
}, [cellSize, gutter, columns, maxPanelHeight]);
|
|
415
|
+
|
|
384
416
|
// Map selectedIds → 1-based selection order for the badge. We freeze a
|
|
385
417
|
// stable order at the time of selection: the order is the insertion
|
|
386
418
|
// order of the Set (which JS preserves natively).
|
|
@@ -515,7 +547,37 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
515
547
|
where Yoga already defaults min-height to 0). */}
|
|
516
548
|
<View className="flex-1 min-h-0 bg-black">
|
|
517
549
|
{/* Photo grid (renders behind the translucent header). */}
|
|
518
|
-
{isEmpty ? (
|
|
550
|
+
{isEmpty && loading ? (
|
|
551
|
+
/* Loading skeleton — the picker's OWN shape: shimmer tiles laid
|
|
552
|
+
out with the SAME placement-aware geometry the real grid uses
|
|
553
|
+
(`computePhotoGridLayout(effectiveWidth)`), pushed below the
|
|
554
|
+
translucent header by the grid content padding. `flexWrap`
|
|
555
|
+
breaks rows exactly at `columns` because the last tile in each
|
|
556
|
+
row carries no right margin (a full row + gutters fits the
|
|
557
|
+
measured width by construction). The real translucent header
|
|
558
|
+
still floats on top, so Cancel stays reachable while loading. */
|
|
559
|
+
<View
|
|
560
|
+
className="pt-[60px]"
|
|
561
|
+
style={{ flexDirection: 'row', flexWrap: 'wrap' }}
|
|
562
|
+
pointerEvents="none"
|
|
563
|
+
accessibilityElementsHidden
|
|
564
|
+
importantForAccessibility="no-hide-descendants"
|
|
565
|
+
>
|
|
566
|
+
{Array.from({ length: skeletonTileCount }, (_, index) => (
|
|
567
|
+
<Skeleton.Box
|
|
568
|
+
key={index}
|
|
569
|
+
width={cellSize}
|
|
570
|
+
height={cellSize}
|
|
571
|
+
borderRadius={CELL_CORNER_RADIUS}
|
|
572
|
+
style={{
|
|
573
|
+
marginRight: (index + 1) % columns === 0 ? 0 : gutter,
|
|
574
|
+
marginBottom: gutter,
|
|
575
|
+
backgroundColor: SKELETON_TILE_COLOR,
|
|
576
|
+
}}
|
|
577
|
+
/>
|
|
578
|
+
))}
|
|
579
|
+
</View>
|
|
580
|
+
) : isEmpty ? (
|
|
519
581
|
<View className="flex-1 items-center justify-center px-space-32 pt-[60px]">
|
|
520
582
|
<View className="opacity-30 mb-space-16">
|
|
521
583
|
<MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
|
|
@@ -6,6 +6,7 @@ import { useFileStore } from '../../../stores/fileStore';
|
|
|
6
6
|
import type { useUploadFile } from '../../../hooks/mutations/useAccountMutations';
|
|
7
7
|
import { convertDocumentPickerAssetToFile, formatFileSize } from '../../../utils/fileManagement';
|
|
8
8
|
import {
|
|
9
|
+
createPendingUploadFile,
|
|
9
10
|
type PendingUploadFile,
|
|
10
11
|
type UploadCandidate,
|
|
11
12
|
candidateName,
|
|
@@ -52,7 +53,7 @@ export interface UseFileUploadStateResult {
|
|
|
52
53
|
handleFileUpload: () => Promise<void>;
|
|
53
54
|
handleConfirmUpload: () => Promise<void>;
|
|
54
55
|
handleCancelUpload: () => void;
|
|
55
|
-
removePendingFile: (
|
|
56
|
+
removePendingFile: (id: string) => void;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
@@ -305,13 +306,7 @@ export const useFileUploadState = ({
|
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
processedFiles.push(
|
|
309
|
-
file,
|
|
310
|
-
preview,
|
|
311
|
-
size,
|
|
312
|
-
name,
|
|
313
|
-
type: fileType
|
|
314
|
-
});
|
|
309
|
+
processedFiles.push(createPendingUploadFile(file, preview, size, name, fileType));
|
|
315
310
|
}
|
|
316
311
|
|
|
317
312
|
if (processedFiles.length === 0) {
|
|
@@ -387,12 +382,13 @@ export const useFileUploadState = ({
|
|
|
387
382
|
setShowUploadPreview(false);
|
|
388
383
|
};
|
|
389
384
|
|
|
390
|
-
const removePendingFile = (
|
|
391
|
-
const file = pendingFiles
|
|
385
|
+
const removePendingFile = (id: string) => {
|
|
386
|
+
const file = pendingFiles.find((pf) => pf.id === id);
|
|
387
|
+
if (!file) return;
|
|
392
388
|
if (file.preview) {
|
|
393
389
|
URL.revokeObjectURL(file.preview);
|
|
394
390
|
}
|
|
395
|
-
const updated = pendingFiles.filter((
|
|
391
|
+
const updated = pendingFiles.filter((pf) => pf.id !== id);
|
|
396
392
|
setPendingFiles(updated);
|
|
397
393
|
if (updated.length === 0) {
|
|
398
394
|
setShowUploadPreview(false);
|
|
@@ -57,9 +57,32 @@ export const candidateUri = (candidate: UploadCandidate): string | undefined =>
|
|
|
57
57
|
|
|
58
58
|
/** A processed file ready for review in the upload preview modal. */
|
|
59
59
|
export interface PendingUploadFile {
|
|
60
|
+
/** Stable row identity for React keys and removal — survives reorder/filter. */
|
|
61
|
+
id: string;
|
|
60
62
|
file: UploadCandidate;
|
|
61
63
|
preview?: string;
|
|
62
64
|
size: number;
|
|
63
65
|
name: string;
|
|
64
66
|
type: string;
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
const createPendingUploadId = (): string =>
|
|
70
|
+
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
71
|
+
? crypto.randomUUID()
|
|
72
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
73
|
+
|
|
74
|
+
/** Mint a {@link PendingUploadFile} row from a validated upload candidate. */
|
|
75
|
+
export const createPendingUploadFile = (
|
|
76
|
+
file: UploadCandidate,
|
|
77
|
+
preview: string | undefined,
|
|
78
|
+
size: number,
|
|
79
|
+
name: string,
|
|
80
|
+
type: string,
|
|
81
|
+
): PendingUploadFile => ({
|
|
82
|
+
id: createPendingUploadId(),
|
|
83
|
+
file,
|
|
84
|
+
preview,
|
|
85
|
+
size,
|
|
86
|
+
name,
|
|
87
|
+
type,
|
|
88
|
+
});
|