@oxyhq/services 22.8.6 → 22.9.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/OxyAccountDialog.js +30 -39
- package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +9 -29
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/SurfaceScreen.js +146 -0
- package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -0
- 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/context/OxyContext.js +32 -1
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/navigation/bottomSheetManager.js +38 -56
- package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +7 -57
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaceNavStack.js +98 -0
- package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -0
- package/lib/commonjs/ui/navigation/surfaceRegistry.js +108 -0
- package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -0
- package/lib/commonjs/ui/navigation/surfaces.js +219 -0
- package/lib/commonjs/ui/navigation/surfaces.js.map +1 -0
- package/lib/commonjs/ui/screens/FileManagementScreen.js +8 -1
- 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 +14 -17
- 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/commonjs/ui/types/surfaceScreen.js +6 -0
- package/lib/commonjs/ui/types/surfaceScreen.js.map +1 -0
- package/lib/module/ui/components/OxyAccountDialog.js +31 -40
- package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +10 -30
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/SurfaceScreen.js +141 -0
- package/lib/module/ui/components/SurfaceScreen.js.map +1 -0
- 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/context/OxyContext.js +32 -1
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/navigation/bottomSheetManager.js +37 -53
- package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +6 -56
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/navigation/surfaceNavStack.js +95 -0
- package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -0
- package/lib/module/ui/navigation/surfaceRegistry.js +102 -0
- package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -0
- package/lib/module/ui/navigation/surfaces.js +207 -0
- package/lib/module/ui/navigation/surfaces.js.map +1 -0
- package/lib/module/ui/screens/FileManagementScreen.js +8 -1
- 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 +14 -18
- 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/module/ui/types/surfaceScreen.js +4 -0
- package/lib/module/ui/types/surfaceScreen.js.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +29 -27
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +3 -2
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts +24 -0
- package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -0
- 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/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +24 -16
- package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +2 -60
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +51 -0
- package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +117 -0
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts +79 -0
- package/lib/typescript/commonjs/ui/navigation/surfaces.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 +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/commonjs/ui/types/surfaceScreen.d.ts +38 -0
- package/lib/typescript/commonjs/ui/types/surfaceScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +29 -27
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts +3 -2
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SurfaceScreen.d.ts +24 -0
- package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -0
- 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/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +24 -16
- package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +2 -60
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +51 -0
- package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -0
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +117 -0
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -0
- package/lib/typescript/module/ui/navigation/surfaces.d.ts +79 -0
- package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -0
- 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/lib/typescript/module/ui/types/surfaceScreen.d.ts +38 -0
- package/lib/typescript/module/ui/types/surfaceScreen.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/ui/components/OxyAccountDialog.tsx +32 -38
- package/src/ui/components/OxyProvider.tsx +12 -34
- package/src/ui/components/SurfaceScreen.tsx +166 -0
- package/src/ui/components/fileManagement/UploadPreview.tsx +8 -16
- package/src/ui/context/OxyContext.tsx +27 -1
- package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +39 -0
- package/src/ui/navigation/bottomSheetManager.ts +46 -73
- package/src/ui/navigation/routes.ts +7 -106
- package/src/ui/navigation/surfaceNavStack.ts +110 -0
- package/src/ui/navigation/surfaceRegistry.ts +174 -0
- package/src/ui/navigation/surfaces.ts +244 -0
- package/src/ui/screens/FileManagementScreen.tsx +8 -1
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +69 -7
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +16 -18
- package/src/ui/screens/fileManagement/shared.ts +23 -0
- package/src/ui/types/surfaceScreen.ts +38 -0
- package/lib/commonjs/ui/components/BottomSheet.js +0 -49
- package/lib/commonjs/ui/components/BottomSheet.js.map +0 -1
- package/lib/commonjs/ui/components/BottomSheetRouter.js +0 -298
- package/lib/commonjs/ui/components/BottomSheetRouter.js.map +0 -1
- package/lib/module/ui/components/BottomSheet.js +0 -45
- package/lib/module/ui/components/BottomSheet.js.map +0 -1
- package/lib/module/ui/components/BottomSheetRouter.js +0 -292
- package/lib/module/ui/components/BottomSheetRouter.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/BottomSheet.d.ts +0 -29
- package/lib/typescript/commonjs/ui/components/BottomSheet.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts +0 -9
- package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/BottomSheet.d.ts +0 -29
- package/lib/typescript/module/ui/components/BottomSheet.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts +0 -9
- package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts.map +0 -1
- package/src/ui/components/BottomSheet.tsx +0 -48
- package/src/ui/components/BottomSheetRouter.tsx +0 -310
- package/src/ui/navigation/__tests__/getSheetConfig.test.ts +0 -36
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, type ErrorInfo } from 'react';
|
|
2
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useStore } from 'zustand';
|
|
4
|
+
import { useTheme } from '@oxyhq/bloom/theme';
|
|
5
|
+
import type { SurfaceControls } from '@oxyhq/bloom/surfaces';
|
|
6
|
+
import type { RouteName } from '../navigation/routes';
|
|
7
|
+
import { getScreenComponent } from '../navigation/routes';
|
|
8
|
+
import type { SurfaceNavStack } from '../navigation/surfaceNavStack';
|
|
9
|
+
import type { SurfacePresentation } from '../navigation/surfaceRegistry';
|
|
10
|
+
import {
|
|
11
|
+
closeAllRouteSurfaces,
|
|
12
|
+
navigateWithinOrPresent,
|
|
13
|
+
presentRoute,
|
|
14
|
+
replaceWithinOrPresent,
|
|
15
|
+
} from '../navigation/surfaces';
|
|
16
|
+
import type { BaseScreenProps } from '../types/navigation';
|
|
17
|
+
|
|
18
|
+
/** Error boundary catching screen render failures (e.g. a lazy `require()` throw). */
|
|
19
|
+
interface ScreenErrorBoundaryState {
|
|
20
|
+
error: Error | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ScreenErrorBoundary extends React.Component<
|
|
24
|
+
{ screenName: string; children: React.ReactNode },
|
|
25
|
+
ScreenErrorBoundaryState
|
|
26
|
+
> {
|
|
27
|
+
state: ScreenErrorBoundaryState = { error: null };
|
|
28
|
+
|
|
29
|
+
static getDerivedStateFromError(error: Error): ScreenErrorBoundaryState {
|
|
30
|
+
return { error };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
34
|
+
if (__DEV__) {
|
|
35
|
+
console.error(
|
|
36
|
+
`[SurfaceScreen] Screen "${this.props.screenName}" crashed:`,
|
|
37
|
+
error,
|
|
38
|
+
info.componentStack,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
componentDidUpdate(prevProps: { screenName: string }): void {
|
|
44
|
+
if (prevProps.screenName !== this.props.screenName && this.state.error) {
|
|
45
|
+
this.setState({ error: null });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
render(): React.ReactNode {
|
|
50
|
+
if (this.state.error) {
|
|
51
|
+
return (
|
|
52
|
+
<View style={errorStyles.container}>
|
|
53
|
+
<Text style={errorStyles.title}>Something went wrong</Text>
|
|
54
|
+
{__DEV__ && <Text style={errorStyles.message}>{this.state.error.message}</Text>}
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return this.props.children;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const errorStyles = StyleSheet.create({
|
|
63
|
+
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
|
|
64
|
+
title: { fontSize: 16, fontWeight: '600', marginBottom: 8 },
|
|
65
|
+
message: { fontSize: 13, textAlign: 'center' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export interface SurfaceScreenProps {
|
|
69
|
+
/** This surface's route history (the NAV-WITHIN axis). */
|
|
70
|
+
navStack: SurfaceNavStack;
|
|
71
|
+
/** Bloom's per-surface controls (dismiss / present a child) — the DEPTH axis. */
|
|
72
|
+
surface: SurfaceControls;
|
|
73
|
+
/** This surface's presentation, so `navigate` knows when to drill in vs. stack. */
|
|
74
|
+
presentation: SurfacePresentation;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Binds one SDK surface to its content: resolves the lazy screen for the current
|
|
79
|
+
* nav-stack frame and renders it inside the error boundary with the per-surface
|
|
80
|
+
* navigation props. Mounted by `surfaces.present` as the Bloom surface's content.
|
|
81
|
+
*
|
|
82
|
+
* Dismissal is driven through the nav stack's `closing` flag (watched in an
|
|
83
|
+
* effect) rather than a captured-controls call, so a dismiss requested before
|
|
84
|
+
* this surface mounted still resolves the Bloom surface exactly once.
|
|
85
|
+
*/
|
|
86
|
+
function SurfaceScreen({ navStack, surface, presentation }: SurfaceScreenProps) {
|
|
87
|
+
const theme = useTheme();
|
|
88
|
+
const state = useStore(navStack.store);
|
|
89
|
+
const top = state.frames[state.frames.length - 1];
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (state.closing) surface.dismiss(state.closeResult);
|
|
93
|
+
}, [state.closing, state.closeResult, surface]);
|
|
94
|
+
|
|
95
|
+
const ScreenComponent = useMemo(() => getScreenComponent(top.route) ?? null, [top.route]);
|
|
96
|
+
|
|
97
|
+
const navigate = useCallback(
|
|
98
|
+
(route: RouteName, props?: Record<string, unknown>) =>
|
|
99
|
+
navigateWithinOrPresent(presentation, navStack, route, props),
|
|
100
|
+
[navStack, presentation],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const replace = useCallback(
|
|
104
|
+
(route: RouteName, props?: Record<string, unknown>) =>
|
|
105
|
+
replaceWithinOrPresent(presentation, navStack, route, props),
|
|
106
|
+
[navStack, presentation],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const dismiss = useCallback((result?: unknown) => navStack.requestDismiss(result), [navStack]);
|
|
110
|
+
|
|
111
|
+
const canGoBack = useCallback(() => navStack.canGoBack(), [navStack]);
|
|
112
|
+
|
|
113
|
+
const setStep = useCallback((step: number) => navStack.setStep(step), [navStack]);
|
|
114
|
+
|
|
115
|
+
// Screen-facing back: pop a frame, else step a wizard back, else dismiss the
|
|
116
|
+
// surface — the exact behaviour the single `BottomSheetRouter` had per session.
|
|
117
|
+
const goBack = useCallback((): void => {
|
|
118
|
+
if (navStack.goBack()) return;
|
|
119
|
+
const current = navStack.getTop();
|
|
120
|
+
if (current.step > 0) {
|
|
121
|
+
navStack.setStep(current.step - 1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
navStack.requestDismiss();
|
|
125
|
+
}, [navStack]);
|
|
126
|
+
|
|
127
|
+
const present = useCallback(
|
|
128
|
+
(route: RouteName, props?: Record<string, unknown>) => presentRoute(route, props).result,
|
|
129
|
+
[],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const screenProps = useMemo<BaseScreenProps>(() => {
|
|
133
|
+
const { initialStep: _omitInitialStep, ...rest } = top.props;
|
|
134
|
+
return {
|
|
135
|
+
// NAV-WITHIN
|
|
136
|
+
navigate,
|
|
137
|
+
goBack,
|
|
138
|
+
replace,
|
|
139
|
+
canGoBack,
|
|
140
|
+
initialStep: top.step,
|
|
141
|
+
step: top.step,
|
|
142
|
+
onStepChange: setStep,
|
|
143
|
+
setStep,
|
|
144
|
+
currentScreen: top.route,
|
|
145
|
+
// DEPTH — `onClose`/`onAuthenticated` unwind the whole session (matching the
|
|
146
|
+
// old `closeBottomSheet`); `dismiss`/`present` are the per-surface controls.
|
|
147
|
+
onClose: closeAllRouteSurfaces,
|
|
148
|
+
onAuthenticated: closeAllRouteSurfaces,
|
|
149
|
+
dismiss,
|
|
150
|
+
present,
|
|
151
|
+
// Theme + the route's own props.
|
|
152
|
+
theme: theme.mode,
|
|
153
|
+
...rest,
|
|
154
|
+
};
|
|
155
|
+
}, [navigate, goBack, replace, canGoBack, setStep, present, dismiss, theme.mode, top.route, top.step, top.props]);
|
|
156
|
+
|
|
157
|
+
if (!ScreenComponent) return null;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<ScreenErrorBoundary screenName={top.route}>
|
|
161
|
+
<ScreenComponent {...screenProps} />
|
|
162
|
+
</ScreenErrorBoundary>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default SurfaceScreen;
|
|
@@ -4,23 +4,15 @@ import { Image as ExpoImage } from 'expo-image';
|
|
|
4
4
|
import { Ionicons } from '@expo/vector-icons';
|
|
5
5
|
import { Dialog, type DialogControlProps } from '@oxyhq/bloom';
|
|
6
6
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
7
|
-
import type { RNFileDescriptor } from '@oxyhq/core';
|
|
8
7
|
import { formatFileSize, getFileIcon } from '../../utils/fileManagement';
|
|
9
|
-
|
|
10
|
-
interface PendingFile {
|
|
11
|
-
file: File | Blob | RNFileDescriptor;
|
|
12
|
-
preview?: string;
|
|
13
|
-
size: number;
|
|
14
|
-
name: string;
|
|
15
|
-
type: string;
|
|
16
|
-
}
|
|
8
|
+
import type { PendingUploadFile } from '../../screens/fileManagement/shared';
|
|
17
9
|
|
|
18
10
|
interface UploadPreviewProps {
|
|
19
11
|
control?: DialogControlProps;
|
|
20
|
-
pendingFiles:
|
|
12
|
+
pendingFiles: PendingUploadFile[];
|
|
21
13
|
onConfirm: () => void;
|
|
22
14
|
onCancel: () => void;
|
|
23
|
-
onRemoveFile: (
|
|
15
|
+
onRemoveFile: (id: string) => void;
|
|
24
16
|
inline?: boolean;
|
|
25
17
|
}
|
|
26
18
|
|
|
@@ -34,10 +26,10 @@ const previewStyles = StyleSheet.create({
|
|
|
34
26
|
});
|
|
35
27
|
|
|
36
28
|
const UploadPreviewContent: React.FC<{
|
|
37
|
-
pendingFiles:
|
|
29
|
+
pendingFiles: PendingUploadFile[];
|
|
38
30
|
onConfirm: () => void;
|
|
39
31
|
onCancel: () => void;
|
|
40
|
-
onRemoveFile: (
|
|
32
|
+
onRemoveFile: (id: string) => void;
|
|
41
33
|
showActions?: boolean;
|
|
42
34
|
}> = ({
|
|
43
35
|
pendingFiles,
|
|
@@ -61,11 +53,11 @@ const UploadPreviewContent: React.FC<{
|
|
|
61
53
|
</View>
|
|
62
54
|
|
|
63
55
|
<ScrollView className="flex-1 p-[16px]">
|
|
64
|
-
{pendingFiles.map((pendingFile
|
|
56
|
+
{pendingFiles.map((pendingFile) => {
|
|
65
57
|
const isImage = pendingFile.type.startsWith('image/');
|
|
66
58
|
return (
|
|
67
59
|
<View
|
|
68
|
-
key={
|
|
60
|
+
key={pendingFile.id}
|
|
69
61
|
className="bg-secondary border-border flex-row items-center p-[12px] rounded-[12px] border mb-[12px] gap-[12px]"
|
|
70
62
|
>
|
|
71
63
|
{isImage && pendingFile.preview ? (
|
|
@@ -96,7 +88,7 @@ const UploadPreviewContent: React.FC<{
|
|
|
96
88
|
</View>
|
|
97
89
|
<TouchableOpacity
|
|
98
90
|
className="p-[4px]"
|
|
99
|
-
onPress={() => onRemoveFile(
|
|
91
|
+
onPress={() => onRemoveFile(pendingFile.id)}
|
|
100
92
|
>
|
|
101
93
|
<Ionicons name="close-circle" size={24} color={colors.error} />
|
|
102
94
|
</TouchableOpacity>
|
|
@@ -45,6 +45,7 @@ import { useDeviceManagement } from '../hooks/useDeviceManagement';
|
|
|
45
45
|
import { getStorageKeys, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
|
|
46
46
|
import type { RouteName } from '../navigation/routes';
|
|
47
47
|
import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
|
|
48
|
+
import { presentDetached, type SurfaceInstance } from '../navigation/surfaces';
|
|
48
49
|
import { useQueryClient, onlineManager } from '@tanstack/react-query';
|
|
49
50
|
import { clearQueryCache } from '../hooks/queryClient';
|
|
50
51
|
import { useAvatarPicker } from '../hooks/useAvatarPicker';
|
|
@@ -643,6 +644,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
643
644
|
const commitSwitchedSessionRef = useRef(commitSwitchedSession);
|
|
644
645
|
commitSwitchedSessionRef.current = commitSwitchedSession;
|
|
645
646
|
|
|
647
|
+
// The live AccountDialog surface (a stacked Bloom surface), while open. Held so
|
|
648
|
+
// `openAccountDialog` can no-op when already open and `closeAccountDialog` /
|
|
649
|
+
// `onSignedIn` can dismiss it. Cleared when the surface settles.
|
|
650
|
+
const accountDialogSurfaceRef = useRef<SurfaceInstance<'AccountDialog'> | null>(null);
|
|
651
|
+
|
|
646
652
|
const accountDialogControllerRef = useRef<AccountDialogController | null>(null);
|
|
647
653
|
if (!accountDialogControllerRef.current) {
|
|
648
654
|
accountDialogControllerRef.current = createAccountDialogController({
|
|
@@ -655,7 +661,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
655
661
|
socketFactory: io,
|
|
656
662
|
commitSession: (session) => handleWebSessionRef.current(session),
|
|
657
663
|
commitSwitchedSession: (session) => commitSwitchedSessionRef.current(session),
|
|
658
|
-
onSignedIn: () =>
|
|
664
|
+
onSignedIn: () => {
|
|
665
|
+
accountDialogSurfaceRef.current?.dismiss();
|
|
666
|
+
setAccountDialogOpen(false);
|
|
667
|
+
},
|
|
659
668
|
openUrl: (url) => {
|
|
660
669
|
if (isWebBrowser()) {
|
|
661
670
|
redirectToAuthorize(url);
|
|
@@ -680,11 +689,28 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
680
689
|
|
|
681
690
|
const openAccountDialog = useCallback((view?: AccountDialogView): void => {
|
|
682
691
|
accountDialogControllerRef.current?.setView(view ?? 'accounts');
|
|
692
|
+
// Present the AccountDialog surface on the shared Bloom stack the FIRST time
|
|
693
|
+
// (subsequent opens just re-point the controller's view above). `presentDetached`
|
|
694
|
+
// keeps it OUT of the `showBottomSheet` route-surface lineage, so closing the
|
|
695
|
+
// bottom-sheet session never touches the account dialog and vice-versa.
|
|
696
|
+
if (!accountDialogSurfaceRef.current) {
|
|
697
|
+
const instance = presentDetached(
|
|
698
|
+
'AccountDialog',
|
|
699
|
+
{ initialView: view ?? 'accounts' },
|
|
700
|
+
{ placement: { base: 'bottom', md: 'center' }, dismissOnBackdrop: false, maxWidth: 420 },
|
|
701
|
+
);
|
|
702
|
+
accountDialogSurfaceRef.current = instance;
|
|
703
|
+
instance.result.finally(() => {
|
|
704
|
+
if (accountDialogSurfaceRef.current === instance) accountDialogSurfaceRef.current = null;
|
|
705
|
+
setAccountDialogOpen(false);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
683
708
|
setAccountDialogOpen(true);
|
|
684
709
|
}, []);
|
|
685
710
|
|
|
686
711
|
const closeAccountDialog = useCallback((): void => {
|
|
687
712
|
accountDialogControllerRef.current?.cancelSignIn();
|
|
713
|
+
accountDialogSurfaceRef.current?.dismiss();
|
|
688
714
|
setAccountDialogOpen(false);
|
|
689
715
|
}, []);
|
|
690
716
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getSurfaceConfig, getSurfacePresentation } from '../surfaceRegistry';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `getSurfaceConfig` resolves which Bloom surface a route is presented in. The
|
|
5
|
+
* flagship image picker (FileManagement in image-only select mode) upgrades to a
|
|
6
|
+
* full-bleed `'fullScreen'` surface that owns its own scrolling; every other
|
|
7
|
+
* route stays on the responsive `'sheet'`. These guard the routing contract.
|
|
8
|
+
*/
|
|
9
|
+
describe('getSurfaceConfig presentation', () => {
|
|
10
|
+
const imageOnlyPickerProps = {
|
|
11
|
+
selectMode: true,
|
|
12
|
+
disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('defaults every route to the responsive sheet', () => {
|
|
16
|
+
expect(getSurfaceConfig('ManageAccount', {}).presentation).toBe('sheet');
|
|
17
|
+
expect(getSurfaceConfig('AvatarCrop', {}).presentation).toBe('sheet');
|
|
18
|
+
expect(getSurfacePresentation('ManageAccount', {})).toBe('sheet');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('routes the FileManagement image-only picker to a full-screen surface', () => {
|
|
22
|
+
const config = getSurfaceConfig('FileManagement', imageOnlyPickerProps);
|
|
23
|
+
expect(config.presentation).toBe('fullScreen');
|
|
24
|
+
// The full-bleed picker owns its own FlatList → the host must not wrap it.
|
|
25
|
+
expect(config.scrollable).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps FileManagement on the sheet when it is not an image-only picker', () => {
|
|
29
|
+
// Browse mode (no selectMode) — the standard file manager.
|
|
30
|
+
expect(getSurfaceConfig('FileManagement', {}).presentation).toBe('sheet');
|
|
31
|
+
// Select mode that still allows non-image types is not the flagship picker.
|
|
32
|
+
expect(
|
|
33
|
+
getSurfaceConfig('FileManagement', {
|
|
34
|
+
selectMode: true,
|
|
35
|
+
disabledMimeTypes: ['video/'],
|
|
36
|
+
}).presentation,
|
|
37
|
+
).toBe('sheet');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,88 +1,61 @@
|
|
|
1
1
|
import type { RouteName } from './routes';
|
|
2
2
|
import { isValidRoute } from './routes';
|
|
3
|
-
import {
|
|
3
|
+
import { getSurfacePresentation } from './surfaceRegistry';
|
|
4
|
+
import { closeAllRouteSurfaces, presentRoute, topRouteSurface } from './surfaces';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Bottom
|
|
7
|
+
* Bottom-sheet manager — the INTERNAL adapter that keeps the historical
|
|
8
|
+
* `showBottomSheet` / `closeBottomSheet` API working on top of the new Bloom
|
|
9
|
+
* surface stack (`navigation/surfaces.ts`). The public signatures are unchanged;
|
|
10
|
+
* this file just re-expresses them as thin calls into the typed surface layer.
|
|
11
|
+
*
|
|
12
|
+
* The old single-surface store is gone: `showBottomSheet` now open-or-navigates
|
|
13
|
+
* the "base" (top-most) route surface, and cross-presentation targets (the
|
|
14
|
+
* full-bleed image picker) stack a NEW surface on top — Bloom coordinates the
|
|
15
|
+
* z-order / backdrop / dismiss so nothing clashes.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
currentScreen: null,
|
|
20
|
-
screenProps: {},
|
|
21
|
-
currentStep: undefined,
|
|
22
|
-
history: [],
|
|
23
|
-
isOpen: false,
|
|
24
|
-
fullScreen: false,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const bottomSheetStore = createStore<BottomSheetState>(() => initialState);
|
|
28
|
-
|
|
29
|
-
export const getState = () => bottomSheetStore.getState();
|
|
30
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Open a bottom-sheet route. If the top-most active surface hosts the SAME
|
|
20
|
+
* presentation (the common all-sheets case) the route is drilled into it; a route
|
|
21
|
+
* with a different presentation (the image picker's full-bleed surface) is
|
|
22
|
+
* presented as a NEW surface on top. Opening with no active surface presents the
|
|
23
|
+
* first one.
|
|
24
|
+
*
|
|
25
|
+
* `fullScreen` is accepted for signature compatibility and ignored — a route's
|
|
26
|
+
* surface is now derived from its registry config, not a per-call flag.
|
|
27
|
+
*/
|
|
31
28
|
export const showBottomSheet = (
|
|
32
|
-
|
|
29
|
+
screenOrConfig:
|
|
30
|
+
| RouteName
|
|
31
|
+
| { screen: RouteName; props?: Record<string, unknown>; fullScreen?: boolean },
|
|
33
32
|
): void => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
history: [...state.history, {
|
|
49
|
-
screen: state.currentScreen,
|
|
50
|
-
props: state.screenProps,
|
|
51
|
-
step: state.currentStep,
|
|
52
|
-
}],
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
bottomSheetStore.setState({
|
|
57
|
-
currentScreen: screen,
|
|
58
|
-
screenProps: props,
|
|
59
|
-
currentStep: typeof props.initialStep === 'number' ? props.initialStep : undefined,
|
|
60
|
-
isOpen: true,
|
|
61
|
-
fullScreen,
|
|
62
|
-
});
|
|
33
|
+
const screen = typeof screenOrConfig === 'string' ? screenOrConfig : screenOrConfig.screen;
|
|
34
|
+
const props = typeof screenOrConfig === 'string' ? {} : screenOrConfig.props ?? {};
|
|
35
|
+
|
|
36
|
+
if (!isValidRoute(screen)) {
|
|
37
|
+
if (__DEV__) console.warn(`[BottomSheet] Invalid route: ${screen}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const top = topRouteSurface();
|
|
42
|
+
if (top && top.presentation === getSurfacePresentation(screen, props)) {
|
|
43
|
+
top.navigate(screen, props);
|
|
44
|
+
} else {
|
|
45
|
+
presentRoute(screen, props);
|
|
46
|
+
}
|
|
63
47
|
};
|
|
64
48
|
|
|
49
|
+
/** Dismiss the whole bottom-sheet session (every active route surface). */
|
|
65
50
|
export const closeBottomSheet = (): void => {
|
|
66
|
-
|
|
51
|
+
closeAllRouteSurfaces();
|
|
67
52
|
};
|
|
68
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Step back within the top-most surface's route history. Returns `true` when a
|
|
56
|
+
* frame was popped, `false` when the surface is already at its root frame.
|
|
57
|
+
*/
|
|
69
58
|
export const goBack = (): boolean => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (history.length > 0) {
|
|
73
|
-
const prev = history[history.length - 1];
|
|
74
|
-
bottomSheetStore.setState({
|
|
75
|
-
currentScreen: prev.screen,
|
|
76
|
-
screenProps: prev.props,
|
|
77
|
-
currentStep: prev.step,
|
|
78
|
-
history: history.slice(0, -1),
|
|
79
|
-
});
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return false;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const updateState = (updates: Partial<BottomSheetState>) => {
|
|
87
|
-
bottomSheetStore.setState((state) => ({ ...state, ...updates }));
|
|
59
|
+
const top = topRouteSurface();
|
|
60
|
+
return top ? top.goBack() : false;
|
|
88
61
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentType
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
2
|
import type { BaseScreenProps } from '../types/navigation';
|
|
3
3
|
|
|
4
4
|
// Lazy loading: Screens are loaded on-demand to break require cycles
|
|
@@ -42,7 +42,8 @@ export type RouteName =
|
|
|
42
42
|
| 'AvatarCrop' // Square-crop editor presented before avatar upload
|
|
43
43
|
| 'Notifications' // Per-channel notification preferences
|
|
44
44
|
| 'ConnectedApps' // OAuth-authorized third-party apps the user can revoke
|
|
45
|
-
| 'Preferences'
|
|
45
|
+
| 'Preferences' // General user preferences (theme, reduce-motion, etc.)
|
|
46
|
+
| 'AccountDialog'; // Unified account switcher + sign-in surface (folded OxyAccountDialog body)
|
|
46
47
|
|
|
47
48
|
// Lazy screen loaders - functions that return screen components on-demand
|
|
48
49
|
// This breaks the require cycle by deferring imports until screens are actually needed
|
|
@@ -86,6 +87,10 @@ const screenLoaders: Record<RouteName, () => ComponentType<BaseScreenProps>> = {
|
|
|
86
87
|
Notifications: () => require('../screens/NotificationsScreen').default,
|
|
87
88
|
ConnectedApps: () => require('../screens/ConnectedAppsScreen').default,
|
|
88
89
|
Preferences: () => require('../screens/PreferencesScreen').default,
|
|
90
|
+
// Unified account switcher + sign-in surface. Its body lives in the
|
|
91
|
+
// `OxyAccountDialog` component (folded from the standalone dialog); the
|
|
92
|
+
// surface stack provides the Dialog chrome around it.
|
|
93
|
+
AccountDialog: () => require('../components/OxyAccountDialog').default,
|
|
89
94
|
};
|
|
90
95
|
|
|
91
96
|
// Cache loaded components to avoid re-requiring
|
|
@@ -121,107 +126,3 @@ export const getScreenComponent = (routeName: RouteName): ComponentType<BaseScre
|
|
|
121
126
|
export const isValidRoute = (routeName: string): routeName is RouteName => {
|
|
122
127
|
return routeName in screenLoaders;
|
|
123
128
|
};
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Configuration that BottomSheetRouter applies to the underlying BottomSheet
|
|
127
|
-
* for a given route. Adding new options here is additive — never rename or
|
|
128
|
-
* remove fields without bumping the consumer surface as a breaking change.
|
|
129
|
-
*/
|
|
130
|
-
export interface SheetRouteConfig {
|
|
131
|
-
/**
|
|
132
|
-
* Which surface hosts the route. `'sheet'` (default) renders the screen in
|
|
133
|
-
* the in-tree `BottomSheet`. `'dialog'` renders it in a responsive Bloom
|
|
134
|
-
* `<Dialog>` (bottom-sheet on narrow, centered card on `md+`) — used for the
|
|
135
|
-
* flagship image picker, which reads better as a centered panel on wide
|
|
136
|
-
* viewports than a full-width sheet. `BottomSheetRouter` keeps BOTH surfaces
|
|
137
|
-
* mounted and routes the active screen into exactly one based on this value.
|
|
138
|
-
*/
|
|
139
|
-
presentation: 'sheet' | 'dialog';
|
|
140
|
-
/**
|
|
141
|
-
* When `false`, BottomSheet skips its internal ScrollView and lets the
|
|
142
|
-
* screen own scrolling. Required for screens that render a FlatList,
|
|
143
|
-
* SectionList, or any other VirtualizedList — nesting one inside a
|
|
144
|
-
* plain ScrollView breaks windowing and triggers a RN warning.
|
|
145
|
-
*
|
|
146
|
-
* Ignored for `presentation: 'dialog'` routes — those don't render in the
|
|
147
|
-
* in-tree `BottomSheet` at all.
|
|
148
|
-
*/
|
|
149
|
-
scrollable: boolean;
|
|
150
|
-
/**
|
|
151
|
-
* Controls the body-pan activation strategy on the underlying bloom
|
|
152
|
-
* `BottomSheet`. `true` uses RNGH's `manualActivation` with scroll-handoff
|
|
153
|
-
* (recommended for scrollable content — the only RNGH 2.x pattern that
|
|
154
|
-
* doesn't steal vertical events from the inner scroller on Android).
|
|
155
|
-
* `false` uses an always-active body pan that gates on scroll offset.
|
|
156
|
-
*
|
|
157
|
-
* Defaults to `true` for all routes — matches the historical in-tree
|
|
158
|
-
* BottomSheet behavior. Per-route opt-out is possible if a screen needs
|
|
159
|
-
* the always-active pan instead.
|
|
160
|
-
*/
|
|
161
|
-
manualActivation: boolean;
|
|
162
|
-
/**
|
|
163
|
-
* When `true`, the backdrop dims proportionally with drag distance (iOS
|
|
164
|
-
* Photos style). Defaults to `true` for all routes — matches the
|
|
165
|
-
* historical in-tree BottomSheet behavior.
|
|
166
|
-
*/
|
|
167
|
-
dynamicBackdrop: boolean;
|
|
168
|
-
/**
|
|
169
|
-
* Optional custom handle slot. When provided, replaces the default
|
|
170
|
-
* 36×5 pill drag handle. The handle remains unconditionally draggable
|
|
171
|
-
* via the dedicated handle gesture (when `manualActivation` is `true`).
|
|
172
|
-
* Use sparingly — screens should default to the standard handle for
|
|
173
|
-
* platform consistency.
|
|
174
|
-
*/
|
|
175
|
-
handleComponent?: () => ReactNode;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Predicate matching FileManagementScreen's internal `isImageOnlyPicker`
|
|
180
|
-
* derivation. When the consumer restricts to image MIME types (no videos,
|
|
181
|
-
* no audio, no documents), FileManagement renders the flagship PhotoPickerView
|
|
182
|
-
* which owns its own FlatList. The sheet must therefore stop scrolling
|
|
183
|
-
* children. Kept in sync with `FileManagementScreen` — both check the same
|
|
184
|
-
* disabled MIME type families.
|
|
185
|
-
*/
|
|
186
|
-
const isFileManagementImageOnlyPicker = (props: Record<string, unknown>): boolean => {
|
|
187
|
-
if (!props.selectMode) return false;
|
|
188
|
-
const disabled = props.disabledMimeTypes;
|
|
189
|
-
if (!Array.isArray(disabled) || disabled.length === 0) return false;
|
|
190
|
-
const has = (predicate: (mt: string) => boolean): boolean =>
|
|
191
|
-
disabled.some((mt) => typeof mt === 'string' && predicate(mt));
|
|
192
|
-
const blocksVideos = has((mt) => mt === 'video/' || mt.startsWith('video/'));
|
|
193
|
-
const blocksAudio = has((mt) => mt === 'audio/' || mt.startsWith('audio/'));
|
|
194
|
-
const blocksDocs = has(
|
|
195
|
-
(mt) => mt === 'application/pdf' || mt === 'application/' || mt.startsWith('application/'),
|
|
196
|
-
);
|
|
197
|
-
return blocksVideos && blocksAudio && blocksDocs;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
/** Defaults shared across all routes — preserves the historical in-tree BS UX. */
|
|
201
|
-
const DEFAULT_SHEET_CONFIG: SheetRouteConfig = {
|
|
202
|
-
presentation: 'sheet',
|
|
203
|
-
scrollable: true,
|
|
204
|
-
manualActivation: true,
|
|
205
|
-
dynamicBackdrop: true,
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Returns the bottom-sheet configuration for a route. Defaults match the
|
|
210
|
-
* pre-refactor in-tree `BottomSheet` (scrollable, manualActivation,
|
|
211
|
-
* dynamicBackdrop). Routes opt out per-field as needed.
|
|
212
|
-
*/
|
|
213
|
-
export const getSheetConfig = (
|
|
214
|
-
routeName: RouteName | null,
|
|
215
|
-
screenProps: Record<string, unknown>,
|
|
216
|
-
): SheetRouteConfig => {
|
|
217
|
-
if (!routeName) return DEFAULT_SHEET_CONFIG;
|
|
218
|
-
if (routeName === 'FileManagement' && isFileManagementImageOnlyPicker(screenProps)) {
|
|
219
|
-
// The flagship photo picker renders in a responsive Bloom `<Dialog>`
|
|
220
|
-
// (bottom-sheet on narrow, centered card on `md+`) rather than the
|
|
221
|
-
// in-tree BottomSheet — it owns its own FlatList and reads better as a
|
|
222
|
-
// centered panel on wide viewports. The Dialog surface, not this
|
|
223
|
-
// config's `scrollable`, governs its scrolling.
|
|
224
|
-
return { ...DEFAULT_SHEET_CONFIG, presentation: 'dialog' };
|
|
225
|
-
}
|
|
226
|
-
return DEFAULT_SHEET_CONFIG;
|
|
227
|
-
};
|