@oxyhq/services 22.11.0 → 22.12.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/ui/components/OxyAuthChooser.js +1 -1
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js +7 -5
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaceRegistry.js +11 -37
- package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
- package/lib/commonjs/ui/screens/AvatarCropScreen.js +9 -13
- package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +55 -13
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PaymentGatewayScreen.js +11 -0
- package/lib/commonjs/ui/screens/PaymentGatewayScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js +91 -117
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +22 -54
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +64 -155
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/module/ui/hooks/useSurfaceHeader.js +7 -5
- package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/module/ui/navigation/surfaceRegistry.js +11 -37
- package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
- package/lib/module/ui/screens/AvatarCropScreen.js +9 -13
- package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +55 -13
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/PaymentGatewayScreen.js +11 -0
- package/lib/module/ui/screens/PaymentGatewayScreen.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +93 -119
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js +23 -54
- package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +64 -155
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +7 -5
- package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +8 -9
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
- package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +7 -5
- package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +8 -9
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
- package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/ui/components/OxyAuthChooser.tsx +1 -1
- package/src/ui/hooks/useSurfaceHeader.ts +25 -5
- package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +16 -2
- package/src/ui/navigation/surfaceRegistry.ts +14 -42
- package/src/ui/screens/AvatarCropScreen.tsx +14 -23
- package/src/ui/screens/FileManagementScreen.tsx +69 -25
- package/src/ui/screens/PaymentGatewayScreen.tsx +11 -0
- package/src/ui/screens/ProfileScreen.tsx +96 -111
- package/src/ui/screens/WelcomeNewUserScreen.tsx +23 -63
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +68 -172
|
@@ -25,13 +25,10 @@ import FileLibraryError from './FileLibraryError';
|
|
|
25
25
|
* import. Matches the pattern used by AvatarCropScreen.
|
|
26
26
|
*/
|
|
27
27
|
type HapticImpact = 'light' | 'medium' | 'heavy';
|
|
28
|
-
type HapticNotification = 'success' | 'warning' | 'error';
|
|
29
28
|
interface HapticsModule {
|
|
30
29
|
impactAsync: (style: unknown) => Promise<void>;
|
|
31
|
-
notificationAsync: (type: unknown) => Promise<void>;
|
|
32
30
|
selectionAsync: () => Promise<void>;
|
|
33
31
|
ImpactFeedbackStyle: { Light: unknown; Medium: unknown; Heavy: unknown };
|
|
34
|
-
NotificationFeedbackType: { Success: unknown; Warning: unknown; Error: unknown };
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
let hapticsModulePromise: Promise<HapticsModule | null> | null = null;
|
|
@@ -76,35 +73,27 @@ const hapticSelection = async (): Promise<void> => {
|
|
|
76
73
|
}
|
|
77
74
|
};
|
|
78
75
|
|
|
79
|
-
const hapticNotification = async (type: HapticNotification): Promise<void> => {
|
|
80
|
-
const h = await getHaptics();
|
|
81
|
-
if (!h) return;
|
|
82
|
-
const typeEnum =
|
|
83
|
-
type === 'error'
|
|
84
|
-
? h.NotificationFeedbackType.Error
|
|
85
|
-
: type === 'warning'
|
|
86
|
-
? h.NotificationFeedbackType.Warning
|
|
87
|
-
: h.NotificationFeedbackType.Success;
|
|
88
|
-
try {
|
|
89
|
-
await h.notificationAsync(typeEnum);
|
|
90
|
-
} catch {
|
|
91
|
-
// Silent.
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
76
|
/**
|
|
96
|
-
* The picker renders inside a Bloom `<Dialog>` (bottom-sheet on
|
|
97
|
-
* card on `md+`). The Dialog owns
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
77
|
+
* The picker renders inside a Bloom `<Dialog>` in NAV-HEADER mode (bottom-sheet on
|
|
78
|
+
* narrow, centered card on `md+`). The Dialog owns the shared gradient nav bar
|
|
79
|
+
* (Cancel / "Choose Photo" / Upload, wired by `FileManagementScreen` through
|
|
80
|
+
* `useSurfaceHeader`) — the picker no longer paints a bar of its own. In this mode
|
|
81
|
+
* the Dialog reserves a fixed nav-bar-height SPACER above the picker's content and
|
|
82
|
+
* its gradient overlay fades a little further into the content; the grid's own
|
|
83
|
+
* top padding just clears that fade so the first row reads cleanly. Two fixed
|
|
84
|
+
* layout constants mirror Bloom internals:
|
|
85
|
+
* - `DIALOG_NAV_BAR_SPACER` mirrors Bloom's `DIALOG_NAV_BAR_HEIGHT` (not exported
|
|
86
|
+
* from `@oxyhq/bloom/dialog`) — the spacer the Dialog inserts above header-mode
|
|
87
|
+
* `scrollable:false` content. The picker's own height is reduced by it so the
|
|
88
|
+
* spacer + grid together fill the `frameSize` morph target (and never overflow
|
|
89
|
+
* the panel's `maxHeightRatio: 0.9` cap, which would clip the last row).
|
|
90
|
+
* - `HEADER_GRADIENT_FADE` is the gradient tail that bleeds past the bar into the
|
|
91
|
+
* content; the grid's `pt-[24px]` (fade + 4dp breathing room) clears it. The
|
|
92
|
+
* `GRID_CONTENT_PADDING_TOP` constant MUST stay in sync with that class.
|
|
104
93
|
*/
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const GRID_CONTENT_PADDING_TOP =
|
|
94
|
+
const DIALOG_NAV_BAR_SPACER = 52;
|
|
95
|
+
const HEADER_GRADIENT_FADE = 20;
|
|
96
|
+
const GRID_CONTENT_PADDING_TOP = HEADER_GRADIENT_FADE + 4; // 24 — matches `pt-[24px]`
|
|
108
97
|
|
|
109
98
|
/**
|
|
110
99
|
* Props for the dedicated photo picker view. Used by FileManagementScreen
|
|
@@ -120,7 +109,6 @@ export interface PhotoPickerViewProps {
|
|
|
120
109
|
refreshing: boolean;
|
|
121
110
|
uploading: boolean;
|
|
122
111
|
isPickingDocument: boolean;
|
|
123
|
-
uploadProgress: { current: number; total: number } | null;
|
|
124
112
|
hasMore: boolean;
|
|
125
113
|
loadingMore: boolean;
|
|
126
114
|
/**
|
|
@@ -143,8 +131,6 @@ export interface PhotoPickerViewProps {
|
|
|
143
131
|
onUpload: () => void;
|
|
144
132
|
onRefresh: () => void;
|
|
145
133
|
onLoadMore: () => void;
|
|
146
|
-
onCancel: () => void;
|
|
147
|
-
onConfirm: () => void;
|
|
148
134
|
t: (key: string, vars?: Record<string, string | number>) => string;
|
|
149
135
|
}
|
|
150
136
|
|
|
@@ -224,9 +210,20 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
|
|
|
224
210
|
// Entrance fade, staggered by the cell's grid index. Started from the wrapper's
|
|
225
211
|
// ref callback (fires on mount) rather than an effect. Reduce-motion pins
|
|
226
212
|
// opacity at 1 and never animates.
|
|
213
|
+
//
|
|
214
|
+
// CRITICAL: `Animated.View` rebuilds its merged ref on every render, so React
|
|
215
|
+
// re-invokes this callback on EVERY commit (not just mount) — and re-running
|
|
216
|
+
// `Animated.timing().start()`/`setValue` each time drives a web re-render that
|
|
217
|
+
// re-fires the ref → "Maximum update depth exceeded" once the grid has cells.
|
|
218
|
+
// The `startedRef` latch makes it genuinely run-once per mount, breaking the
|
|
219
|
+
// loop. (jest/react-test-renderer never exercises the Animated merged-ref path,
|
|
220
|
+
// so this only reproduces in a real browser.)
|
|
227
221
|
const opacity = useRef(new Animated.Value(reduceMotion ? 1 : 0)).current;
|
|
222
|
+
const startedRef = useRef(false);
|
|
228
223
|
const startEntrance = useCallback((node: unknown) => {
|
|
229
224
|
if (!node) return; // unmount
|
|
225
|
+
if (startedRef.current) return; // ref re-fired on a later commit — already ran
|
|
226
|
+
startedRef.current = true;
|
|
230
227
|
if (reduceMotion) {
|
|
231
228
|
opacity.setValue(1);
|
|
232
229
|
return;
|
|
@@ -243,10 +240,18 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
|
|
|
243
240
|
// Selection-ring pulse: a quick scale bump that springs back to rest. The ring
|
|
244
241
|
// renders ONLY while selected, so its own mount IS the "became selected"
|
|
245
242
|
// signal — driven from the ring's ref callback, not an effect. Reduce-motion
|
|
246
|
-
// holds the ring at rest scale.
|
|
243
|
+
// holds the ring at rest scale. Same run-once latch as the entrance (the ring's
|
|
244
|
+
// `Animated.View` re-fires this ref every commit too) — reset on unmount so a
|
|
245
|
+
// later re-selection pulses again.
|
|
247
246
|
const ringScale = useRef(new Animated.Value(1)).current;
|
|
247
|
+
const pulsedRef = useRef(false);
|
|
248
248
|
const startRingPulse = useCallback((node: unknown) => {
|
|
249
|
-
if (!node)
|
|
249
|
+
if (!node) {
|
|
250
|
+
pulsedRef.current = false; // ring unmounted (deselected) — arm the next pulse
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (pulsedRef.current) return; // ref re-fired on a later commit — already pulsed
|
|
254
|
+
pulsedRef.current = true;
|
|
250
255
|
ringScale.setValue(1);
|
|
251
256
|
if (reduceMotion) return;
|
|
252
257
|
Animated.sequence([
|
|
@@ -343,7 +348,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
343
348
|
refreshing,
|
|
344
349
|
uploading,
|
|
345
350
|
isPickingDocument,
|
|
346
|
-
uploadProgress,
|
|
347
351
|
hasMore,
|
|
348
352
|
loadingMore,
|
|
349
353
|
loading,
|
|
@@ -358,8 +362,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
358
362
|
onUpload,
|
|
359
363
|
onRefresh,
|
|
360
364
|
onLoadMore,
|
|
361
|
-
onCancel,
|
|
362
|
-
onConfirm,
|
|
363
365
|
t,
|
|
364
366
|
}) => {
|
|
365
367
|
// Grid sizing width. We PREFER the measured grid-container width (the
|
|
@@ -381,12 +383,18 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
381
383
|
|
|
382
384
|
// The picker FILLS its panel: the surface morphs the container UP to an
|
|
383
385
|
// explicit large target (`frameSize.heightRatio: 0.9` in `surfaceRegistry`),
|
|
384
|
-
// and this root takes that SAME 0.9-of-viewport height
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
386
|
+
// and this root takes that SAME 0.9-of-viewport height MINUS the Dialog's
|
|
387
|
+
// nav-bar spacer (which the Dialog inserts ABOVE this content in header mode).
|
|
388
|
+
// spacer + root then exactly fill the morph target and stay within the panel's
|
|
389
|
+
// `maxHeightRatio: 0.9` cap — without the subtraction the grid's last row would
|
|
390
|
+
// clip. A `flex: 1` root can't fill (no definite bound propagates through the
|
|
391
|
+
// sheet's flex chain at rest, where the panel is height:auto), so the fixed
|
|
392
|
+
// height is required; the FlatList scrolls within it. Keep the 0.9 ratio in
|
|
393
|
+
// sync with the picker's `frameSize` in `surfaceRegistry.ts`.
|
|
394
|
+
const panelHeight = useMemo(
|
|
395
|
+
() => Math.round(windowHeight * 0.9) - DIALOG_NAV_BAR_SPACER,
|
|
396
|
+
[windowHeight],
|
|
397
|
+
);
|
|
390
398
|
const rootStyle = useMemo(() => ({ height: panelHeight }), [panelHeight]);
|
|
391
399
|
|
|
392
400
|
const effectiveWidth = gridWidth > 0 ? gridWidth : windowWidth;
|
|
@@ -421,9 +429,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
421
429
|
|
|
422
430
|
const hasAnySelection = selectedIds.size > 0;
|
|
423
431
|
|
|
424
|
-
// Compact icon-only upload pill on narrow sheets; full pill otherwise.
|
|
425
|
-
const showUploadLabel = effectiveWidth >= 360;
|
|
426
|
-
|
|
427
432
|
const isEmpty = photos.length === 0;
|
|
428
433
|
const atSelectionLimit =
|
|
429
434
|
multiSelect && maxSelection != null && selectedIds.size >= maxSelection;
|
|
@@ -448,16 +453,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
448
453
|
[onPreviewPhoto],
|
|
449
454
|
);
|
|
450
455
|
|
|
451
|
-
const handleConfirm = useCallback(() => {
|
|
452
|
-
if (multiSelect) {
|
|
453
|
-
if (selectedIds.size === 0) return;
|
|
454
|
-
if (Platform.OS !== 'web') {
|
|
455
|
-
void hapticNotification('success');
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
onConfirm();
|
|
459
|
-
}, [multiSelect, selectedIds.size, onConfirm]);
|
|
460
|
-
|
|
461
456
|
// FlatList renderItem: each cell knows its enterIndex (for stagger).
|
|
462
457
|
const renderItem = useCallback(
|
|
463
458
|
({ item, index }: { item: FileMetadata; index: number }) => {
|
|
@@ -518,16 +513,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
518
513
|
onLoadMore();
|
|
519
514
|
}, [loadingMore, hasMore, onLoadMore]);
|
|
520
515
|
|
|
521
|
-
const confirmDisabled = multiSelect && selectedIds.size === 0;
|
|
522
|
-
const confirmLabel = multiSelect
|
|
523
|
-
? t('fileManagement.doneWithCount', { count: selectedIds.size })
|
|
524
|
-
: t('fileManagement.done');
|
|
525
|
-
|
|
526
|
-
// The progress fill width. Guard against zero division.
|
|
527
|
-
const progressFraction = uploadProgress && uploadProgress.total > 0
|
|
528
|
-
? Math.min(1, Math.max(0, uploadProgress.current / uploadProgress.total))
|
|
529
|
-
: 0;
|
|
530
|
-
|
|
531
516
|
return (
|
|
532
517
|
// Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
|
|
533
518
|
// `rootStyle` is placement-aware — a `maxHeight` cap that gives the
|
|
@@ -540,12 +525,15 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
540
525
|
root (the CSS flexbox `min-height:auto` trap; a no-op on native,
|
|
541
526
|
where Yoga already defaults min-height to 0). */}
|
|
542
527
|
<View className="flex-1 min-h-0 bg-black">
|
|
543
|
-
{/* Photo grid
|
|
528
|
+
{/* Photo grid — full-bleed black canvas CONTENT under the shared
|
|
529
|
+
Dialog nav bar (the Dialog reserves the bar's height above this
|
|
530
|
+
and its gradient fades a little further down; the top padding
|
|
531
|
+
clears that fade). */}
|
|
544
532
|
{isEmpty && loadError ? (
|
|
545
533
|
/* Terminal load failure — a DISTINCT error surface (not the
|
|
546
|
-
empty state),
|
|
547
|
-
|
|
548
|
-
<View className="flex-1 items-center justify-center pt-[
|
|
534
|
+
empty state), cleared below the nav bar's gradient fade.
|
|
535
|
+
Retry re-runs the list query. */
|
|
536
|
+
<View className="flex-1 items-center justify-center pt-[24px]">
|
|
549
537
|
<FileLibraryError
|
|
550
538
|
title={t('fileManagement.loadError.title')}
|
|
551
539
|
description={t('fileManagement.loadError.description')}
|
|
@@ -560,14 +548,13 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
560
548
|
) : isEmpty && loading ? (
|
|
561
549
|
/* Loading skeleton — the picker's OWN shape: shimmer tiles laid
|
|
562
550
|
out with the SAME placement-aware geometry the real grid uses
|
|
563
|
-
(`computePhotoGridLayout(effectiveWidth)`),
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
still floats on top, so Cancel stays reachable while loading. */
|
|
551
|
+
(`computePhotoGridLayout(effectiveWidth)`), cleared below the
|
|
552
|
+
nav bar's gradient fade. `flexWrap` breaks rows exactly at
|
|
553
|
+
`columns` because the last tile in each row carries no right
|
|
554
|
+
margin (a full row + gutters fits the measured width by
|
|
555
|
+
construction). */
|
|
569
556
|
<View
|
|
570
|
-
className="pt-[
|
|
557
|
+
className="pt-[24px]"
|
|
571
558
|
style={{ flexDirection: 'row', flexWrap: 'wrap' }}
|
|
572
559
|
pointerEvents="none"
|
|
573
560
|
accessibilityElementsHidden
|
|
@@ -575,7 +562,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
575
562
|
>
|
|
576
563
|
{Array.from({ length: skeletonTileCount }, (_, index) => (
|
|
577
564
|
<Skeleton.Box
|
|
578
|
-
key={index}
|
|
565
|
+
key={`skeleton-${index}`}
|
|
579
566
|
width={cellSize}
|
|
580
567
|
height={cellSize}
|
|
581
568
|
borderRadius={CELL_CORNER_RADIUS}
|
|
@@ -588,7 +575,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
588
575
|
))}
|
|
589
576
|
</View>
|
|
590
577
|
) : isEmpty ? (
|
|
591
|
-
<View className="flex-1 items-center justify-center px-space-32 pt-[
|
|
578
|
+
<View className="flex-1 items-center justify-center px-space-32 pt-[24px]">
|
|
592
579
|
<View className="opacity-30 mb-space-16">
|
|
593
580
|
<MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
|
|
594
581
|
</View>
|
|
@@ -629,7 +616,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
629
616
|
keyExtractor={keyExtractor}
|
|
630
617
|
numColumns={columns}
|
|
631
618
|
className="flex-1 min-h-0"
|
|
632
|
-
contentContainerClassName="pt-[
|
|
619
|
+
contentContainerClassName="pt-[24px] pb-space-24"
|
|
633
620
|
showsVerticalScrollIndicator={false}
|
|
634
621
|
refreshControl={
|
|
635
622
|
<RefreshControl
|
|
@@ -648,97 +635,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
648
635
|
windowSize={9}
|
|
649
636
|
/>
|
|
650
637
|
)}
|
|
651
|
-
|
|
652
|
-
{/* Translucent black header floating over the grid. The Dialog owns
|
|
653
|
-
the drag handle (a floating pill above this bar in bottom
|
|
654
|
-
placement, drawn at a higher z-index; none in center), so the
|
|
655
|
-
header no longer reserves the 28dp handle strip — it is just the
|
|
656
|
-
56dp app bar (`min-h-[56px]`). */}
|
|
657
|
-
<View className="absolute top-0 left-0 right-0 flex-row items-center justify-between px-space-12 z-30 bg-[#000000EB] min-h-[56px]">
|
|
658
|
-
<View className="flex-row items-center justify-between w-full h-14">
|
|
659
|
-
<View className="basis-0 grow flex-row items-center justify-start">
|
|
660
|
-
<TouchableOpacity
|
|
661
|
-
onPress={onCancel}
|
|
662
|
-
className="px-1.5 py-space-8 min-h-9 min-w-11 justify-center"
|
|
663
|
-
accessibilityRole="button"
|
|
664
|
-
accessibilityLabel={t('fileManagement.a11y.cancelPicker')}
|
|
665
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
666
|
-
>
|
|
667
|
-
<Text className="text-white text-[17px] font-medium">
|
|
668
|
-
{t('fileManagement.cancel')}
|
|
669
|
-
</Text>
|
|
670
|
-
</TouchableOpacity>
|
|
671
|
-
</View>
|
|
672
|
-
<View pointerEvents="none">
|
|
673
|
-
<Text
|
|
674
|
-
className="text-white text-[17px] font-semibold text-center"
|
|
675
|
-
style={{ letterSpacing: -0.2 }}
|
|
676
|
-
numberOfLines={1}
|
|
677
|
-
>
|
|
678
|
-
{t('fileManagement.choosePhoto')}
|
|
679
|
-
</Text>
|
|
680
|
-
</View>
|
|
681
|
-
<View className="basis-0 grow flex-row items-center justify-end">
|
|
682
|
-
{multiSelect ? (
|
|
683
|
-
<TouchableOpacity
|
|
684
|
-
onPress={handleConfirm}
|
|
685
|
-
disabled={confirmDisabled}
|
|
686
|
-
className="flex-row items-center gap-1.5 px-3.5 py-space-8 rounded-full min-h-9"
|
|
687
|
-
style={{
|
|
688
|
-
backgroundColor: confirmDisabled
|
|
689
|
-
? 'rgba(255,255,255,0.18)'
|
|
690
|
-
: primaryColor,
|
|
691
|
-
opacity: confirmDisabled ? 0.6 : 1,
|
|
692
|
-
}}
|
|
693
|
-
accessibilityRole="button"
|
|
694
|
-
accessibilityLabel={t('fileManagement.a11y.confirmSelection')}
|
|
695
|
-
accessibilityState={{ disabled: confirmDisabled }}
|
|
696
|
-
>
|
|
697
|
-
<Text className="text-white text-[15px] font-semibold">
|
|
698
|
-
{confirmLabel}
|
|
699
|
-
</Text>
|
|
700
|
-
</TouchableOpacity>
|
|
701
|
-
) : (
|
|
702
|
-
isOwner && allowUpload && (
|
|
703
|
-
<TouchableOpacity
|
|
704
|
-
onPress={onUpload}
|
|
705
|
-
disabled={uploading || isPickingDocument}
|
|
706
|
-
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'}`}
|
|
707
|
-
style={{ backgroundColor: primaryColor }}
|
|
708
|
-
accessibilityRole="button"
|
|
709
|
-
accessibilityLabel={t('fileManagement.a11y.uploadFromDevice')}
|
|
710
|
-
accessibilityState={{ busy: uploading || isPickingDocument }}
|
|
711
|
-
>
|
|
712
|
-
{(uploading || isPickingDocument) ? (
|
|
713
|
-
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
714
|
-
) : (
|
|
715
|
-
<>
|
|
716
|
-
<Ionicons name="cloud-upload" size={16} color="#FFFFFF" />
|
|
717
|
-
{showUploadLabel && (
|
|
718
|
-
<Text className="text-white text-[15px] font-semibold">
|
|
719
|
-
{t('fileManagement.upload')}
|
|
720
|
-
</Text>
|
|
721
|
-
)}
|
|
722
|
-
</>
|
|
723
|
-
)}
|
|
724
|
-
</TouchableOpacity>
|
|
725
|
-
)
|
|
726
|
-
)}
|
|
727
|
-
</View>
|
|
728
|
-
</View>
|
|
729
|
-
{/* Subtle top progress bar during upload (non-blocking). */}
|
|
730
|
-
{uploading && (
|
|
731
|
-
<View className="absolute left-0 right-0 bottom-0 h-0.5 bg-[#FFFFFF1F]">
|
|
732
|
-
<View
|
|
733
|
-
className="h-full"
|
|
734
|
-
style={{
|
|
735
|
-
width: `${Math.round(progressFraction * 100)}%`,
|
|
736
|
-
backgroundColor: primaryColor,
|
|
737
|
-
}}
|
|
738
|
-
/>
|
|
739
|
-
</View>
|
|
740
|
-
)}
|
|
741
|
-
</View>
|
|
742
638
|
</View>
|
|
743
639
|
</View>
|
|
744
640
|
);
|